Linux Kernel: cgroup v1 の制限下で slab_out_of_memory を発生させて観察する
イントロダクション
cgroup v1 の memory コントローラーで memory.kmem.limit_in_bytes
を制限すると slab_out_of_memory を起こすことができるので、それを調べたり観察したエントリです
不具合・バグの調査ではないです
ソースやコマンドの結果を大量に貼り付けているので めんどうな人は流し読みしてください
環境
vagrant@bionic:~$ uname -a Linux bionic 4.15.0-66-generic #75-Ubuntu SMP Tue Oct 1 05:24:09 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
再現
1. cgroup v1 で memory.kmem.limit_in_bytes だけ制限を課します
root@bionic:~# echo $$ > /sys/fs/cgroup/memory/000/tasks root@bionic:~# echo 100M > /sys/fs/cgroup/memory/000/memory.kmem.limit_in_bytes
2. negative dentry (slab) を大量に生成する
memory.kmem.limit_in_bytes に制限を課したプロセスで、存在しないパスに stat(2) を呼び出しをして negative dentry (slab) を大量に生成します
# # https://hiboma.hatenadiary.jp/entry/20140212/1392131530 # root@bionic:~# perl -e 'stat "/$_" for 1..10000000' ^C
同時に /proc/memoinfo を見て、 slab が増えなくなったら Perl のスクリプトは止めてもいいです
vagrant@bionic:~$ grep -e Slab -e SReclaimable /proc/meminfo Slab: 163288 kB SReclaimable: 131084 kB
さて、もういっぺん Perl を実行しようとすると ... oh
root@bionic:~# perl -e 'stat "/$_" for 1..10000000' -su: fork: Cannot allocate memory root@bionic:~# ls -su: fork: Cannot allocate memory root@bionic:~# ps auxf -su: fork: Cannot allocate memory
3. dmesg の確認
この時 dmesg にはこんなログが出ました
[ 804.033214] slab_out_of_memory: 1089967 callbacks suppressed [ 804.033220] SLUB: Unable to allocate memory on node -1, gfp=0x14000c0(GFP_KERNEL) [ 804.033224] cache: dentry(897:000), object size: 192, buffer size: 192, default order: 0, min order: 0 [ 804.033227] node 0: slabs: 25487, objs: 535227, free: 0 [ 804.033236] SLUB: Unable to allocate memory on node -1, gfp=0x14000c0(GFP_KERNEL) [ 804.033238] cache: dentry(897:000), object size: 192, buffer size: 192, default order: 0, min order: 0 [ 804.033241] node 0: slabs: 25487, objs: 535227, free: 0 [ 804.033247] SLUB: Unable to allocate memory on node -1, gfp=0x14000c0(GFP_KERNEL) [ 804.033250] cache: dentry(897:000), object size: 192, buffer size: 192, default order: 0, min order: 0 [ 954.323173] SLUB: Unable to allocate memory on node -1, gfp=0x15000c0(GFP_KERNEL_ACCOUNT) [ 954.323176] cache: kmalloc-96(897:000), object size: 96, buffer size: 96, default order: 0, min order: 0 [ 954.323179] node 0: slabs: 1, objs: 42, free: 0 [ 955.018891] SLUB: Unable to allocate memory on node -1, gfp=0x15000c0(GFP_KERNEL_ACCOUNT) [ 955.018896] cache: kmalloc-96(897:000), object size: 96, buffer size: 96, default order: 0, min order: 0 [ 955.018899] node 0: slabs: 1, objs: 42, free: 0
(ソースを読んでいくとわかるんですが ) Slab (SLUB) 用のページアロケートに失敗した旨を示すログです
4. slabinfo の確認
この時、 cgroup の slabinfo は下記の通りです. dentry が多いですね
vagrant@bionic:~$ cat /cgroup/000/memory.kmem.slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> proc_inode_cache 24 24 680 24 4 : tunables 0 0 0 : slabdata 1 1 0 radix_tree_node 28 28 584 28 4 : tunables 0 0 0 : slabdata 1 1 0 ext4_inode_cache 30 30 1088 30 8 : tunables 0 0 0 : slabdata 1 1 0 anon_vma 92 92 88 46 1 : tunables 0 0 0 : slabdata 2 2 0 vm_area_struct 76 76 208 19 1 : tunables 0 0 0 : slabdata 4 4 0 mm_struct 30 30 2112 15 8 : tunables 0 0 0 : slabdata 2 2 0 signal_cache 16 16 1024 16 4 : tunables 0 0 0 : slabdata 1 1 0 sighand_cache 15 15 2112 15 8 : tunables 0 0 0 : slabdata 1 1 0 kmalloc-96 42 42 96 42 1 : tunables 0 0 0 : slabdata 1 1 0 kmalloc-2048 18 18 2048 16 8 : tunables 0 0 0 : slabdata 2 2 0 kmalloc-64 128 128 64 64 1 : tunables 0 0 0 : slabdata 2 2 0 files_cache 46 46 704 23 4 : tunables 0 0 0 : slabdata 2 2 0 pid 192 192 64 64 1 : tunables 0 0 0 : slabdata 3 3 0 cred_jar 63 63 192 21 1 : tunables 0 0 0 : slabdata 3 3 0 task_struct 10 10 5824 5 8 : tunables 0 0 0 : slabdata 2 2 0 kmalloc-1024 32 32 1024 16 4 : tunables 0 0 0 : slabdata 2 2 0 kmalloc-192 42 42 192 21 1 : tunables 0 0 0 : slabdata 2 2 0 inode_cache 52 52 608 26 4 : tunables 0 0 0 : slabdata 2 2 0 dentry 535227 535227 192 21 1 : tunables 0 0 0 : slabdata 25487 25487 0 👈 filp 48 48 256 16 1 : tunables 0 0 0 : slabdata 3 3 0
cgroup memory の何らかの上限に達したら reclaim ( slab の回収処理 ) が実行されるのかと思っていたが、そうではないらしい
( memory.limit_in_bytes も設定すると reclaim は実行される )
📗ドキュメントを読む
memory.kmem.limit_in_bytes の設定だけで reclaim が実行されない理由は下記の通り
Currently no soft limit is implemented for kernel memory. It is future work to trigger slab reclaim when those limits are reached.
なんと slab の回収処理は実装されていない. ソフトリミットに限定した話か、ハードリミットにも同様なのか?
📙 ソースを読む
周辺おソースを読んだりトレースをとったりして理解を深めます
SLUB: Unable to allocate memory on node ... のログを出す箇所
slab (SLUB) のログなので mm/*
を対象にして grep をかけて ログを探し出し出して見つかります
static noinline void slab_out_of_memory(struct kmem_cache *s, gfp_t gfpflags, int nid) { #ifdef CONFIG_SLUB_DEBUG static DEFINE_RATELIMIT_STATE(slub_oom_rs, DEFAULT_RATELIMIT_INTERVAL, DEFAULT_RATELIMIT_BURST); int node; struct kmem_cache_node *n; if ((gfpflags & __GFP_NOWARN) || !__ratelimit(&slub_oom_rs)) return; pr_warn("SLUB: Unable to allocate memory on node %d, gfp=%#x(%pGg)\n", 👈 nid, gfpflags, &gfpflags); pr_warn(" cache: %s, object size: %d, buffer size: %d, default order: %d, min order: %d\n", 👈 s->name, s->object_size, s->size, oo_order(s->oo), oo_order(s->min)); if (oo_order(s->min) > get_order(s->object_size)) pr_warn(" %s debugging increased min order, use slub_debug=O to disable.\n", s->name); for_each_kmem_cache_node(s, node, n) { unsigned long nr_slabs; unsigned long nr_objs; unsigned long nr_free; nr_free = count_partial(n, count_free); nr_slabs = node_nr_slabs(n); nr_objs = node_nr_objs(n); pr_warn(" node %d: slabs: %ld, objs: %ld, free: %ld\n", node, nr_slabs, nr_objs, nr_free); } #endif }
CONFIG_SLUB_DEBUG
#ifdef CONFIG_SLUB_DEBUG
が付いてるぞ? Ubuntu Kernel は CONFIG_SLUB_DEBUG=y でビルドしてんですね
vagrant@bionic:~$ sudo grep CONFIG_SLUB_DEBUG /boot/* /boot/config-4.15.0-29-generic:CONFIG_SLUB_DEBUG=y /boot/config-4.15.0-29-generic:# CONFIG_SLUB_DEBUG_ON is not set /boot/config-4.15.0-66-generic:CONFIG_SLUB_DEBUG=y /boot/config-4.15.0-66-generic:# CONFIG_SLUB_DEBUG_ON is not set
bpftrace で確かめる
bpftrace で上記の slab_out_of_memory() がどのようなパスで呼び出されるのかを確かめました
# slab_out_of_memory() は probe として扱えないので、 pr_warn() = printk() を代替の probe として使う vagrant@bionic:~$ sudo bpftrace -e 'kprobe:printk { printf("%d %s\n%s", pid, comm, kstack) }' 2427 perl printk+1 👈 ___slab_alloc+958 👈 __slab_alloc+32 kmem_cache_alloc+379 __d_alloc+41 d_alloc+26 d_alloc_parallel+90 lookup_slow+130 walk_component+451 path_lookupat+132 filename_lookup+182 user_path_at_empty+54 vfs_statx+118 SYSC_newstat+61 sys_newstat+14 do_syscall_64+115 entry_SYSCALL_64_after_hwframe+61 ... 延々と続く
バックトレースを読んで __slab_alloc() が呼び出し元だとわかりました。ソースと付き合わせます
/* * Another one that disabled interrupt and compensates for possible * cpu changes by refetching the per cpu area pointer. */ static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr, struct kmem_cache_cpu *c) { void *p; unsigned long flags; ... new_slab: if (slub_percpu_partial(c)) { page = c->page = slub_percpu_partial(c); slub_set_percpu_partial(c, page); stat(s, CPU_PARTIAL_ALLOC); goto redo; } freelist = new_slab_objects(s, gfpflags, node, &c); if (unlikely(!freelist)) { slab_out_of_memory(s, gfpflags, node); 👈 return NULL; } ...
new_slab_objects() の中を読んでいくと cgroup の制限との関係がわかりそうですね
🔍 /sys/fs/cgroup/*/memory.kmem.failcnt について調べる
/sys/fs/cgroup/*/memory.kmem.failcnt
という統計を取れるファイルがあるので数値をみてみますと、めちゃ増えてる
vagrant@bionic:~$ cat /sys/fs/cgroup/000/memory.kmem.failcnt 6979483
この数値の読み方について、ドキュメントを参照します
5.4 failcnt A memory cgroup provides memory.failcnt and memory.memsw.failcnt files. This failcnt(== failure count) shows the number of times that a usage counter hit its limit. When a memory cgroup hits a limit, failcnt increases and memory under it will be reclaimed. You can reset failcnt by writing 0 to failcnt file. # echo 0 > .../memory.failcnt
memory cgroup の制限に達すると +1 されるようですね
ソースを読む
grep して以下のような定義を見つけました
static struct cftype mem_cgroup_legacy_files[] = { ... { .name = "kmem.failcnt", .private = MEMFILE_PRIVATE(_KMEM, RES_FAILCNT), .write = mem_cgroup_reset, .read_u64 = mem_cgroup_read_u64, }, ...
RES_FAILCNT を手がかりにソースを読み進めていくと、 failcnt は下記の箇所でインクリメントされる実装でした
/** * page_counter_try_charge - try to hierarchically charge pages * @counter: counter * @nr_pages: number of pages to charge * @fail: points first counter to hit its limit, if any * * Returns %true on success, or %false and @fail if the counter or one * of its ancestors has hit its configured limit. */ bool page_counter_try_charge(struct page_counter *counter, unsigned long nr_pages, struct page_counter **fail) { struct page_counter *c; for (c = counter; c; c = c->parent) { long new; /* * Charge speculatively to avoid an expensive CAS. If * a bigger charge fails, it might falsely lock out a * racing smaller charge and send it into reclaim * early, but the error is limited to the difference * between the two sizes, which is less than 2M/4M in * case of a THP locking out a regular page charge. * * The atomic_long_add_return() implies a full memory * barrier between incrementing the count and reading * the limit. When racing with page_counter_limit(), * we either see the new limit or the setter sees the * counter has changed and retries. */ new = atomic_long_add_return(nr_pages, &c->count); if (new > c->limit) { atomic_long_sub(nr_pages, &c->count); /* * This is racy, but we can live with some * inaccuracy in the failcnt. */ c->failcnt++; 👈 *fail = c; goto failed; } /* * Just like with failcnt, we can live with some * inaccuracy in the watermark. */ if (new > c->watermark) c->watermark = new; } return true; failed: for (c = counter; c != *fail; c = c->parent) page_counter_cancel(c, nr_pages); return false; }
bpftrace で確かめる
bpftrace で該当の page_counter_try_charge() がほんとに呼び出されているのかどうかを確かめます
vagrant@bionic:~$ sudo bpftrace -e 'kprobe:page_counter_try_charge { printf("%d %s\n%s", pid, comm, kstack) }' ... 2014 perl page_counter_try_charge+1 👈 new_slab+213 ___slab_alloc+855 __slab_alloc+32 kmem_cache_alloc+379 __d_alloc+41 d_alloc+26 d_alloc_parallel+90 lookup_slow+130 walk_component+451 path_lookupat+132 filename_lookup+182 user_path_at_empty+54 vfs_statx+118 SYSC_newstat+61 sys_newstat+14 do_syscall_64+115 entry_SYSCALL_64_after_hwframe+61 ... 延々と続く
確かに呼び出されているのを確認できました. 途中に __slab_alloc() もでてきています.
尚、 newslab() -> page_counter_try_charge() を呼び出すまでの inline な関数や static な関数は bpftrace では表示されていない
page_counter_try_charge() 🕶 memcg_kmem_charge_memcg() 🕶 memcg_charge_slab() 🕶 alloc_slab_page() 🕶allocate_slab() new_slab() new_slab_objects() 🕶 をつけた関数は出てこない. ソースと突き合わせる時に迷子になりやすい
page_counter_try_charge() が false を返す状態で、slab_out_of_memory() を呼び出すまでが繋がった. 詳細はあまり追えていない
まとめ
- memory.kmem.limit_in_bytes を制限して slab_out_of_memory を発生させた
- memory.kmem.limit_in_bytes だけの制限だと slab の reclaim は走らんのか
- 自分の理解を深めるためのエントリなので、ちょいとコンテキストの説明不足で読んでる人はわかりにくいところもあるかも