socket 与 slab dentry

栏目: PHP · 发布时间: 7年前

内容简介:有一台机器,监控发现经常出现内存不足的情况,如下:可以看到 32G 内存,可用内存大概就剩下 6500M 左右。本来剩个 6G 内存问题倒不大,但是问题是系统上的业务进程基本上没使用多少内存,从 ps 命令输出的结果来看所有进程加起来大概也就用了不到 5G:

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

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 占用内存过多的情况,网上也有相当多的资料告诉我们应该如何调整内核参数,如:

socket 与 slab dentry

再如:

socket 与 slab dentry

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

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

socket 与 slab dentry

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

dentry

那么,dentry 又是什么呢?

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

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

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

socket 与 slab dentry

那么究竟是什么情况导致 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。

找到内核源码看下:

socket 与 slab 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 来申请内存:

socket 与 slab dentry

再来看下 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

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

socket 与 slab dentry

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

其他

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

socket 与 slab 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_rootdentry 结构体,然后 dentry 有个叫 mnt_rootdentry 结构体,然后 dentry 结构体有个叫 d_nameqstr 结构体,然后 qstr 有个变量 name ,那我们可以这么写: $path->mnt->mnt_root->d_name->name$

关于 socket 方面的辅助函数

socket 与 slab dentry

如可以这么用

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Graph Theory

Introduction to Graph Theory

Douglas B. West / Prentice Hall / 2000-9-1 / USD 140.00

For undergraduate or graduate courses in Graph Theory in departments of mathematics or computer science. This text offers a comprehensive and coherent introduction to the fundamental topics of graph ......一起来看看 《Introduction to Graph Theory》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具