本文代码基于linux内核4.19.195.
这周同事反馈一个疑似slab内存泄漏的问题,问题是这样的。
同事在做业务升级测试,不断的升级24小时后发现,通过/proc/meminfo看到的slab内存占用比不跑升级测试的机器多了3.5G(整个系统只有8G内存),怀疑是slab内存泄漏了。
找他要了一台不跑升级的机器和一台跑升级测试的机器对比着看,有如下发现:
- 通过free命令查看,available确实少了2.6G
- 通过/proc/meminfo对比查看,确实是slab相关的几行有着较大的变化,其他条目变化不大
- 使用slabtop -s -c收集一下数据,好家伙,第一名和第二名都是和文件相关的radix_tree_node和filp,分别占用了0.45G和0.27G,接下来3-7位分别是kmalloc-1024、ext4_inode_cache、buffer_head、vm_area_struct、task_struct,这几个都是占用了0.2+G内存;另外,注意到一个细节,这几个object的USE项都是0%,what?
先和同事确认业务升级在做的操作,会有fork动作以及使用share memory的动作。
先不管那些细节,顺着同事的思路,如果radix_tree_node或者filp有泄漏,那么我猜测有如下几种可能:
- fork之后又重复打开了父进程已经打开了的文件,导致fd越开越多(从而filp越来越大)
- 某些文件约写约大,导致radix_tree_node占用内存越来越多
- 某些文件已经被删除了,但是因为引用计数的原因导致删除动作迟迟没有进行?
首先,调用
lsof -n | grep deleted
了一把,发现只有11个文件,每个只有5M大小,55M应该不会导致什么问题吧;
然后,通过
for dir in `ls -d /proc/[0-9]*`;do num=`ls $dir/fd/ | wc -l`;comm=`cat $dir/comm`;echo "$comm -> $num" ;done
看看是不是有哪个进程打开了过多的文件,发现最多的一个进程打开了820个fd,其余都是100以下的。进入业务进程查看,发现只打开了42个fd。
再者,进入业务进程的/proc/pid/fd目录下,执行
for file in `ls -l | awk '{print $NF}'`;do du -h $file;done
发现业务进程打开的最大的文件是84M。
获取了这么多数据,好像没什么异常的,那么是如何引发的slab内存泄漏呢?
回想起以前看过的slab内存泄漏的问题,现象与这个链接中是非常相似的,也就是某个slab对象占用的内存非常大(链接中占用的内存80+G),大到和其他对象有着明显的差距。
重新看了slabtop的输出,再次注意到头几个object的USE项都是0%这个细节,回想了一下slab的原理,突然有了一种想法:
slab内存不足时,会从buddy中申请内存,但是这些内存并不是把所有slab object归还给slab时,就会归还的buddy的,slub会保留一部分内存作为缓存,供下次申请时使用。如果业务升级流程将相关slub的缓存给打满,那么就会出现同事反馈的meminfo中显示slab占用内存增加的现象,而且,slabtop中USE是0%(当然这个数值感觉不怎么准)也能得到很好的解释
刚好借此机会,复习一下slab中和这个缓存相关的代码:
struct kmem_cache {
****
unsigned long min_partial; //代表的是理论上kmem_cache在每个node节点上最多拥有的slab个数,若超过这个数量会尝试释放相关page给buddy
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial; //每个CPU节点上最多拥有的slab个数,若超过这个数量会尝试将相关page释放到node节点上
#endif
****
}
在mm/slub.c的代码中,slab的object释放时会对进行kmem_cache中的阈值进行判断,从而确定slab是转移到node节点上还是还给buddy,下面罗列与归还到buddy相关的一小部分代码
static void deactivate_slab(struct kmem_cache *s, struct page *page,
void *freelist)
{
****
if (!new.inuse && n->nr_partial >= s->min_partial)
m = M_FREE;
***
}else if (m == M_FREE) {
stat(s, DEACTIVATE_EMPTY);
discard_slab(s, page);
stat(s, FREE_SLAB);
}
}
static void __unfreeze_partials(struct kmem_cache *s, struct page *partial_page)
{
****
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
page->next = discard_page;
discard_page = page;
}
******
}
从代码中可以看到,必须是slab的数量超过了阈值,才会把内存归还给buddy,否则相关内存还是处在slab的管辖中。
理论上内存被slab缓存在自己的object中这个猜想是说的通了,那么如何验证呢?想了下有几种方法:
- 继续跑升级测试,看看再过一段时间这个slab占用是否持续增加或者比较平稳了,如果比较平稳了那么猜想正确,否则内核代码可能存在slab内存泄漏问题
- 既然同事觉得slab占用内存太大,那么找几个重点用户,shrink一下,看看slab内存是不是能够降回原来的水准,如果可以,也能怎么猜想正确
- 把slab缓存的阈值调小,也就是不让slab缓存太多内存,这样slab如果增加的值变小,也是能够怎么猜想正确的
说到这,那么怎么去shrink slab,又怎么去调slab缓存数量的阈值呢?这个问题就交给内核文档去解释吧
What: /sys/kernel/slab/cache/shrink
Date: May 2007
KernelVersion: 2.6.22
Contact: Pekka Enberg <penberg@cs.helsinki.fi>,
Christoph Lameter <cl@linux-foundation.org>
Description:
The shrink file is used to reclaim unused slab cache
memory from a cache. Empty per-cpu or partial slabs
are freed and the partial list is sorted so the slabs
with the fewest available objects are used first.
It only accepts a value of "1" on write for shrinking
the cache. Other input values are considered invalid.
Shrinking slab caches might be expensive and can
adversely impact other running applications. So it
should be used with care.
What: /sys/kernel/slab/cache/min_partial
Date: February 2009
KernelVersion: 2.6.30
Contact: Pekka Enberg <penberg@cs.helsinki.fi>,
David Rientjes <rientjes@google.com>
Description:
The min_partial file specifies how many empty slabs shall
remain on a node's partial list to avoid the overhead of
allocating new slabs. Such slabs may be reclaimed by utilizing
the shrink file.
还有一个/sys/kernel/slab/cache/cpu_partial,在文档里找不到,不知道为啥。
顺带提一下,shrink的代码挺有意思,执行shrink的函数是__kmem_cache_shrink()。另外,可以通过观察/proc/PID/status中VmRSS的值来判断是否存在内存泄漏
除此之外,笔者还发现,在4.19版本的内核中(4.14也有该问题),在使能了kmem的memory cgroup中,会为每个memory cgroup分配一个kmem_cache结构体实例,用于该cgroup对应slub的内存分配,具体可以查看函数slab_pre_alloc_hook()->memcg_kmem_get_cache()。也就是说,每个cgroup会拥有属于自己的一个slub管理结构体,各个cgroup之间的内存分配并不是在一个kmem_cache结构体实例中处理的,这样可能带来的后果是,cgroup1分配了1个8byte的内存,cgroup2也分配了1个8byte的内存,在系统总体来看,系统中实际的内存支出是2个page(假设这个kmem_cache每次从buddy系统中获取的内存是一个page),与同在一个cgroup里面申请两个8byte内存,系统中实际的内存支出1个page是不同的。这样带来的后果是,若系统中存在的memory cgroup数量比较多的话,会带来较大的内存消耗,笔者曾经遇到过,因为用户态没有删除memory cgroup,导致的系统slab占用是正常占用的3-4倍的问题。这个问题在5.4内核上得到了解决,也就是所有cgroup中的slub内存分配都使用全局的kmem_cache。