【内核模块auth_rpcgss】netns引用计数泄露导致容器弹性网卡残留
我們不久前定位了一個Linux內(nèi)核bug,這個bug會影響所有在特權(quán)容器中啟用了use-gss-proxy的Linux環(huán)境,表現(xiàn)為容器的網(wǎng)絡(luò)命名空間(net namespace)無法徹底釋放,導(dǎo)致容器終止后關(guān)聯(lián)的虛擬網(wǎng)卡未能自動清除,運(yùn)行時間長的機(jī)器上會觀察到內(nèi)存泄露。目前upstream還沒有對這個bug的修復(fù),我們內(nèi)部已經(jīng)做好了patch待測。
這個問題的定位過程很有借鑒價值,特此與大家分享。
在k8s環(huán)境里,容器終止之后概率性地發(fā)生彈性網(wǎng)卡殘留現(xiàn)象,而且只有privileged容器才有問題,不加privileged就沒問題:
這個問題在客戶的環(huán)境里可以穩(wěn)定復(fù)現(xiàn),但是在容器團(tuán)隊的測試環(huán)境無法復(fù)現(xiàn),給排查問題的過程增加了不少障礙。
1. 為什么虛擬網(wǎng)卡未被自動刪除?
思路是這樣的:因為測試發(fā)現(xiàn)殘留的彈性網(wǎng)卡是可以通過"ip link del ..."命令手工刪除的,內(nèi)核中刪除彈性網(wǎng)卡的函數(shù)是veth_dellink(),我們可以利用ftrace跟蹤veth_dellink()調(diào)用,對比正常情況和發(fā)生殘留的情況,試圖搞清楚發(fā)生殘留的時候有什么異常。ftrace腳本如下:
#!/bin/bash SYS_TRACE=/sys/kernel/debug/tracing [ -e $SYS_TRACE/events/kprobes/enable ] && echo 0 > $SYS_TRACE/events/kprobes/enable echo > $SYS_TRACE/kprobe_events echo > $SYS_TRACE/trace echo nostacktrace > $SYS_TRACE/trace_options if [ $# -eq 1 -a $1=="stop" ]; then echo "stopped" exit fi echo "p veth_dellink net_device=%di" >> $SYS_TRACE/kprobe_events echo stacktrace > $SYS_TRACE/trace_options for evt in `ls $SYS_TRACE/events/kprobes/*/enable`; do echo 1 > $evt done cat $SYS_TRACE/trace_pipe?以上ftrace腳本觀察到正常場景下,tke-eni-cni進(jìn)程有主動刪除網(wǎng)卡的動作,而發(fā)生殘留的場景下則沒有。主動刪除網(wǎng)卡的call trace是這樣的:
tke-eni-cni進(jìn)程不主動刪除網(wǎng)卡的原因是net namespace已經(jīng)被刪除了,"lsns -t net"已經(jīng)看不到了,在/var/run/docker/netns下也沒了。而且net namespace被刪除不應(yīng)該導(dǎo)致虛擬網(wǎng)卡殘留,恰恰相反,理論上net namespace銷毀的過程中會自動刪除關(guān)聯(lián)的彈性網(wǎng)卡,可以通過以下的簡單測試來驗證,在客戶的系統(tǒng)上驗證也是沒問題的:
閱讀源代碼,看到netns的內(nèi)核數(shù)據(jù)結(jié)構(gòu)是struct net,其中的count字段表示引用計數(shù),只有當(dāng)netns的引用計數(shù)歸零之后才能執(zhí)行銷毀動作:
struct net { refcount_t passive; atomic_t count; ...?用crash工具查看內(nèi)核,可以看到struct net的引用計數(shù)確實沒有歸零,難怪沒有觸發(fā)銷毀動作:
crash> struct net.count ffffa043bb9d9600 count = { counter = 2 }至此我們得出的判斷是:導(dǎo)致彈性網(wǎng)卡殘留的原因是netns泄露。
2. 是誰導(dǎo)致了netns引用計數(shù)泄露?
由于彈性網(wǎng)卡殘留現(xiàn)象只出現(xiàn)在privileged容器,那么加不加privileged有什么區(qū)別呢?
加了privileged權(quán)限可以在容器里啟動systemd服務(wù)。
對比發(fā)現(xiàn),privileged容器里多了很多后臺服務(wù),懷疑是其中某個服務(wù)導(dǎo)致了netns引用計數(shù)泄露。我們一個一個依次排除,最終找到了直接導(dǎo)致netns泄露的后臺服務(wù)是:gssproxy。
可是,容器終止后,在gssproxy后臺進(jìn)程也消失的情況下,netns引用計數(shù)仍然不能歸零,這就很難解釋了,因為用戶態(tài)進(jìn)程退出之后應(yīng)該會釋放它占用的所有資源,不應(yīng)該影響內(nèi)核,說明問題沒那么簡單,很可能內(nèi)核有bug。
之前容器團(tuán)隊在測試環(huán)境里復(fù)現(xiàn)不出問題,是因為信息量不夠,我們定位到這一步,得到的信息已經(jīng)可以復(fù)現(xiàn)問題了,步驟如下。
然后創(chuàng)建鏡像,并運(yùn)行它,注意以下第一條命令是執(zhí)行特權(quán)容器,第二條是非特權(quán)容器
先創(chuàng)建以下Dockerfile用于制作鏡像:
FROM centos:7 ENV container docker RUN echo 'root:root' | chpasswd ADD gssproxy-0.4.1-7.el7.x86_64.rpm /gssproxy-0.4.1-7.el7.x86_64.rpm ADD nfs-utils-1.3.0-0.21.el7.x86_64.rpm /nfs-utils-1.3.0-0.21.el7.x86_64.rpm RUN yum localinstall -y gssproxy-0.4.1-7.el7.x86_64.rpm RUN yum localinstall -y nfs-utils-1.3.0-0.21.el7.x86_64.rpm RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \ systemd-tmpfiles-setup.service ] || rm -f $i; done); \ rm -f /etc/systemd/system/*.wants/*;\ rm -f /lib/systemd/system/local-fs.target.wants/*; \ rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ rm -f /lib/systemd/system/basic.target.wants/*;\ rm -f /lib/systemd/system/anaconda.target.wants/*; VOLUME [ "/sys/fs/cgroup" ] RUN systemctl enable gssproxy CMD ["/usr/sbin/init"]然后創(chuàng)建鏡像,并運(yùn)行它,注意以下第一條命令是執(zhí)行特權(quán)容器,第二條是非特權(quán)容器
同時利用crash工具實時觀察net namespace:在
crash> net_namespace_list net_namespace_list = $2 = { next = 0xffffffff907ebe18, prev = 0xffffa043bb9dac18 }在運(yùn)行容器之前,所有的nets如下:
crash> list 0xffffffff907ebe18 ffffffff907ebe18 ffffa043bb220018 ffffa043bb221618 ffffa043bb222c18 ffffa043bb224218 ffffa043bb225818 ffffa043bb9d8018 ffffa043bb9d9618 ffffffff907ed400在運(yùn)行容器之后,多了一個netns:
crash> list 0xffffffff907ebe18 ffffffff907ebe18 ffffa043bb220018 ffffa043bb221618 ffffa043bb222c18 ffffa043bb224218 ffffa043bb225818 ffffa043bb9d8018 ffffa043bb9d9618 ffffa043bb9dac18 <<<新增的netns ffffffff907ed400然后我們殺掉這個容器:
[root@tlinux-test ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 12e632ca0ac7 local/c7-systemd "/usr/sbin/init" 2 hours ago Up 2 hours zealous_darwin [root@tlinux-test ~]# docker kill 12e632ca0ac7 12e632ca0ac7crash再看,netns并沒有釋放(如下),這樣就成功地復(fù)現(xiàn)了問題。
crash> list 0xffffffff907ebe18 ffffffff907ebe18 ffffa043bb220018 ffffa043bb221618 ffffa043bb222c18 ffffa043bb224218 ffffa043bb225818 ffffa043bb9d8018 ffffa043bb9d9618 ffffa043bb9dac18 <<< 沒有釋放 ffffffff907ed400 crash> struct net.count ffffa043bb9dac00 count = { counter = 2 <<< 引用計數(shù)沒有歸零 }內(nèi)核里修改引用計數(shù)的函數(shù)是get_net和put_net,一個遞增,一個遞減,通過追蹤這兩個函數(shù)就可以找到導(dǎo)致netns泄露的代碼,但是由于它們都是inline函數(shù),所以無法用ftrace或systemtap等工具進(jìn)行動態(tài)追蹤,只能自制debug kernel來調(diào)試。我們在get_net和put_net中添加了如下代碼:
+ printk("put_net: %px count: %d PID: %i(%s)\n", net, net->count, current->pid, current->comm);+ dump_stack();捕捉到可疑的調(diào)用棧如下,auth_rpcgss內(nèi)核模塊中,write_gssp()產(chǎn)生了兩次get_net引用,但是容器終止的過程中沒有相應(yīng)的put_net:
通過strace跟蹤gssproxy進(jìn)程的啟動過程,可以看到導(dǎo)致調(diào)用write_gssp()的操作是往/proc/net/rpc/use-gss-proxy里寫入1:
20818 open("/proc/net/rpc/use-gss-proxy", O_RDWR) = 9 20818 write(9, "1", 1) = 1到這里我們終于知道為什么一個用戶態(tài)進(jìn)程會導(dǎo)致內(nèi)核問題了:
因為gssproxy進(jìn)程向/proc/net/rpc/use-gss-proxy寫入1之后,觸發(fā)了內(nèi)核模塊auth_rpcgss的一些列動作,真正有問題的是這個內(nèi)核模塊,而不是gssproxy進(jìn)程本身。
【臨時規(guī)避方法】
客戶使用的gssproxy版本是:gssproxy-0.4.1-7.el7.x86_64。用最新版本gssproxy-0.7.0-21.el7.x86_64測試,發(fā)現(xiàn)問題消失。對比這兩個版本的配置文件,發(fā)現(xiàn)老版本0.4.1-7的配置文件包含如下內(nèi)容,而新版本0.7.0-21則沒有:
# cat /etc/gssproxy/gssproxy.conf ... [service/nfs-server] mechs = krb5 socket = /run/gssproxy.sock cred_store = keytab:/etc/krb5.keytab trusted = yes kernel_nfsd = yes euid = 0 ...按照手冊,kernel_nfsd的含義如下,它會影響對/proc/net/rpc/use-gss-proxy的操作:
kernel_nfsd (boolean) Boolean flag that allows the Linux kernel to check if gssproxy is running (via /proc/net/rpc/use-gss-proxy). Default: kernel_nfsd = false在老版本gssproxy-0.4.1-7上測試把kernel_nfsd從yes改為no,問題也隨之消失。?
所以臨時規(guī)避的方法有兩個:
1、在特權(quán)容器中,從gssproxy的配置文件/etc/gssproxy/gssproxy.conf中關(guān)掉kernel_nfsd即可,即kernel_nfsd=no。
2、在特權(quán)容器中,把gssproxy版本升級到0.7.0-21。
分析源代碼,這個問題的根本原因是內(nèi)核模塊auth_rpcgss通過gssp_rpc_create()創(chuàng)建了一個rpc client,因為用到了網(wǎng)絡(luò)命名空間,所以要遞增引用計數(shù)。代碼路徑如下:
?
當(dāng)rpc client關(guān)閉的時候,引用計數(shù)會相應(yīng)遞減,負(fù)責(zé)的函數(shù)是rpcsec_gss_exit_net(),這是一個回調(diào)函數(shù),它什么時候被調(diào)用呢?它的調(diào)用路徑如下:
put_net => cleanup_net => ops_exit_list => rpcsec_gss_exit_net => gss_svc_shutdown_net當(dāng)put_net引用計數(shù)降到0的時候,會觸發(fā)cleanup_net(),cleanup_net()隨后會調(diào)用包括rpcsec_gss_exit_net()在內(nèi)的一系列pernet_operations exit方法。問題就出在這:負(fù)責(zé)遞減引用計數(shù)的函數(shù)rpcsec_gss_exit_net()必須在引用計數(shù)歸零之后才能被調(diào)用,而rpcsec_gss_exit_net()不調(diào)用就無法遞減引用計數(shù),邏輯上發(fā)生了死鎖。
修復(fù)這個bug的思路是打破上面這個死鎖,讓rpcsec_gss_exit_net()在恰當(dāng)?shù)臈l件下得以執(zhí)行。我的patch是把它放進(jìn)nsfs_evict()中,當(dāng)netns被卸載的時候,nsfs_evict()會被調(diào)用,在這個時刻調(diào)用rpcsec_gss_exit_net()比較合理。提交給TLinux3 Public的補(bǔ)丁如下:
https://git.code.oa.com/tlinux/tkernel3-public/commit/55f576e2dd113047424fb90883dabc647aa7b143
總結(jié)
以上是生活随笔為你收集整理的【内核模块auth_rpcgss】netns引用计数泄露导致容器弹性网卡残留的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巨人的魔法——腾讯打造会思考的数据中心
- 下一篇: 决战9小时,产品上线的危机时刻