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),目录项缓存。具体作用可以看
这篇文件,写得非常好,但在我们这个案例里,我们只需要知道 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?
kernel.org 有解释:
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())
}
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 等,而不是数字。
查看某个结构体的大小
可以用这个脚本
# stap sizeof.stp dentry "kernel:<include/linux/dcache.h>"
type dentry in kernel:<include/linux/dcache.h> byte-size: 192