内容简介:有一台机器,监控发现经常出现内存不足的情况,如下:可以看到 32G 内存,可用内存大概就剩下 6500M 左右。本来剩个 6G 内存问题倒不大,但是问题是系统上的业务进程基本上没使用多少内存,从 ps 命令输出的结果来看所有进程加起来大概也就用了不到 5G:
有一台机器,监控发现经常出现内存不足的情况,如下:
可以看到 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}"
输出结果:
跑了 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
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂
刘玉红 / 2015-1-1 / 68
《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》作者根据在长期教学中积累的网页设计教学经验,完整、详尽地介绍HTML 5 + CSS 3 + JavaScript网页设计技术。 《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》共分24章,分别介绍HTML 5概述、HTML 5网页文档结构、HTML 5网页中的文本和图像、HTML......一起来看看 《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》 这本书的介绍吧!