有一臺機器,監控發現經常出現內存不足的情況,如下:
可以看到 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
以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,也希望大家多多支持