【内核模块auth_rpcgss】netns引用计数泄露导致容器弹性网卡残留
我們不久前定位了一個Linux內核bug,這個bug會影響所有在特權容器中啟用了use-gss-proxy的Linux環境,表現為容器的網絡命名空間(net namespace)無法徹底釋放,導致容器終止后關聯的虛擬網卡未能自動清除,運行時間長的機器上會觀察到內存泄露。目前upstream還沒有對這個bug的修復,我們內部已經做好了patch待測。
這個問題的定位過程很有借鑒價值,特此與大家分享。
在k8s環境里,容器終止之后概率性地發生彈性網卡殘留現象,而且只有privileged容器才有問題,不加privileged就沒問題:
這個問題在客戶的環境里可以穩定復現,但是在容器團隊的測試環境無法復現,給排查問題的過程增加了不少障礙。
1. 為什么虛擬網卡未被自動刪除?
思路是這樣的:因為測試發現殘留的彈性網卡是可以通過"ip link del ..."命令手工刪除的,內核中刪除彈性網卡的函數是veth_dellink(),我們可以利用ftrace跟蹤veth_dellink()調用,對比正常情況和發生殘留的情況,試圖搞清楚發生殘留的時候有什么異常。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進程有主動刪除網卡的動作,而發生殘留的場景下則沒有。主動刪除網卡的call trace是這樣的:
tke-eni-cni進程不主動刪除網卡的原因是net namespace已經被刪除了,"lsns -t net"已經看不到了,在/var/run/docker/netns下也沒了。而且net namespace被刪除不應該導致虛擬網卡殘留,恰恰相反,理論上net namespace銷毀的過程中會自動刪除關聯的彈性網卡,可以通過以下的簡單測試來驗證,在客戶的系統上驗證也是沒問題的:
閱讀源代碼,看到netns的內核數據結構是struct net,其中的count字段表示引用計數,只有當netns的引用計數歸零之后才能執行銷毀動作:
struct net { refcount_t passive; atomic_t count; ...?用crash工具查看內核,可以看到struct net的引用計數確實沒有歸零,難怪沒有觸發銷毀動作:
crash> struct net.count ffffa043bb9d9600 count = { counter = 2 }至此我們得出的判斷是:導致彈性網卡殘留的原因是netns泄露。
2. 是誰導致了netns引用計數泄露?
由于彈性網卡殘留現象只出現在privileged容器,那么加不加privileged有什么區別呢?
加了privileged權限可以在容器里啟動systemd服務。
對比發現,privileged容器里多了很多后臺服務,懷疑是其中某個服務導致了netns引用計數泄露。我們一個一個依次排除,最終找到了直接導致netns泄露的后臺服務是:gssproxy。
可是,容器終止后,在gssproxy后臺進程也消失的情況下,netns引用計數仍然不能歸零,這就很難解釋了,因為用戶態進程退出之后應該會釋放它占用的所有資源,不應該影響內核,說明問題沒那么簡單,很可能內核有bug。
之前容器團隊在測試環境里復現不出問題,是因為信息量不夠,我們定位到這一步,得到的信息已經可以復現問題了,步驟如下。
然后創建鏡像,并運行它,注意以下第一條命令是執行特權容器,第二條是非特權容器
先創建以下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"]然后創建鏡像,并運行它,注意以下第一條命令是執行特權容器,第二條是非特權容器
同時利用crash工具實時觀察net namespace:在
crash> net_namespace_list net_namespace_list = $2 = { next = 0xffffffff907ebe18, prev = 0xffffa043bb9dac18 }在運行容器之前,所有的nets如下:
crash> list 0xffffffff907ebe18 ffffffff907ebe18 ffffa043bb220018 ffffa043bb221618 ffffa043bb222c18 ffffa043bb224218 ffffa043bb225818 ffffa043bb9d8018 ffffa043bb9d9618 ffffffff907ed400在運行容器之后,多了一個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并沒有釋放(如下),這樣就成功地復現了問題。
crash> list 0xffffffff907ebe18 ffffffff907ebe18 ffffa043bb220018 ffffa043bb221618 ffffa043bb222c18 ffffa043bb224218 ffffa043bb225818 ffffa043bb9d8018 ffffa043bb9d9618 ffffa043bb9dac18 <<< 沒有釋放 ffffffff907ed400 crash> struct net.count ffffa043bb9dac00 count = { counter = 2 <<< 引用計數沒有歸零 }內核里修改引用計數的函數是get_net和put_net,一個遞增,一個遞減,通過追蹤這兩個函數就可以找到導致netns泄露的代碼,但是由于它們都是inline函數,所以無法用ftrace或systemtap等工具進行動態追蹤,只能自制debug kernel來調試。我們在get_net和put_net中添加了如下代碼:
+ printk("put_net: %px count: %d PID: %i(%s)\n", net, net->count, current->pid, current->comm);+ dump_stack();捕捉到可疑的調用棧如下,auth_rpcgss內核模塊中,write_gssp()產生了兩次get_net引用,但是容器終止的過程中沒有相應的put_net:
通過strace跟蹤gssproxy進程的啟動過程,可以看到導致調用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到這里我們終于知道為什么一個用戶態進程會導致內核問題了:
因為gssproxy進程向/proc/net/rpc/use-gss-proxy寫入1之后,觸發了內核模塊auth_rpcgss的一些列動作,真正有問題的是這個內核模塊,而不是gssproxy進程本身。
【臨時規避方法】
客戶使用的gssproxy版本是:gssproxy-0.4.1-7.el7.x86_64。用最新版本gssproxy-0.7.0-21.el7.x86_64測試,發現問題消失。對比這兩個版本的配置文件,發現老版本0.4.1-7的配置文件包含如下內容,而新版本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,問題也隨之消失。?
所以臨時規避的方法有兩個:
1、在特權容器中,從gssproxy的配置文件/etc/gssproxy/gssproxy.conf中關掉kernel_nfsd即可,即kernel_nfsd=no。
2、在特權容器中,把gssproxy版本升級到0.7.0-21。
分析源代碼,這個問題的根本原因是內核模塊auth_rpcgss通過gssp_rpc_create()創建了一個rpc client,因為用到了網絡命名空間,所以要遞增引用計數。代碼路徑如下:
?
當rpc client關閉的時候,引用計數會相應遞減,負責的函數是rpcsec_gss_exit_net(),這是一個回調函數,它什么時候被調用呢?它的調用路徑如下:
put_net => cleanup_net => ops_exit_list => rpcsec_gss_exit_net => gss_svc_shutdown_net當put_net引用計數降到0的時候,會觸發cleanup_net(),cleanup_net()隨后會調用包括rpcsec_gss_exit_net()在內的一系列pernet_operations exit方法。問題就出在這:負責遞減引用計數的函數rpcsec_gss_exit_net()必須在引用計數歸零之后才能被調用,而rpcsec_gss_exit_net()不調用就無法遞減引用計數,邏輯上發生了死鎖。
修復這個bug的思路是打破上面這個死鎖,讓rpcsec_gss_exit_net()在恰當的條件下得以執行。我的patch是把它放進nsfs_evict()中,當netns被卸載的時候,nsfs_evict()會被調用,在這個時刻調用rpcsec_gss_exit_net()比較合理。提交給TLinux3 Public的補丁如下:
https://git.code.oa.com/tlinux/tkernel3-public/commit/55f576e2dd113047424fb90883dabc647aa7b143
總結
以上是生活随笔為你收集整理的【内核模块auth_rpcgss】netns引用计数泄露导致容器弹性网卡残留的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巨人的魔法——腾讯打造会思考的数据中心
- 下一篇: 决战9小时,产品上线的危机时刻