systemtap探測vfs dentry

有一臺機器,監控發現經常出現內存不足的情況,如下:

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_root 的 dentry 結構體,然後 dentry 有個叫 mnt_root 的 dentry 結構體,然後 dentry 結構體有個叫 d_name 的 qstr 結構體,然後 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

以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,也希望大家多多支持

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章