來自:酷 殼 - CoolShell,作者: 陳皓
昨天晚上,臨下班的時(shí)候,用戶給我們報(bào)了一個(gè)比較怪異的Kubernetes集群下的網(wǎng)絡(luò)不能正常訪問的問題,讓我們幫助查看一下,我們從下午5點(diǎn)半左右一直跟進(jìn)到晚上十點(diǎn)左右,在遠(yuǎn)程不能訪問用戶機(jī)器只能遠(yuǎn)程遙控用戶的情況找到了的問題。這個(gè)問題比較有意思,我個(gè)人覺得其中的調(diào)查用到的的命令以及排障的一些方法可以分享一下,所以寫下了這篇文章。
問題的癥狀
用戶直接在微信里說,他們發(fā)現(xiàn)在Kuberbnetes下的某個(gè)pod被重啟了幾百次甚至上千次,于是開啟調(diào)查這個(gè)pod,發(fā)現(xiàn)上面的服務(wù)時(shí)而能夠訪問,時(shí)而不能訪問,也就是有一定概率不能訪問,不知道是什么原因。而且并不是所有的pod出問題,而只是特定的一兩個(gè)pod出了網(wǎng)絡(luò)訪問的問題。用戶說這個(gè)pod運(yùn)行著Java程序,為了排除是Java的問題,用戶用 docker exec -it 命令直接到容器內(nèi)啟了一個(gè) Python的 SimpleHttpServer來測(cè)試發(fā)現(xiàn)也是一樣的問題。
我們大概知道用戶的集群是這樣的版本,Kuberbnetes 是1.7,網(wǎng)絡(luò)用的是flannel的gw模式,Docker版本未知,操作系統(tǒng)CentOS 7.4,直接在物理機(jī)上跑docker,物理的配置很高,512GB內(nèi)存,若干CPU核,上面運(yùn)行著幾百個(gè)Docker容器。
問題的排查
問題初查
首先,我們排除了flannel的問題,因?yàn)檎麄€(gè)集群的網(wǎng)絡(luò)通信都正常,只有特定的某一兩個(gè)pod有問題。而用 telnet ip port 的命令手工測(cè)試網(wǎng)絡(luò)連接時(shí)有很大的概率出現(xiàn) connection refused 錯(cuò)誤,大約 1/4的概率,而3/4的情況下是可以正常連接的。
當(dāng)時(shí),我們讓用戶抓個(gè)包看看,然后,用戶抓到了有問題的TCP連接是收到了 SYN 后,立即返回了 RST, ACK
我問一下用戶這兩個(gè)IP所在的位置,知道了,10.233.14.129 是 docker0,10.233.14.145 是容器內(nèi)的IP。所以,這基本上可以排除了所有和kubernets或是flannel的問題,這就是本地的Docker上的網(wǎng)絡(luò)的問題。
對(duì)于這樣被直接 Reset 的情況,在 telnet 上會(huì)顯示 connection refused 的錯(cuò)誤信息,對(duì)于我個(gè)人的經(jīng)驗(yàn),這種 SYN完直接返回 RST, ACK的情況只會(huì)有三種情況:
TCP鏈接不能建立,不能建立連接的原因基本上是標(biāo)識(shí)一條TCP鏈接的那五元組不能完成,絕大多數(shù)情況都是服務(wù)端沒有相關(guān)的端口號(hào)。
TCP鏈接建錯(cuò)誤,有可能是因?yàn)樾薷牧艘恍㏕CP參數(shù),尤其是那些默認(rèn)是關(guān)閉的參數(shù),因?yàn)檫@些參數(shù)會(huì)導(dǎo)致TCP協(xié)議不完整。
有防火墻iptables的設(shè)置,其中有 REJECT 規(guī)則。
因?yàn)楫?dāng)時(shí)還在開車,在等紅燈的時(shí)候,我感覺到有點(diǎn)像 NAT 的網(wǎng)絡(luò)中服務(wù)端開啟了 tcp_tw_recycle 和 tcp_tw_reuse 的癥況(詳細(xì)參看《TCP的那些事(上)》),所以,讓用戶查看了一上TCP參數(shù),發(fā)現(xiàn)用戶一個(gè)TCP的參數(shù)都沒有改,全是默認(rèn)的,于是我們排除了TCP參數(shù)的問題。
然后,我也不覺得容器內(nèi)還會(huì)設(shè)置上iptables,而且如果有那就是100%的問題,不會(huì)時(shí)好時(shí)壞。所以,我懷疑容器內(nèi)的端口號(hào)沒有偵聽上,但是馬上又好了,這可能會(huì)是應(yīng)用的問題。于是我讓用戶那邊看一下,應(yīng)用的日志,并用 kublet describe看一下運(yùn)行的情況,并把宿主機(jī)的 iptables 看一下。
然而,我們發(fā)現(xiàn)并沒有任何的問題。這時(shí),我們失去了所有的調(diào)查線索,感覺不能繼續(xù)下去了……
重新梳理
這個(gè)時(shí)候,回到家,大家吃完飯,和用戶通了一個(gè)電話,把所有的細(xì)節(jié)再重新梳理了一遍,這個(gè)時(shí)候,用戶提供了一個(gè)比較關(guān)鍵的信息—— “抓包這個(gè)事,在 docker0 上可以抓到,然而到了容器內(nèi)抓不到容器返回 RST, ACK ” !然而,根據(jù)我的知識(shí),我知道在 docker0 和容器內(nèi)的 veth 網(wǎng)卡上,中間再也沒有什么網(wǎng)絡(luò)設(shè)備了(參看《Docker基礎(chǔ)技術(shù):LINUX NAMESPACE(下)》)!
于是這個(gè)事把我們逼到了最后一種情況 —— IP地址沖突了!
Linux下看IP地址沖突還不是一件比較簡(jiǎn)單事的,而在用戶的生產(chǎn)環(huán)境下沒有辦法安裝一些其它的命令,所以只能用已有的命令,這個(gè)時(shí)候,我們發(fā)現(xiàn)用戶的機(jī)器上有 arping 于是我們用這個(gè)命令來檢測(cè)有沒有沖突的IP地址。使用了下面的命令:
$ arping -D -I docker0 -c 2 10.233.14.145$ echo $?
根據(jù)文檔,-D 參數(shù)是檢測(cè)IP地址沖突模式,如果這個(gè)命令的退狀態(tài)是 0 那么就有沖突。結(jié)果返回了 1 。而且,我們用 arping IP的時(shí)候,沒有發(fā)現(xiàn)不同的mac地址。 這個(gè)時(shí)候,似乎問題的線索又?jǐn)嗔恕?/p>
因?yàn)榭蛻裟沁呥€在處理一些別的事情,所以,我們?cè)跁r(shí)斷時(shí)續(xù)的情況下工作,而還一些工作都需要用戶完成,所以,進(jìn)展有點(diǎn)緩慢,但是也給我們一些時(shí)間思考問題。
柳暗花明
現(xiàn)在我們知道,IP沖突的可能性是非常大的,但是我們找不出來是和誰的IP沖突了。而且,我們知道只要把這臺(tái)機(jī)器重啟一下,問題一定就解決掉了,但是我們覺得這并不是解決問題的方式,因?yàn)橹貑C(jī)器可以暫時(shí)的解決掉到這個(gè)問題,而如果我們不知道這個(gè)問題怎么發(fā)生的,那么未來這個(gè)問題還會(huì)再來。而重啟線上機(jī)器這個(gè)成本太高了。
于是,我們的好奇心驅(qū)使我們繼續(xù)調(diào)查。我讓用戶 kubectl delete 其中兩個(gè)有問題的pod,因?yàn)楸緛砭头?wù)不斷重啟,所以,刪掉也沒有什么問題。刪掉這兩個(gè)pod后(一個(gè)是IP為 10.233.14.145 另一個(gè)是 10.233.14.137),我們發(fā)現(xiàn),kubernetes在其它機(jī)器上重新啟動(dòng)了這兩個(gè)服務(wù)的新的實(shí)例。然而,在問題機(jī)器上,這兩個(gè)IP地址居然還可以ping得通。
好了,IP地址沖突的問題可以確認(rèn)了。因?yàn)?0.233.14.xxx 這個(gè)網(wǎng)段是 docker 的,所以,這個(gè)IP地址一定是在這臺(tái)機(jī)器上。所以,我們想看看所有的 network namespace 下的 veth 網(wǎng)卡上的IP。
在這個(gè)事上,我們費(fèi)了點(diǎn)時(shí)間,因?yàn)閷?duì)相關(guān)的命令也 很熟悉,所以花了點(diǎn)時(shí)間Google,以及看相關(guān)的man。
首先,我們到 /var/run/netns目錄下查看系統(tǒng)的network namespace,發(fā)現(xiàn)什么也沒有。
然后,我們到 /var/run/docker/netns 目錄下查看Docker的namespace,發(fā)現(xiàn)有好些。
于是,我們用指定位置的方式查看Docker的network namespace里的IP地址
這里要?jiǎng)佑?nsenter 命令,這個(gè)命令可以進(jìn)入到namespace里執(zhí)行一些命令。比如
$ nsenter --net=/var/run/docker/netns/421bdb2accf1 ifconfig -a
上述的命令,到 var/run/docker/netns/421bdb2accf1 這個(gè)network namespace里執(zhí)行了 ifconfig -a 命令。于是我們可以用下面 命令來遍歷所有的network namespace。
$ ls /var/run/docker/netns | xargs -I {} nsenter --net=/var/run/docker/netns/{} ip addr
然后,我們發(fā)現(xiàn)了比較詭異的事情。
10.233.14.145 我們查到了這個(gè)IP,說明,docker的namespace下還有這個(gè)IP。
10.233.14.137,這個(gè)IP沒有在docker的network namespace下查到。
有namespace leaking?于是我上網(wǎng)查了一下,發(fā)現(xiàn)了一個(gè)docker的bug – 在docker remove/stop 一個(gè)容器的時(shí)候,沒有清除相應(yīng)的network namespace,這個(gè)問題被報(bào)告到了 Issue#31597 然后被fix在了 PR#31996,并Merge到了 Docker的 17.05版中。而用戶的版本是 17.09,應(yīng)該包含了這個(gè)fix。不應(yīng)該是這個(gè)問題,感覺又走不下去了。
不過, 10.233.14.137 這個(gè)IP可以ping得通,說明這個(gè)IP一定被綁在某個(gè)網(wǎng)卡,而且被隱藏到了某個(gè)network namespace下。
到這里,要查看所有network namespace,只有最后一條路了,那就是到 /proc/ 目錄下,把所有的pid下的 /proc/<pid>/ns 目錄給窮舉出來。好在這里有一個(gè)比較方便的命令可以干這個(gè)事 :lsns
于是我寫下了如下的命令:
$ lsns -t net | awk ‘{print $4}' | xargs -t -I {} nsenter -t {} -n ip addr | grep -C 4 "10.233.14.137"
解釋一下。
lsns -t net 列出所有開了network namespace的進(jìn)程,其第4列是進(jìn)程PID
把所有開過network namespace的進(jìn)程PID拿出來,轉(zhuǎn)給 xargs 命令
由 xargs 命令把這些PID 依次傳給 nsenter 命令,
xargs -t 的意思是會(huì)把相關(guān)的執(zhí)行命令打出來,這樣我知道是那個(gè)PID。
xargs -I {} 是聲明一個(gè)占位符來替換相關(guān)的PID
最后,我們發(fā)現(xiàn),雖然在 /var/run/docker/netns 下沒有找到 10.233.14.137 ,但是在 lsns 中找到了三個(gè)進(jìn)程,他們都用了10.233.14.137 這個(gè)IP(沖突了這么多),而且他們的MAC地址全是一樣的?。ü植坏胊rping找不到)。通過ps 命令,可以查到這三個(gè)進(jìn)程,有兩個(gè)是java的,還有一個(gè)是/pause (這個(gè)應(yīng)該是kubernetes的沙盒)。
我們繼續(xù)乘勝追擊,窮追猛打,用pstree命令把整個(gè)進(jìn)程樹打出來。發(fā)現(xiàn)上述的三個(gè)進(jìn)程的父進(jìn)程都在多個(gè)同樣叫 docker-contiane 的進(jìn)程下!
這明顯還是docker的,但是在docker ps 中卻找不道相應(yīng)的容器,什么鬼!快崩潰了……
繼續(xù)看進(jìn)程樹,發(fā)現(xiàn),這些 docker-contiane 的進(jìn)程的父進(jìn)程不在 dockerd 下面,而是在 systemd 這個(gè)超級(jí)父進(jìn)程PID 1下,我靠!進(jìn)而發(fā)現(xiàn)了一堆這樣的野進(jìn)程(這種野進(jìn)程或是僵尸進(jìn)程對(duì)系統(tǒng)是有害的,至少也是會(huì)讓系統(tǒng)進(jìn)入亞健康的狀態(tài),因?yàn)樗麄冞€在占著資源)。
docker-contiane 應(yīng)該是 dockerd 的子進(jìn)程,被掛到了 pid 1 只有一個(gè)原因,那就是父進(jìn)程“飛”掉了,只能找 pid 1 當(dāng)養(yǎng)父。這說明,這臺(tái)機(jī)器上出現(xiàn)了比較嚴(yán)重的 dockerd 進(jìn)程退出的問題,而且是非常規(guī)的,因?yàn)?systemd 之所以要成為 pid 1,其就是要監(jiān)管所有進(jìn)程的子子孫孫,居然也沒有管理好,說明是個(gè)非常規(guī)的問題。(注,關(guān)于 systemd,請(qǐng)參看《Linux PID 1 和 Systemd 》,關(guān)于父子進(jìn)程的事,請(qǐng)參看《Unix高級(jí)環(huán)境編程》一書)
接下來就要看看 systemd 為 dockerd 記錄的日志了…… (然而日志只有3天的了,這3天dockerd沒有任何異常)
總結(jié)
通過這個(gè)調(diào)查,可以總結(jié)一下,
1) 對(duì)于問題調(diào)查,需要比較扎實(shí)的基礎(chǔ)知識(shí),知道問題的成因和范圍。
2)如果走不下去了,要重新梳理一下,回頭仔細(xì)看一下一些蛛絲馬跡,認(rèn)真推敲每一個(gè)細(xì)節(jié)。
3) 各種診斷工具要比較熟悉,這會(huì)讓你事半功倍。
4)系統(tǒng)維護(hù)和做清潔比較類似,需要經(jīng)??纯聪到y(tǒng)中是否有一些僵尸進(jìn)程或是一些垃圾東西,這些東西要及時(shí)清理掉。
最后,多說一下,很多人都說,Docker適合放在物理機(jī)內(nèi)運(yùn)行,這并不完全對(duì),因?yàn)樗麄冎豢紤]到了性能成本,沒有考慮到運(yùn)維成本,在這樣512GB中啟動(dòng)幾百個(gè)容器的玩法,其實(shí)并不好,因?yàn)檫@本質(zhì)上是個(gè)大單體,因?yàn)槟阋焕硪貑⒛承╆P(guān)鍵進(jìn)程或是機(jī)器,你的影響面是巨大的。
———————— 更新 2018/12/10 —————————
問題原因
這兩天在自己的環(huán)境下測(cè)試了一下,發(fā)現(xiàn),只要是通過 systemctl start/stop docker 這樣的命令來啟停 Docker, 是可以把所有的進(jìn)程和資源全部干掉的。這個(gè)是沒有什么問題的。我唯一能重現(xiàn)用戶問題的的操作就是直接 kill -9 <dockerd pid> 但是這個(gè)事用戶應(yīng)該不會(huì)干。而 Docker 如果有 crash 事件時(shí),Systemd 是可以通過 journalctl -u docker 這樣的命令查看相關(guān)的系統(tǒng)日志的。
于是,我找用戶了解一下他們?cè)贒ocker在啟停時(shí)的問題,用戶說,他們的執(zhí)行 systemctl stop docker 這個(gè)命令的時(shí)候,發(fā)現(xiàn)這個(gè)命令不響應(yīng)了,有可能就直接按了 Ctrl +C了!
這個(gè)應(yīng)該就是導(dǎo)致大量的 docker-containe 進(jìn)程掛到 PID 1 下的原因了。前面說過,用戶的一臺(tái)物理機(jī)上運(yùn)行著上百個(gè)容器,所以,那個(gè)進(jìn)程樹也是非常龐大的,我想,停服的時(shí)候,系統(tǒng)一定是要遍歷所有的docker子進(jìn)程來一個(gè)一個(gè)發(fā)退出信號(hào)的,這個(gè)過程可能會(huì)非常的長(zhǎng)。導(dǎo)致操作員以為命令假死,而直接按了 Ctrl + C ,最后導(dǎo)致很多容器進(jìn)程并沒有終止……
其它事宜
有同學(xué)問,為什么我在這個(gè)文章里寫的是 docker-containe 而不是 containd 進(jìn)程?這是因?yàn)楸?pstree 給截?cái)嗔?,?ps 命令可以看全,只是進(jìn)程名的名字有一個(gè) docker-的前綴。
下面是這兩種不同安裝包的進(jìn)程樹的差別(其中 sleep 是我用 buybox 鏡像啟動(dòng)的)
systemd───dockerd─┬─docker-contained─┬─3*[docker-contained-shim─┬─sleep] │ │ └─9*[{docker-containe}]] │ ├─docker-contained-shim─┬─sleep │ │ └─10*[{docker-containe}] │ └─14*[{docker-contained-shim}] └─17*[{dockerd}]
systemd───dockerd─┬─containerd─┬─3*[containerd-shim─┬─sleep] │ │ └─9*[{containerd-shim}] │ ├─2*[containerd-shim─┬─sleep] │ │ └─9*[{containerd-shim}]] │ └─11*[{containerd}] └─10*[{dockerd}]
順便說一下,自從 Docker 1.11版以后,Docker進(jìn)程組模型就改成上面這個(gè)樣子了.
dockerd 是 Docker Engine守護(hù)進(jìn)程,直接面向操作用戶。dockerd 啟動(dòng)時(shí)會(huì)啟動(dòng) containerd 子進(jìn)程,他們之前通過RPC進(jìn)行通信。
containerd 是dockerd和runc之間的一個(gè)中間交流組件。他與 dockerd 的解耦是為了讓Docker變得更為的中立,而支持OCI 的標(biāo)準(zhǔn) 。
containerd-shim 是用來真正運(yùn)行的容器的,每啟動(dòng)一個(gè)容器都會(huì)起一個(gè)新的shim進(jìn)程, 它主要通過指定的三個(gè)參數(shù):容器id,boundle目錄(containerd的對(duì)應(yīng)某個(gè)容器生成的目錄,一般位于:/var/run/docker/libcontainerd/containerID), 和運(yùn)行命令(默認(rèn)為 runc)來創(chuàng)建一個(gè)容器。
docker-proxy 你有可能還會(huì)在新版本的Docker中見到這個(gè)進(jìn)程,這個(gè)進(jìn)程是用戶級(jí)的代理路由。只要你用 ps -elf 這樣的命令把其命令行打出來,你就可以看到其就是做端口映射的。如果你不想要這個(gè)代理的話,你可以在 dockerd 啟動(dòng)命令行參數(shù)上加上: --userland-proxy=false 這個(gè)參數(shù)。
更多的細(xì)節(jié),大家可以自行Google。這里推薦兩篇文章:
-
Docker, Containerd & Standalone Runtimes — Here’s What You Should Know
https://hackernoon.com/docker-containerd-standalone-runtimes-heres-what-you-should-know-b834ef155426
-
Docker components explained