socket 与 slab dentry

有一台机器,监控发现经常出现内存不足的情况,如下:

可以看到 32G 内存,可用内存大概就剩下 6500M 左右。本来剩个 6G 内存问题倒不大,但是问题是系统上的业务进程基本上没使用多少内存,从 ps 命令输出的结果来看所有进程加起来大概也就用了不到 5G:

# ps aux | awk '{sum+=$6}END{printf("%.2f\n",sum/1024.0/1024)}'
4.62

那么剩下的 22G 内存去哪了呢?


slab


经验告诉我,这些“看不到”的内存大概率是被 slab 使用了。slab allocator 是 Linux 内核的内存分配机制,是给内核对象分配内存的,所以在 ps 或者 top 上是看不到的,可以查看 /proc/meminfo 文件:

... 省略上面的输出 ...
Slab:           23043264 kB
SReclaimable:   22953172 kB
SUnreclaim:        90092 kB
... 省略下面的输出 ...

可以看到确实是 slab 占用了大概 22G 内存,绝大部分是可回收(SReclaimable),即意味着可以通过以下命令来释放内存:

# echo 2 > /proc/sys/vm/drop_caches


slabinfo


现在虽然知道内存是被 slab 所使用了,但是因为 slab 里面有各种不同的内核对象(object),还需要找到是哪些对象占用了内存,可以查看 /proc/slabinfo 文件,发现占用最多的是 dentry 对象:

slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
... 省略上面的输出 ...
dentry            113671590 113671700    192   20    1 : tunables  120   60    8 : slabdata 5683585 5683585     80
... 省略下面的输出 ...

可以看到,每个 dentry 对象的大小是 192 bytes,系统当前有 113671700 个 dentry 对象,因此,单单是这些 dentry 对象就占用了 (113671700*192)/1024/1024/1024 = 20.33G 的内存,与我们上面”丢失的“内存数量是基本吻合的。

另外还有一个 slabtop 命令,用类似于 top 的输出,更加直观地列出各内核对象所占用的内存。


一些可调整的内核参数


对于这类 dentry cache 占用内存过多的情况,网上也有相当多的资料告诉我们应该如何调整内核参数,如:

再如:

不过网上的资料,水平参差不齐,某些文章连修改的风险都没有提及(特别是 min_free_kbytes 参数)。

还有一种方法是定时任务 drop cache,不过过几天就会反弹:

所以最好还是深入点研究下是什么原因导致 dentry cache 持续不断地上涨。


dentry


那么,dentry 又是什么呢?

dentry (directory entry),目录项缓存。具体作用可以看

这篇文件

VFS中的file,dentry和inodebean-li.github.io图标

写得非常好,但在我们这个案例里,我们只需要知道 dentry 是内核用来高速查找文件的,也就是每个文件都会在内核里有个 dentry 结构体。

这么说很可能是系统内文件过多是吧。很遗憾,`df -i` 的结果显示并不如此:

那么究竟是什么情况导致 dentry cache 过高的呢?


fs/dcache.c


使用 systemtap 来分析问题。

因为 slab 属于内核的内存分配机制,所以应该有内核函数会提及到 dentry,先使用

# stap -L 'kernel.function("*dentry*")'

来查找内核函数探测点 (probe)。

输出的结果中有很多内核函数都来自 `fs/dcache.c`,再看下这个文件的内核函数:

# stap -L 'kernel.function("*@fs/dcache.c")'

从名字上看,这两个内核函数相当可疑:

kernel.function("d_alloc@fs/dcache.c:968") $parent:struct dentry* $name:struct qstr const*
kernel.function("d_free@fs/dcache.c:89") $dentry:struct dentry*

看起来像是 d_alloc 分配 dentry,而 d_free 是释放 dentry。

找到内核源码看下:


看起来是这样。写个 stap 脚本验证下:

probe kernel.function("d_alloc")
{
    printf("%s[%ld] %s %s\n", execname(), pid(), pp(), probefunc())
}
probe kernel.function("d_free")
{
    printf("%s[%ld] %s %s\n", execname(), pid(), pp(), probefunc())
}
probe timer.s(5)
{
    exit()
}

分别抓取这两个内核函数的请求记录,跑 5 秒钟,然后比对下这 5 秒钟内系统中 dentry 的变化:

bef=$(awk '{print $1}' /proc/sys/fs/dentry-state)
stap dentry.stp > d.txt
aft=$(awk '{print $1}' /proc/sys/fs/dentry-state)
d_alloc=$(/bin/grep 'd_alloc' d.txt | wc -l)
d_free=$(/bin/grep 'd_free' d.txt | wc -l)
diff_a=$(( $aft - $bef ))
diff_b=$(( $d_alloc - $d_free ))
echo "${diff_a} ${diff_b}"

输出结果:

2893 2921

跑了 6 次,有 4 次基本上是一致的,这说明我们的方向是对的。

然后再统计下这 5 秒内,哪些进程调用 d_alloc 较多:

# awk '/d_alloc/{a[$1]++}END{for(i in a)print i, a[i]}' d.txt | sort -k2rn | head
php[30225] 2268
php[30274] 1614
1_scheduler[7841] 993
php[21772] 810
php[9063] 417
php[7778] 382
php[1167] 331
php[12378] 299
2_scheduler[7841] 264
irqbalance[1142] 89

基本上就是 PHP 应用。


d_alloc


接下来我们需要分析下 PHP 调用 d_alloc 来做些什么操作。

先从 d_alloc 的参数入手:

struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)

加个 $$parms 查看函数的参数:

probe kernel.function("d_alloc")
{
    printf("%s[%ld] %s %s %s\n", execname(), pid(), pp(), probefunc(), $$parms)
}

基本上 PHP 的 parent 参数都是 0x0,如:

php[2738] kernel.function("d_alloc@fs/dcache.c:968") d_alloc parent=0x0 name=0xffff88055b533ec8

而其他的一些进程,比如 zaabix_agentd 的 parenet 是有具体的数值的:

zabbix_agentd[20239] kernel.function("d_alloc@fs/dcache.c:968") d_alloc parent=0xffff88082a001b00 name=0xffff880286acbcd8

再来查看下参数结构体里面的内容:

probe kernel.function("d_alloc")
{
    printf("%s[%ld] %s %s %s %s\n", execname(), pid(), pp(), probefunc(), $parent$, $name$)
}

结果 PHP 进程的 parent 查看不了:

php[3078] kernel.function("d_alloc@fs/dcache.c:968") d_alloc ERROR {.hash=0, .len=0, .name=""}

那就看它返回的变量吧

probe kernel.function("d_alloc").return
{
    printf("%s[%ld] %s %s %s\n", execname(), pid(), pp(), probefunc(), $dentry$)
}

然后看到 probefunc() 变成了 d_alloc_pseudo:

php[18178] kernel.function("d_alloc@fs/dcache.c:968").return d_alloc_pseudo {.d_count={...}, .d_flags=?, .d_lock={...}, .d_mounted=?, .d_inode=?, .d_hash={...}, .d_parent=?, .d_name={...}, .d_lru={...}, .d_u={...}, .d_subdirs={...}, .d_alias={...}, .d_time=?, .d_op=?, .d_sb=?, .d_fsdata=?, .d_iname=[...]}

看下调用的情况

probe kernel.function("d_alloc").call
{
    if(execname() == "php")
        printf("%s -> %s\n", thread_indent(1), probefunc())
}
probe kernel.function("d_alloc").return
{
    if(execname() == "php")
        printf("%s <- %s\n", thread_indent(-1), probefunc())
}
probe timer.s(5)
{
    exit()
}

确实是 d_alloc 调用了 d_alloc_pseudo

     0 php(9063): -> d_alloc
     4 php(9063): <- d_alloc_pseudo

d_alloc_pseudo


又得往下走了,再看看这个 d_alloc_pseudo:

d_alloc_pseudo - allocate a dentry (for lookup-less filesystems)

内核的注释说明这个函数是给 lookup-less filesystems 分配一个 dentry。

那什么是 lookup-less filesystem?

The Linux Kernel Archives 有解释:

For a filesystem that just pins its dentries in memory and never performs lookups at all, return an unhashed IS_ROOT dentry.

就是只需要用到 dentry 而不需要在文件系统中查找的,换句话说,也就是在文件系统上找不到的。

听起来是不是有点耳熟?


sock_alloc_file


继续往下挖:

probe kernel.function("d_alloc_pseudo").call
{
    if(execname() == "php")
        printf("%s -> %s\n", thread_indent(1), probefunc())
}
probe kernel.function("d_alloc_pseudo").return
{
    if(execname() == "php")
        printf("%s <- %s\n", thread_indent(-1), probefunc())
}

是 sock_alloc_file

     0 php(12378): -> d_alloc_pseudo
     5 php(12378): <- sock_alloc_file

再往下看,发现是 sock_map_fd 函数

     0 php(7778): -> sock_alloc_file
     6 php(7778): <- sock_map_fd

显而易见,sock_map_fd 是将 socket 映射到文件描述符,然后 socket 才能通过 fd 进行访问。

比如:

# ll /proc/31433/fd/4
lrwx------ 1 root root 64 Aug 27 15:07 /proc/31433/fd/4 -> socket:[1901557712]

再往下就是 sys_socket 了,这已经是系统调用了。

最终的调用栈:

    d_alloc
->  d_alloc_pseudo
->  sock_alloc_file
->  sock_map_fd
->  sys_socket

而 d_alloc 通过 kmem_cache_alloc 来申请内存:

再来看下 PHP 在 socket 相关函数的调用栈:

probe kernel.function("sock_*").call
{
    if(execname() == "php")
        printf("%s -> %s\n", thread_indent(1), probefunc())
}
probe kernel.function("sock_*").return
{
    if(execname() == "php")
        printf("%s <- %s\n", thread_indent(-1), probefunc())
}
probe timer.s(5)
{
    exit()
}

可以看到短短 5 秒钟,就调用了 sock_map_fd 将近 3000 次:

# /bin/grep -E ' -> sock_map_fd' php_sock.txt | wc -l
2897

可以计算出 5 分钟内,光给 PHP 脚本分配的 dentry 就已经是

(3000/5)*192*300/1024=33750 kbytes

跟监控比起来也比较吻合:

最终的结论就是 PHP 脚本不停地在申请 socket 导致 dentry cache 不停上涨。(虽然可以回收,但是没到内核设置的水位线内核是不会自动释放的)


其他


其实最好的验证方法是将这些 PHP 脚本停下来,看 dentry 还会不会不停上涨,结果也验证了我的判断,停止 PHP 脚本时 dentry 停止了上涨,而重新启动脚本,则 dentry 再次上涨:

如何处理就不是我关注的范围了,留给开发同学去优化了。

这里再总结下另外的一些使用 systemtap 排查问题的技巧。


用 `$name$` 直接打印结构体的内容

如:

static int do_lookup(struct nameidata *nd, struct qstr *name,
			struct path *path, struct inode **inode)

打印

printf("%s[%ld] %s %s %s %s\n", execname(), pid(), pp(), probefunc(), $name$)

大括号 { } 里面的即是结构体:

zabbix_agentd[21424] kernel.function("do_lookup@fs/namei.c:1022").return __link_path_walk {.hash=306156246, .len=5, .name="lib64/ld-linux-x86-64.so.2"}

如果再想取里面的变量,可以用 $name->name$

printf("%s[%ld] %s %s %s %s\n", execname(), pid(), pp(), probefunc(), $name->name$)

输出

zabbix_agentd[20244] kernel.function("do_lookup@fs/namei.c:1022").return __link_path_walk "proc/31080/status"
zabbix_agentd[20244] kernel.function("do_lookup@fs/namei.c:1022").return __link_path_walk "31080/status"
zabbix_agentd[20244] kernel.function("do_lookup@fs/namei.c:1022").return __link_path_walk "status"


如果结构体嵌套着结构体,还可以用 -> 继续往下找,比如上面的 path 参数,它有个 vfsmount 结构体 mnt,然后 vfsmount 又有个叫 mnt_root 的 dentry 结构体,然后 dentry 有个叫 mnt_root 的 dentry 结构体,然后 dentry 结构体有个叫 d_name 的 qstr 结构体,然后 qstr 有个变量 name,那我们可以这么写:

$path->mnt->mnt_root->d_name->name$


关于 socket 方面的辅助函数

如可以这么用

probe kernel.function("sock_map_fd").return
{
    printf("%s[%ld] %s %s %s %s %s %s\n", execname(), pid(), pp(), probefunc(), $$parms, $sock->ops$, sock_type_num2str($sock->type), $$return)
}
probe timer.s(3)
{
    exit()
}

它会直接打印出 STREAM 或者 DGRAM 等,而不是数字。


查看某个结构体的大小


可以用这个脚本

jav/systemtapgithub.com
# stap sizeof.stp dentry "kernel:<include/linux/dcache.h>"
type dentry in kernel:<include/linux/dcache.h> byte-size: 192

发布于 2018-08-28