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 が実行されない理由は下記の通り

github.com

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 は走らんのか
  • 自分の理解を深めるためのエントリなので、ちょいとコンテキストの説明不足で読んでる人はわかりにくいところもあるかも