昨天周五晚上,臨下班的時(shí)候宣渗,用戶(hù)給我們報(bào)了一個(gè)比較怪異的Kubernetes集群下的網(wǎng)絡(luò)不能正常訪(fǎng)問(wèn)的問(wèn)題它掂,讓我們幫助查看一下棺弊,我們從下午5點(diǎn)半左右一直跟進(jìn)到晚上十點(diǎn)左右,在遠(yuǎn)程不能訪(fǎng)問(wèn)用戶(hù)機(jī)器只能遠(yuǎn)程遙控用戶(hù)的情況找到了的問(wèn)題鱼喉。這個(gè)問(wèn)題比較有意思秀鞭,我個(gè)人覺(jué)得其中的調(diào)查用到的的命令以及排障的一些方法可以分享一下,所以寫(xiě)下了這篇文章扛禽。
問(wèn)題的癥狀
用戶(hù)直接在微信里說(shuō)锋边,他們發(fā)現(xiàn)在Kuberbnetes下的某個(gè)pod被重啟了幾百次甚至上千次,于是開(kāi)啟調(diào)查這個(gè)pod编曼,發(fā)現(xiàn)上面的服務(wù)時(shí)而能夠訪(fǎng)問(wèn)豆巨,時(shí)而不能訪(fǎng)問(wèn),也就是有一定概率不能訪(fǎng)問(wèn)掐场,不知道是什么原因往扔。而且并不是所有的pod出問(wèn)題,而只是特定的一兩個(gè)pod出了網(wǎng)絡(luò)訪(fǎng)問(wèn)的問(wèn)題熊户。用戶(hù)說(shuō)這個(gè)pod運(yùn)行著Java程序萍膛,為了排除是Java的問(wèn)題,用戶(hù)用?docker exec -it命令直接到容器內(nèi)啟了一個(gè) Python的 SimpleHttpServer來(lái)測(cè)試發(fā)現(xiàn)也是一樣的問(wèn)題嚷堡。
我們大概知道用戶(hù)的集群是這樣的版本蝗罗,Kuberbnetes 是1.7,網(wǎng)絡(luò)用的是flannel的gw模式,Docker版本未知串塑,操作系統(tǒng)CentOS 7.4沼琉,直接在物理機(jī)上跑docker,物理的配置很高桩匪,512GB內(nèi)存打瘪,若干CPU核,上面運(yùn)行著幾百個(gè)Docker容器傻昙。
問(wèn)題的排查
問(wèn)題初查
首先瑟慈,我們排除了flannel的問(wèn)題,因?yàn)檎麄€(gè)集群的網(wǎng)絡(luò)通信都正常屋匕,只有特定的某一兩個(gè)pod有問(wèn)題葛碧。而用?telnet ip port的命令手工測(cè)試網(wǎng)絡(luò)連接時(shí)有很大的概率出現(xiàn)?connection refused錯(cuò)誤,大約 1/4的概率过吻,而3/4的情況下是可以正常連接的进泼。
當(dāng)時(shí),我們讓用戶(hù)抓個(gè)包看看纤虽,然后乳绕,用戶(hù)抓到了有問(wèn)題的TCP連接是收到了?SYN后,立即返回了?RST, ACK
我問(wèn)一下用戶(hù)這兩個(gè)IP所在的位置逼纸,知道了洋措,10.233.14.129是?docker0,10.233.14.145是容器內(nèi)的IP杰刽。所以菠发,這基本上可以排除了所有和kubernets或是flannel的問(wèn)題,這就是本地的Docker上的網(wǎng)絡(luò)的問(wèn)題贺嫂。
對(duì)于這樣被直接 Reset 的情況滓鸠,在?telnet?上會(huì)顯示?connection refused的錯(cuò)誤信息,對(duì)于我個(gè)人的經(jīng)驗(yàn)第喳,這種?SYN完直接返回?RST, ACK的情況只會(huì)有三種情況:
?TCP鏈接不能建立糜俗,不能建立連接的原因基本上是標(biāo)識(shí)一條TCP鏈接的那五元組不能完成,絕大多數(shù)情況都是服務(wù)端沒(méi)有相關(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í)還在開(kāi)車(chē)梁丘,在等紅燈的時(shí)候侵浸,我感覺(jué)到有點(diǎn)像 NAT 的網(wǎng)絡(luò)中服務(wù)端開(kāi)啟了?tcp_tw_recycle和?tcp_tw_reuse的癥況(詳細(xì)參看《TCP的那些事(上)》)旺韭,所以氛谜,讓用戶(hù)查看了一上TCP參數(shù),發(fā)現(xiàn)用戶(hù)一個(gè)TCP的參數(shù)都沒(méi)有改区端,全是默認(rèn)的值漫,于是我們排除了TCP參數(shù)的問(wèn)題。
然后织盼,我也不覺(jué)得容器內(nèi)還會(huì)設(shè)置上iptables杨何,而且如果有那就是100%的問(wèn)題,不會(huì)時(shí)好時(shí)壞沥邻。所以危虱,我懷疑容器內(nèi)的端口號(hào)沒(méi)有偵聽(tīng)上,但是馬上又好了唐全,這可能會(huì)是應(yīng)用的問(wèn)題埃跷。于是我讓用戶(hù)那邊看一下,應(yīng)用的日志邮利,并用?kublet describe看一下運(yùn)行的情況弥雹,并把宿主機(jī)的 iptables 看一下。
然而延届,我們發(fā)現(xiàn)并沒(méi)有任何的問(wèn)題剪勿。這時(shí),我們失去了所有的調(diào)查線(xiàn)索方庭,感覺(jué)不能繼續(xù)下去了……
重新梳理
這個(gè)時(shí)候厕吉,回到家,大家吃完飯械念,和用戶(hù)通了一個(gè)電話(huà)赴涵,把所有的細(xì)節(jié)再重新梳理了一遍,這個(gè)時(shí)候订讼,用戶(hù)提供了一個(gè)比較關(guān)鍵的信息—— “抓包這個(gè)事髓窜,在?docker0上可以抓到,然而到了容器內(nèi)抓不到容器返回?RST, ACK?” 欺殿!然而寄纵,根據(jù)我的知識(shí),我知道在?docker0?和容器內(nèi)的?veth?網(wǎng)卡上脖苏,中間再也沒(méi)有什么網(wǎng)絡(luò)設(shè)備了(參看《Docker基礎(chǔ)技術(shù):LINUX NAMESPACE(下)》)!
于是這個(gè)事把我們逼到了最后一種情況 —— IP地址沖突了程拭!
Linux下看IP地址沖突還不是一件比較簡(jiǎn)單事的,而在用戶(hù)的生產(chǎn)環(huán)境下沒(méi)有辦法安裝一些其它的命令棍潘,所以只能用已有的命令恃鞋,這個(gè)時(shí)候崖媚,我們發(fā)現(xiàn)用戶(hù)的機(jī)器上有?arping于是我們用這個(gè)命令來(lái)檢測(cè)有沒(méi)有沖突的IP地址。使用了下面的命令:
$ arping -D -I docker0 -c 2 10.233.14.145
$ echo$?
根據(jù)文檔恤浪,-D參數(shù)是檢測(cè)IP地址沖突模式畅哑,如果這個(gè)命令的退狀態(tài)是?0那么就有沖突。結(jié)果返回了?1水由。而且荠呐,我們用?arpingIP的時(shí)候,沒(méi)有發(fā)現(xiàn)不同的mac地址砂客。這個(gè)時(shí)候泥张,似乎問(wèn)題的線(xiàn)索又?jǐn)嗔?/b>。
因?yàn)榭蛻?hù)那邊還在處理一些別的事情鞠值,所以媚创,我們?cè)跁r(shí)斷時(shí)續(xù)的情況下工作,而還一些工作都需要用戶(hù)完成彤恶,所以钞钙,進(jìn)展有點(diǎn)緩慢,但是也給我們一些時(shí)間思考問(wèn)題粤剧。
柳暗花明
現(xiàn)在我們知道歇竟,IP沖突的可能性是非常大的,但是我們找不出來(lái)是和誰(shuí)的IP沖突了抵恋。而且焕议,我們知道只要把這臺(tái)機(jī)器重啟一下,問(wèn)題一定就解決掉了弧关,但是我們覺(jué)得這并不是解決問(wèn)題的方式盅安,因?yàn)橹貑C(jī)器可以暫時(shí)的解決掉到這個(gè)問(wèn)題,而如果我們不知道這個(gè)問(wèn)題怎么發(fā)生的世囊,那么未來(lái)這個(gè)問(wèn)題還會(huì)再來(lái)别瞭。而重啟線(xiàn)上機(jī)器這個(gè)成本太高了。
于是株憾,我們的好奇心驅(qū)使我們繼續(xù)調(diào)查蝙寨。我讓用戶(hù)?kubectl delete其中兩個(gè)有問(wèn)題的pod,因?yàn)楸緛?lái)就服務(wù)不斷重啟嗤瞎,所以墙歪,刪掉也沒(méi)有什么問(wèn)題。刪掉這兩個(gè)pod后(一個(gè)是IP為?10.233.14.145另一個(gè)是?10.233.14.137)贝奇,我們發(fā)現(xiàn)虹菲,kubernetes在其它機(jī)器上重新啟動(dòng)了這兩個(gè)服務(wù)的新的實(shí)例。然而掉瞳,在問(wèn)題機(jī)器上毕源,這兩個(gè)IP地址居然還可以ping得通浪漠。
好了,IP地址沖突的問(wèn)題可以確認(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)什么也沒(méi)有。
然后擦耀,我們到?/var/run/docker/netns?目錄下查看Docker的namespace棉圈,發(fā)現(xiàn)有好些。
于是眷蜓,我們用指定位置的方式查看Docker的network namespace里的IP地址
這里要?jiǎng)佑?nsenter命令分瘾,這個(gè)命令可以進(jìn)入到namespace里執(zhí)行一些命令。比如
1$ nsenter --net=/var/run/docker/netns/421bdb2accf1ifconfig-a
上述的命令吁系,到?var/run/docker/netns/421bdb2accf1這個(gè)network namespace里執(zhí)行了?ifconfig -a命令德召。于是我們可以用下面 命令來(lái)遍歷所有的network namespace。
1$ ls/var/run/docker/netns| xargs-I {} nsenter --net=/var/run/docker/netns/{} ip addr
然后汽纤,我們發(fā)現(xiàn)了比較詭異的事情上岗。
10.233.14.145?我們查到了這個(gè)IP,說(shuō)明蕴坪,docker的namespace下還有這個(gè)IP肴掷。
10.233.14.137,這個(gè)IP沒(méi)有在docker的network namespace下查到背传。
有namespace leaking呆瞻?于是我上網(wǎng)查了一下,發(fā)現(xiàn)了一個(gè)docker的bug – 在docker remove/stop 一個(gè)容器的時(shí)候续室,沒(méi)有清除相應(yīng)的network namespace栋烤,這個(gè)問(wèn)題被報(bào)告到了?Issue#31597然后被fix在了?PR#31996,并Merge到了 Docker的 17.05版中挺狰。而用戶(hù)的版本是 17.09明郭,應(yīng)該包含了這個(gè)fix买窟。不應(yīng)該是這個(gè)問(wèn)題,感覺(jué)又走不下去了薯定。
不過(guò)始绍,?10.233.14.137這個(gè)IP可以ping得通,說(shuō)明這個(gè)IP一定被綁在某個(gè)網(wǎng)卡话侄,而且被隱藏到了某個(gè)network namespace下亏推。
到這里,要查看所有network namespace年堆,只有最后一條路了吞杭,那就是到?/proc/目錄下,把所有的pid下的?/proc/<pid>/ns目錄給窮舉出來(lái)变丧。好在這里有一個(gè)比較方便的命令可以干這個(gè)事 :lsns
于是我寫(xiě)下了如下的命令:
1$ lsns -t net | awk‘{print $4}' | xargs-t -I {} nsenter -t {} -n ip addr | grep-C 4 "10.233.14.137"
解釋一下芽狗。
lsns -t net列出所有開(kāi)了network namespace的進(jìn)程,其第4列是進(jìn)程PID
把所有開(kāi)過(guò)network namespace的進(jìn)程PID拿出來(lái)痒蓬,轉(zhuǎn)給?xargs命令
由?xargs?命令把這些PID 依次傳給?nsenter?命令童擎,
xargs -t的意思是會(huì)把相關(guān)的執(zhí)行命令打出來(lái),這樣我知道是那個(gè)PID攻晒。
xargs -I {}?是聲明一個(gè)占位符來(lái)替換相關(guān)的PID
最后顾复,我們發(fā)現(xiàn),雖然在?/var/run/docker/netns下沒(méi)有找到?10.233.14.137鲁捏,但是在?lsns中找到了三個(gè)進(jìn)程芯砸,他們都用了10.233.14.137這個(gè)IP(沖突了這么多),而且他們的MAC地址全是一樣的碴萧!(怪不得arping找不到)乙嘀。通過(guò)ps命令,可以查到這三個(gè)進(jìn)程破喻,有兩個(gè)是java的虎谢,還有一個(gè)是/pause(這個(gè)應(yīng)該是kubernetes的沙盒)。
我們繼續(xù)乘勝追擊曹质,窮追猛打婴噩,用pstree命令把整個(gè)進(jìn)程樹(shù)打出來(lái)。發(fā)現(xiàn)上述的三個(gè)進(jìn)程的父進(jìn)程都在多個(gè)同樣叫?docker-contiane的進(jìn)程下羽德!
這明顯還是docker的几莽,但是在docker ps?中卻找不道相應(yīng)的容器,什么鬼宅静!快崩潰了……
繼續(xù)看進(jìn)程樹(shù)章蚣,發(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)父。這說(shuō)明菇肃,這臺(tái)機(jī)器上出現(xiàn)了比較嚴(yán)重的?dockerd進(jìn)程退出的問(wèn)題地粪,而且是非常規(guī)的,因?yàn)?systemd之所以要成為 pid 1巷送,其就是要監(jiān)管所有進(jìn)程的子子孫孫驶忌,居然也沒(méi)有管理好矛辕,說(shuō)明是個(gè)非常規(guī)的問(wèn)題笑跛。(注,關(guān)于 systemd聊品,請(qǐng)參看《Linux PID 1 和 Systemd?》飞蹂,關(guān)于父子進(jìn)程的事,請(qǐng)參看《Unix高級(jí)環(huán)境編程》一書(shū))
接下來(lái)就要看看?systemd?為?dockerd?記錄的日志了…… (然而日志只有3天的了翻屈,這3天dockerd沒(méi)有任何異常)
總結(jié)
通過(guò)這個(gè)調(diào)查陈哑,可以總結(jié)一下,
1) 對(duì)于問(wèn)題調(diào)查伸眶,需要比較扎實(shí)的基礎(chǔ)知識(shí)惊窖,知道問(wèn)題的成因和范圍。
2)如果走不下去了厘贼,要重新梳理一下界酒,回頭仔細(xì)看一下一些蛛絲馬跡,認(rèn)真推敲每一個(gè)細(xì)節(jié)嘴秸。
3) 各種診斷工具要比較熟悉毁欣,這會(huì)讓你事半功倍。
4)系統(tǒng)維護(hù)和做清潔比較類(lèi)似岳掐,需要經(jīng)称敬看看系統(tǒng)中是否有一些僵尸進(jìn)程或是一些垃圾東西,這些東西要及時(shí)清理掉串述。
最后执解,多說(shuō)一下,很多人都說(shuō)纲酗,Docker適合放在物理機(jī)內(nèi)運(yùn)行衰腌,這并不完全對(duì)逝淹,因?yàn)樗麄冎豢紤]到了性能成本,沒(méi)有考慮到運(yùn)維成本桶唐,在這樣512GB中啟動(dòng)幾百個(gè)容器的玩法栅葡,其實(shí)并不好,因?yàn)檫@本質(zhì)上是個(gè)大單體尤泽,因?yàn)槟阋焕硪貑⒛承╆P(guān)鍵進(jìn)程或是機(jī)器欣簇,你的影響面是巨大的。
問(wèn)題原因
這兩天在自己的環(huán)境下測(cè)試了一下坯约,發(fā)現(xiàn)熊咽,只要是通過(guò)?systemctl start/stop docker這樣的命令來(lái)啟停 Docker, 是可以把所有的進(jìn)程和資源全部干掉的闹丐。這個(gè)是沒(méi)有什么問(wèn)題的横殴。我唯一能重現(xiàn)用戶(hù)問(wèn)題的的操作就是直接?kill -9 <dockerd pid>但是這個(gè)事用戶(hù)應(yīng)該不會(huì)干。而 Docker 如果有 crash 事件時(shí)卿拴,Systemd 是可以通過(guò)?journalctl -u docker這樣的命令查看相關(guān)的系統(tǒng)日志的衫仑。
于是,我找用戶(hù)了解一下他們?cè)贒ocker在啟停時(shí)的問(wèn)題堕花,用戶(hù)說(shuō)文狱,他們的執(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下的原因了。前面說(shuō)過(guò)壕曼,用戶(hù)的一臺(tái)物理機(jī)上運(yùn)行著上百個(gè)容器苏研,所以,那個(gè)進(jìn)程樹(shù)也是非常龐大的腮郊,我想摹蘑,停服的時(shí)候,系統(tǒng)一定是要遍歷所有的docker子進(jìn)程來(lái)一個(gè)一個(gè)發(fā)退出信號(hào)的伴榔,這個(gè)過(guò)程可能會(huì)非常的長(zhǎng)纹蝴。導(dǎo)致操作員以為命令假死,而直接按了?Ctrl + C踪少,最后導(dǎo)致很多容器進(jìn)程并沒(méi)有終止……
其它事宜
有同學(xué)問(wèn)塘安,為什么我在這個(gè)文章里寫(xiě)的是?docker-containe而不是?containd進(jìn)程?這是因?yàn)楸?pstree給截?cái)嗔嗽荩?ps命令可以看全兼犯,只是進(jìn)程名的名字有一個(gè)?docker-的前綴。
下面是這兩種不同安裝包的進(jìn)程樹(shù)的差別(其中?sleep?是我用?buybox?鏡像啟動(dòng)的)
CENTOS 系統(tǒ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}]
DOCKER 官方安裝包
systemd───dockerd─┬─containerd─┬─3*[containerd-shim─┬─sleep]
??????????????????│??????????? │???????????????? └─9*[{containerd-shim}]
??????????????????│??????????? ├─2*[containerd-shim─┬─sleep]
??????????????????│??????????? │??????????????????? └─9*[{containerd-shim}]]
??????????????????│??????????? └─11*[{containerd}]
??????????????????└─10*[{dockerd}]
順便說(shuō)一下,自從 Docker 1.11版以后切黔,Docker進(jìn)程組模型就改成上面這個(gè)樣子了.
dockerd是 Docker Engine守護(hù)進(jìn)程砸脊,直接面向操作用戶(hù)。dockerd?啟動(dòng)時(shí)會(huì)啟動(dòng)?containerd?子進(jìn)程纬霞,他們之前通過(guò)RPC進(jìn)行通信凌埂。
containerd?是dockerd和runc之間的一個(gè)中間交流組件。他與?dockerd的解耦是為了讓Docker變得更為的中立诗芜,而支持OCI 的標(biāo)準(zhǔn) 瞳抓。
containerd-shim是用來(lái)真正運(yùn)行的容器的,每啟動(dòng)一個(gè)容器都會(huì)起一個(gè)新的shim進(jìn)程伏恐, 它主要通過(guò)指定的三個(gè)參數(shù):容器id孩哑,boundle目錄(containerd的對(duì)應(yīng)某個(gè)容器生成的目錄,一般位于:/var/run/docker/libcontainerd/containerID)翠桦, 和運(yùn)行命令(默認(rèn)為?runc)來(lái)創(chuàng)建一個(gè)容器横蜒。
docker-proxy你有可能還會(huì)在新版本的Docker中見(jiàn)到這個(gè)進(jìn)程,這個(gè)進(jìn)程是用戶(hù)級(jí)的代理路由销凑。只要你用?ps -elf?這樣的命令把其命令行打出來(lái)丛晌,你就可以看到其就是做端口映射的。如果你不想要這個(gè)代理的話(huà)闻鉴,你可以在?dockerd啟動(dòng)命令行參數(shù)上加上:?--userland-proxy=false?這個(gè)參數(shù)茵乱。
更多的細(xì)節(jié),大家可以自行Google孟岛。這里推薦兩篇文章:
Docker, Containerd & Standalone Runtimes — Here’s What You Should Know
轉(zhuǎn)載 -Browsed By 作者:陳皓