You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
19 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 加餐 | 我是如何使用tracepoint来分析内核Bug的
你好,我是邵亚方。
我们这个系列课程的目标受众是应用开发者和运维人员所以你可以看到课程里的案例在分析应用问题的时候都在尽量避免分析内核bug避免把内核代码拿过来逐个函数地解析为什么会这样。我希望这个课程可以降低内核的门槛让更多人可以更加容易地了解内核机制从而更好地解决应用难题、提升应用性能。
不过在我们这个课程的学习者中还是有一些内核开发者的因此我写了这篇加餐来分析内核bug希望能把分析内核bug的一些经验分享给这些内核开发者们。
通过对课程的学习你应该能发现我对tracepoint和ftrace是极其推崇的。我对它推崇备至不是没有道理的这节课我就带你来看下我是如何借助tracepoint来分析内核bug的。
## 炫技般存在的tracepoint内核源码
如果你看过tracepoint的内核代码相信你一定对它炫技般存在的[宏定义](https://elixir.bootlin.com/linux/v5.9-rc6/source/include/linux/tracepoint.h#L229)印象深刻。我在第一眼看到这些宏定义时也是一脸懵逼不知从何下手但是很快我就看懂了。为了证明我看懂了我还特意给tracepoint的这些宏定义[又增加了一些定义](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc8&id=163363455b42a1cf833742177149d1352dfe673e),我增加的这个宏定义,其关键部分如下:
![](https://static001.geekbang.org/resource/image/51/5e/51b5c43325b69edae6ae5a73cd20b75e.png)
如果你能看明白这些那就说明你对这些tracepoint宏的工作机制一清二楚了。当然这节课我不是来剖析tracepoint内核源码的。如果你不懂tracepoint内核源码也不妨碍你使用它不过这对一名内核开发者而言终究是一件憾事。
因为我经常使用tracepoint所以我对tracepoint的一些功能也比较上心。比如最近在我的推动下tracepoint模块的maintainer Steven Rostedt[又给tracepoint增加了一个宏定义](https://lore.kernel.org/linux-mm/20200925211206.423598568@goodmis.org/)。我之所以推动Steven增加该宏是为了让tracepoint函数可以在头文件中使用以减少因为额外函数调用而带来的开销。有了这个新增的宏之后你就可以方便地在头文件中使用tracepoint了。
接下来我要讲的这个内核bug就是借助tracepoint来分析的。
## 内存申请在慢速路径上为什么会失败
有一次业务人员反馈说他们在启动程序时会偶然失败我查看内核日志后发现了下面这些报错信息这个系统为CentOS-7对应的内核版本为3.10
```
kworker/31:0: page allocation failure: order:5, mode:0x104050
CPU: 31 PID: 635928 Comm: kworker/31:0 Tainted: G
0000000000104050 000000009a44a60e ffff882016b93808 ffffffff81686b13
ffff882016b93898 ffffffff81187010 0000000000000000 ffff88207ffd8000
0000000000000005 0000000000104050 ffff882016b93898 000000009a44a60e
Call Trace:
[<ffffffff81686b13>] dump_stack+0x19/0x1b
[<ffffffff81187010>] warn_alloc_failed+0x110/0x180
[<ffffffff816826a7>] __alloc_pages_slowpath+0x6b7/0x725
[<ffffffff8118b5c5>] __alloc_pages_nodemask+0x405/0x420
[<ffffffff811cf77a>] alloc_pages_current+0xaa/0x170
[<ffffffff81185eee>] __get_free_pages+0xe/0x50
[<ffffffff811db01e>] kmalloc_order_trace+0x2e/0xa0
[<ffffffff811e05d9>] __kmalloc_track_caller+0x219/0x230
[<ffffffff8119f78f>] krealloc+0x4f/0xa0
[<ffffffffa07eebe6>] osdmap_set_max_osd+0x76/0x1d0 [libceph]
[<ffffffffa07f14f6>] ceph_osdmap_decode+0x216/0x600 [libceph]
[<ffffffffa07ecce4>] handle_one_map+0x224/0x250 [libceph]
[<ffffffffa07ed98f>] ceph_osdc_handle_map+0x6cf/0x720 [libceph]
[<ffffffffa07e3340>] dispatch+0x350/0x7c0 [libceph]
[<ffffffffa07deecf>] try_read+0x4df/0x1260 [libceph]
[<ffffffffa07dfd09>] ceph_con_workfn+0xb9/0x650 [libceph]
[<ffffffff810a845b>] process_one_work+0x17b/0x470
[<ffffffff810a9296>] worker_thread+0x126/0x410
[<ffffffff810b0a4f>] kthread+0xcf/0xe0
[<ffffffff81697118>] ret_from_fork+0x58/0x90
Mem-Info:
active_anon:13891624 inactive_anon:358552 isolated_anon:0#012 active_file:1652146 inactive_file:14520052 isolated_file:0#012 unevictable:0 dirty:472 writeback:0 unstable:0#012 slab_reclaimable:1071185 slab_unreclaimable:201037#012 mapped:53234 shmem:1046930 pagetables:31965 bounce:0#012 free:966225 free_pcp:185 free_cma:0
Node 0 DMA free:15864kB min:48kB low:60kB high:72kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15948kB managed:15864kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
lowmem_reserve[]: 0 1700 64161 64161
Node 0 DMA32 free:261328kB min:5412kB low:6764kB high:8116kB active_anon:303220kB inactive_anon:2972kB active_file:51840kB inactive_file:239320kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:1952512kB managed:1741564kB mlocked:0kB dirty:0kB writeback:0kB mapped:1160kB shmem:15968kB slab_reclaimable:687588kB slab_unreclaimable:183756kB kernel_stack:6640kB pagetables:724kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
lowmem_reserve[]: 0 0 62460 62460
Node 0 Normal free:272880kB min:198808kB low:248508kB high:298212kB active_anon:26813396kB inactive_anon:912660kB active_file:2646440kB inactive_file:30619568kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:65011712kB managed:63960000kB mlocked:0kB dirty:36kB writeback:0kB mapped:144152kB shmem:2567180kB slab_reclaimable:1870364kB slab_unreclaimable:262608kB kernel_stack:12880kB pagetables:45840kB unstable:0kB bounce:0kB free_pcp:652kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:32 all_unreclaimable? no
lowmem_reserve[]: 0 0 0 0
Node 1 Normal free:3315332kB min:205324kB low:256652kB high:307984kB active_anon:28449880kB inactive_anon:518576kB active_file:3910304kB inactive_file:27221320kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:67108864kB managed:66056916kB mlocked:0kB dirty:1852kB writeback:0kB mapped:67624kB shmem:1604572kB slab_reclaimable:1726788kB slab_unreclaimable:357784kB kernel_stack:18928kB pagetables:81296kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
lowmem_reserve[]: 0 0 0 0
Node 0 DMA: 0*4kB 1*8kB (U) 1*16kB (U) 1*32kB (U) 1*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15864kB
Node 0 DMA32: 36913*4kB (UEM) 14087*8kB (UEM) 44*16kB (UEM) 17*32kB (UEM) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 261596kB
Node 0 Normal: 69629*4kB (UEM) 411*8kB (UEM) 1*16kB (E) 3*32kB (E) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 281916kB
Node 1 Normal: 241701*4kB (UEM) 240734*8kB (UEM) 24010*16kB (UEM) 990*32kB (UEM) 175*64kB (UEM) 20*128kB (UEM) 3*256kB (EM) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 3323044kB
Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
Node 1 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
```
从上面的日志可以看出,这是在\_\_alloc\_pages\_slowpath()分配内存失败,所以有了这个打印:
```
__alloc_pages_slowpath
{
...
nopage:
// 这里打印的错误日志
warn_alloc_failed(gfp_mask, order, NULL);
return NULL;
}
```
此时申请的内存大小是order 5也就是32个连续页。紧接着我们可以看到各个node具体内存使用情况的打印该机器共有2个node
```
Node 0 DMA free:15864kB min:48kB
Node 0 DMA32 free:261328kB min:5412kB
Node 0 Normal free:272880kB min:198808kB
Node 1 Normal free:3315332kB min:205324kB
```
从中我们可以发现各个zone的free内存都大于min而且相差不止32个page。也就是说从free内存的大小来看各个zone都是可以满足需求的。那么为什么此时会申请内存失败呢
接下来,我们一起分析失败的原因。
## 逐一排查可能的情况
对于3.10版本的内核而言,在内存分配慢速路径里失败,原因可以分为以下三种情况:
* 特殊的GFP flags导致
* 进程自身的状态;
* reclaim和compact无法满足需求。
现在,我们来逐一排查究竟是哪种情况导致的。
#### GFP flags
此时的GFP flags是0x104050对应于下面这几项
```
#define ___GFP_WAIT 0x10u
#define ___GFP_IO 0x40u
#define ___GFP_COMP 0x4000u
#define ___GFP_KMEMCG 0x100000u
```
看到这里你是否思考过为什么不直接在内核日志里打印出这些GFP flags呢如果你思考了那么恭喜你你具备内核开发者的特质如果你没有思考过那么你需要加强这方面的思考我觉得内核这里有点不好我得改变它。
我觉得内核日志里打印这些数字不如直接打印对应的GFP flags好然后我就去查看最新的内核代码发现这部分已经在新版本的内核里被修改过了看来其他的内核开发者与我的想法一致。当然这也说明了使用老版本的内核做开发是一件多么憋屈的事因为你会发现你在老版本内核里分析清楚的内核bug早已在新版本中被别人给fix了这大大限制了我们的发挥空间。
通过前面的调用栈我们可以知道申请内存是在osdmap\_set\_max\_osd()这个函数中进行的,它对应的内核代码如下:
```
osdmap_set_max_osd
addr = krealloc(map->osd_addr, max*sizeof(*addr), GFP_NOFS);
if (!addr)
return -ENOMEM;
```
我们看到这里的GFP flags为GFP\_NOFS它的定义如下
```
#define GFP_NOFS (__GFP_WAIT | __GFP_IO)
```
这与内核日志里的GFP flags是对得上的。然后我们去比较`goto nopage;`的情况可以发现这几个flag不会导致这种情况也就是不会导致nopage。
关于GFP\_NOFS的作用我在这里大致说明一下。它的作用是为了防止某些路径上触发直接内存回收时回收到正在进行I/O的page从而引起死锁。那什么情况下可能会引起死锁呢你可以参考一下我尚未完成的[PATCHxfs: avoid deadlock when trigger memory reclaim in ->writepages](https://lore.kernel.org/linux-mm/1592222181-9832-1-git-send-email-laoar.shao@gmail.com/)。这个链接里描述的问题在3.10版本以及最新版本的内核上都存在之所以我还没有完成该PATCH是因为它依赖于我的另外一组PATCH而我目前很少有精力去写它们。具体的逻辑你可以看下这个PATCH的代码以及描述我就不在这里细说了。
现在我们排除了GFP flags产生nopage的可能接下来继续看看另外两种情况。
#### 进程标记current->flag
在warn\_alloc\_failed里我们可以看到如果是因为进程自身的状态有问题比如正在退出或者正在oom过程中等那么`SHOW_MEM_FILTER_NODES`这个标记位就会被清掉然后各个zone的具体内存信息就不会被打印出来了。
因此,内存申请在慢速路径上失败也不是因为这个原因。
#### reclaim和compact无法满足需求
现在就只有“reclaim和compact无法满足需求”这一种情况了。
根据前面的信息我们可以知道此时free的内存其实挺多可以排除reclaim无法满足需求的情况。所以只剩下compcat这个因素了。也就是说由于申请的是连续页而系统中没有足够的连续页所以compact也满足不了需求进而导致分配内存失败。
那么在什么情况下compact会失败呢我们继续来看代码
```
try_to_compact_pages
int may_enter_fs = gfp_mask & __GFP_FS;
int may_perform_io = gfp_mask & __GFP_IO;
if (!order || !may_enter_fs || !may_perform_io)
return rc;
```
我们可以看到`__GFP_FS`没有被设置无法进行compaction直接返回了`COMPACT_SKIPPED`。
明白了问题所在后,我们需要在生产环境上验证一下,看看到底是不是这个原因。
## 使用tracepoint分析生产环境
tracepoint是一种性能开销比较小的追踪手段在生产环境上使用它不会给业务带来明显的性能影响。
在使用tracepoint分析前我们需要明确它可以追踪什么事件。
因为我们目前的问题是compact fail所以我们需要去追踪direct compact这个事件。新版本的内核里有compact相关的tracepoint我们直接打开对应的tracepoint就可以了。不过3.10版本的内核没有compact相关的tracepoint这个时候我们就需要借助kprobe机制了最简单的方式是利用ftrace提供的kprobe\_events功能或者是ftrace的function tracer功能。我们以function tracer为例来追踪direct compact
```
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ echo __alloc_pages_direct_compact > /sys/kernel/debug/tracing/set_ftrace_filter
```
这样当发生direct compact时在trace\_pipe中就会有相应的信息输出。不过这显示不了compact的细节我们还需要结合其他手段查看是否进行了compact。方式有很多在这里我们结合源码来看一种比较简单的方式
```
__alloc_pages_direct_compact
try_to_compact_pages
/* Check if the GFP flags allow compaction */
if (!order || !may_enter_fs || !may_perform_io)
return rc;
// 如果可以进行direct compact的话会有COMPACTSTALL事件
count_compact_event(COMPACTSTALL);
```
从源码中我们能看到如果可以进行direct compact的话会有相应的COMPACTSTALL事件而该事件会统计在/proc/vmstat中
```
$ cat /proc/vmstat | grep compact
```
这样我们就可以知道调用\_\_alloc\_pages\_direct\_compact时有没有真正进行compact的行为。另外在compact的过程中还会伴随着direct reclaim我们也需要看下direct reclaim的细节看看direct claim能否成功回收内存。我们可以借助direct reclaim的tracepoint来查看该tracepoint在3.10版本的内核里已经有了:
```
$ echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_begin/enable
$ echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_end/enable
```
在追踪这些事件之后我们就可以看到direct compact前后的信息了。
direct compact前的vmstat指标为
```
$ cat /proc/vmstat | grep compact
compact_migrate_scanned 690141298
compact_free_scanned 186406569096
comoact_isolated 332972232
compact_stall 87914
compact_fail 40935
compact_success 46979
```
compact过程中的事件
![](https://static001.geekbang.org/resource/image/3e/78/3ed8e4629bfbbb0a39b4cdb65f31da78.png)
compact结束后的vmstat指标为
```
$ cat /proc/vmstat | grep compact
compact_migrate_scanned 690141298
compact_free_scanned 186406569096
comoact_isolated 332972232
compact_stall 87914
compact_fail 40935
compact_success 46979
```
我们可以看到在compact前后compcat\_stall这个指标没有任何变化也就是说try\_to\_compact\_pages中没有进行真正的compact行为从direct reclaim事件中的nr\_reclaimed=3067可以看到此时可以回收到足够的page也就是说direct reclaim没有什么问题同样direct reclaim的“order=5, gfp\_flags=GFP\_NOFS|GFP\_COMP” 也与之前日志里的信息吻合。因此,这些追踪数据进一步印证了我们之前的猜测:`__GFP_FS`没有被设置无法进行compaction。
我们现在再次观察申请内存失败时的日志可以发现此时free list上其实有当前order的内存因为没有GFP\_DMA所以会先从NORMAL zone申请内存
```
Node 0 Normal: 69629*4kB (UEM) 411*8kB (UEM) 1*16kB (E) 3*32kB (E) 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 281916kB
Node 1 Normal: 241701*4kB (UEM) 240734*8kB (UEM) 24010*16kB (UEM) 990*32kB (UEM) 175*64kB (UEM) 20*128kB (UEM) 3*256kB (EM) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 3323044kB
```
我们能看到node 1大于order 5即128K的连续物理内存有很多那为什么不能从这些zone->free\_area\[order\]里分配内存呢?
```
答案就在于该zone的水位不满足要求见__zone_watermark_ok()
__zone_watermark_ok
{
...
for (o = 0; o < order; o++) {
free_pages -= z->free_area[o].nr_free << o;
min >>= 1;
if (free_pages <= min)
return false;
}
}
```
对于node 1而言4K/8K/16K/32K/64K内存和为3319716kB该zone的watermark min为205324kB该node的总内存为3323044KB我们可以简单地进行如下比较
```
(3323044-3319716) 为3328KB
(205324kB >> 5) 为6416KB
```
因此order 5无法满足水位。
根据上述这些追踪信息我们可以得出这样的结论在内存分配慢速路径上失败是因为当前的内存申请请求无法进行碎片整理而系统中low order的内存又太多从而导致了该order的内存申请失败。
## 解决方案
因为此时normal zone的order=5的free page依然有很多而整体的watermark又满足需求所以不应该让order=5的内存申请失败这是一个内核缺陷。我去查看upstream的最新代码时发现该缺陷已经被修改过了。你可以看到使用老版本的内核做开发的确是一件很憋屈的事。
关于upstream的修改方案你可以参考这个patch以及它的一些依赖
[mm, page\_alloc: only enforce watermarks for order-0 allocations](https://github.com/torvalds/linux/commit/97a16fc82a7c5b0cfce95c05dfb9561e306ca1b1)
如果你无法通过修改内核来解决这一问题的话,那就采取一些规避措施。
* 规避方案一:
通过drop\_caches清理掉pagecache不过这种做法也有很多缺陷具体你可以参考我们这个课程的pagecache模块我在这里就不细说了。
* 规避方案二:
调整碎片指数,确保紧急情况下能够申请到连续页。内存碎片指数对应的文件是`/proc/sys/vm/extfrag_threshold`它的默认值是500 ,我们可以适当降低该值,不过在这种情况下,降低它的效果并不会很明显。
* 规避方案三:
手动compact你可以通过写入/proc/sys/vm/compact\_memory来触发compact。
* 规避方案四:
调整 vm.vfs\_cache\_pressure降低它的值让pagecache被回收得更多以此来减少freelist中order为0的page个数。
至此,我们这个问题的分析就结束了。
## 总结
在比较新的内核上我们也可以通过eBPF来分析内核bug比如在我们的生产环境中我就通过eBPF追踪过fadvise的内核bug引起的业务抖动问题具体bug你可以看看我贡献给内核的这个[PATCH: mm,fadvise: improve the expensive remote LRU cache draining after FADV\_DONTNEED](https://ozlabs.org/~akpm/mmots/broken-out/mm-fadvise-improve-the-expensive-remote-lru-cache-draining-after-fadv_dontneed.patch),这也再次印证了我的观点:内核开发者只有在新版本的内核里做开发,才会有更多的发挥空间。
另外虽然eBPF很强大但是它依然难以替代ftrace在我心中的地位。