昨天周五晚上,臨下班的時候,使用者給我們報了一個比較怪異的Kubernetes叢集下的網絡不能正常通路的問題,讓我們幫助檢視一下,我們從下午5點半左右一直跟進到晚上十點左右,在遠端不能通路使用者機器隻能遠端遙控使用者的情況找到了的問題。這個問題比較有意思,我個人覺得其中的調查用到的的指令以及排障的一些方法可以分享一下,是以寫下了這篇文章。
問題的症狀
使用者直接在微信裡說,他們發現在Kuberbnetes下的某個pod被重新開機了幾百次甚至上千次,于是開啟調查這個pod,發現上面的服務時而能夠通路,時而不能通路,也就是有一定機率不能通路,不知道是什麼原因。而且并不是所有的pod出問題,而隻是特定的一兩個pod出了網絡通路的問題。使用者說這個pod運作着Java程式,為了排除是Java的問題,使用者用
docker exec -it
指令直接到容器内啟了一個 Python的 SimpleHttpServer來測試發現也是一樣的問題。
我們大概知道使用者的叢集是這樣的版本,Kuberbnetes 是1.7,網絡用的是flannel的gw模式,Docker版本未知,作業系統CentOS 7.4,直接在實體機上跑docker,實體的配置很高,512GB記憶體,若幹CPU核,上面運作着幾百個Docker容器。
問題的排查
問題初查
首先,我們排除了flannel的問題,因為整個叢集的網絡通信都正常,隻有特定的某一兩個pod有問題。而用
telnet ip port
的指令手工測試網絡連接配接時有很大的機率出現
connection refused
錯誤,大約 1/4的機率,而3/4的情況下是可以正常連接配接的。
當時,我們讓使用者抓個包看看,然後,使用者抓到了有問題的TCP連接配接是收到了
SYN
後,立即傳回了
RST, ACK
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SN2UWYhNTYhNmYzMWO5cDM0UWMyAzN4Y2Y3AjN3cDNy8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
我問一下使用者這兩個IP所在的位置,知道了,
10.233.14.129
是
docker0
,
10.233.14.145
是容器内的IP。是以,這基本上可以排除了所有和kubernets或是flannel的問題,這就是本地的Docker上的網絡的問題。
對于這樣被直接 Reset 的情況,在
telnet
上會顯示
connection refused
的錯誤資訊,對于我個人的經驗,這種
SYN
完直接傳回
RST, ACK
的情況隻會有三種情況:
- TCP連結不能建立,不能建立連接配接的原因基本上是辨別一條TCP連結的那五元組不能完成,絕大多數情況都是服務端沒有相關的端口号。
- TCP連結建錯誤,有可能是因為修改了一些TCP參數,尤其是那些預設是關閉的參數,因為這些參數會導緻TCP協定不完整。
- 有防火牆iptables的設定,其中有
規則。REJECT
因為當時還在開車,在等紅燈的時候,我感覺到有點像 NAT 的網絡中服務端開啟了
tcp_tw_recycle
和
tcp_tw_reuse
的症況(詳細參看《TCP的那些事(上)》),是以,讓使用者檢視了一上TCP參數,發現使用者一個TCP的參數都沒有改,全是預設的,于是我們排除了TCP參數的問題。
然後,我也不覺得容器内還會設定上iptables,而且如果有那就是100%的問題,不會時好時壞。是以,我懷疑容器内的端口号沒有偵聽上,但是馬上又好了,這可能會是應用的問題。于是我讓使用者那邊看一下,應用的日志,并用
kublet describe
看一下運作的情況,并把主控端的 iptables 看一下。
然而,我們發現并沒有任何的問題。這時,
我們失去了所有的調查線索,感覺不能繼續下去了……重新梳理
這個時候,回到家,大家吃完飯,和使用者通了一個電話,把所有的細節再重新梳理了一遍,這個時候,使用者提供了一個比較關鍵的資訊—— “
抓包這個事,在docker0
上可以抓到,然而到了容器内抓不到容器傳回 RST, ACK
” !然而,根據我的知識,我知道在
docker0
和容器内的
veth
網卡上,中間再也沒有什麼網絡裝置了(參看《Docker基礎技術:LINUX NAMESPACE(下)》)!
于是這個事把我們逼到了最後一種情況 —— IP位址沖突了!
Linux下看IP位址沖突還不是一件比較簡單事的,而在使用者的生産環境下沒有辦法安裝一些其它的指令,是以隻能用已有的指令,這個時候,我們發現使用者的機器上有
arping
于是我們用這個指令來檢測有沒有沖突的IP位址。使用了下面的指令:
$ arping -D -I docker0 -c 2 10.233.14.145
$ echo $?
根據文檔,
-D
參數是檢測IP位址沖突模式,如果這個指令的退狀态是
那麼就有沖突。結果傳回了
1
。而且,我們用
arping
IP的時候,沒有發現不同的mac位址。
這個時候,似乎問題的線索又斷了。
因為客戶那邊還在處理一些别的事情,是以,我們在時斷時續的情況下工作,而還一些工作都需要使用者完成,是以,進展有點緩慢,但是也給我們一些時間思考問題。
柳暗花明
現在我們知道,IP沖突的可能性是非常大的,但是我們找不出來是和誰的IP沖突了。而且,我們知道隻要把這台機器重新開機一下,問題一定就解決掉了,但是我們覺得這并不是解決問題的方式,因為重新開機機器可以暫時的解決掉到這個問題,而如果我們不知道這個問題怎麼發生的,那麼未來這個問題還會再來。而重新開機線上機器這個成本太高了。
于是,我們的好奇心驅使我們繼續調查。我讓使用者
kubectl delete
其中兩個有問題的pod,因為本來就服務不斷重新開機,是以,删掉也沒有什麼問題。删掉這兩個pod後(一個是IP為
10.233.14.145
另一個是
10.233.14.137
),我們發現,kubernetes在其它機器上重新啟動了這兩個服務的新的執行個體。然而,
在問題機器上,這兩個IP位址居然還可以ping得通。
好了,IP位址沖突的問題可以确認了。因為
10.233.14.xxx
這個網段是 docker 的,是以,這個IP位址一定是在這台機器上。是以,我們想看看所有的 network namespace 下的 veth 網卡上的IP。
在這個事上,我們費了點時間,因為對相關的指令也 很熟悉,是以花了點時間Google,以及看相關的man。
- 首先,我們到
目錄下檢視系統的network namespace,發現什麼也沒有。/var/run/netns
- 然後,我們到
目錄下檢視Docker的namespace,發現有好些。/var/run/docker/netns
- 于是,我們用指定位置的方式檢視Docker的network namespace裡的IP位址
這裡要動用
nsenter
指令,這個指令可以進入到namespace裡執行一些指令。比如
$ nsenter --net=/var/run/docker/netns/421bdb2accf1 ifconfig -a
上述的指令,到
var/run/docker/netns/421bdb2accf1
這個network namespace裡執行了
ifconfig -a
指令。于是我們可以用下面 指令來周遊所有的network namespace。
$ ls /var/run/docker/netns | xargs -I {} nsenter --net=/var/run/docker/netns/{} ip addr
然後,我們發現了比較詭異的事情。
-
我們查到了這個IP,說明,docker的namespace下還有這個IP。10.233.14.145
-
,這個IP沒有在docker的network namespace下查到。10.233.14.137
有namespace leaking?于是我上網查了一下,發現了一個docker的bug – 在docker remove/stop 一個容器的時候,沒有清除相應的network namespace,這個問題被報告到了 Issue#31597 然後被fix在了 PR#31996,并Merge到了 Docker的 17.05版中。而使用者的版本是 17.09,應該包含了這個fix。不應該是這個問題,感覺又走不下去了。
不過,
10.233.14.137
這個IP可以ping得通,說明這個IP一定被綁在某個網卡,而且被隐藏到了某個network namespace下。
到這裡,要檢視所有network namespace,隻有最後一條路了,那就是到
/proc/
目錄下,把所有的pid下的
/proc/<pid>/ns
目錄給窮舉出來。好在這裡有一個比較友善的指令可以幹這個事 :
lsns
于是我寫下了如下的指令:
$ lsns -t net | awk ‘{print $4}' | xargs -t -I {} nsenter -t {} -n ip addr | grep -C 4 "10.233.14.137"
解釋一下。
-
列出所有開了network namespace的程序,其第4列是程序PIDlsns -t net
- 把所有開過network namespace的程序PID拿出來,轉給
指令xargs
- 由
指令把這些PID 依次傳給xargs
指令,nsenter
-
的意思是會把相關的執行指令打出來,這樣我知道是那個PID。xargs -t
-
是聲明一個占位符來替換相關的PIDxargs -I {}
-
最後,我們發現,雖然在
/var/run/docker/netns
下沒有找到
10.233.14.137
,但是在
lsns
中找到了三個程序,他們都用了
10.233.14.137
這個IP(沖突了這麼多),
而且他們的MAC位址全是一樣的!(怪不得arping找不到)。通過
ps
指令,可以查到這三個程序,有兩個是java的,還有一個是
/pause
(這個應該是kubernetes的沙盒)。
我們繼續乘勝追擊,窮追猛打,用
pstree
指令把整個程序樹打出來。發現上述的三個程序的父程序都在多個同樣叫
docker-contiane
的程序下!
這明顯還是docker的,但是在docker ps
中卻找不道相應的容器,什麼鬼!快崩潰了…… 繼續看程序樹,發現,這些
docker-contiane
的程序的父程序不在
dockerd
下面,而是在
systemd
這個超級父程序PID 1下,我靠!進而發現了一堆這樣的野程序(這種野程序或是僵屍程序對系統是有害的,至少也是會讓系統進入亞健康的狀态,因為他們還在占着資源)。
docker-contiane
應該是
dockerd
的子程序,被挂到了
pid 1
隻有一個原因,那就是父程序“飛”掉了,隻能找 pid 1 當養父。這說明,這台機器上出現了比較嚴重的
dockerd
程序退出的問題,而且是非正常的,因為
systemd
之是以要成為 pid 1,其就是要監管所有程序的子子孫孫,居然也沒有管理好,說明是個非正常的問題。(注,關于 systemd,請參看《Linux PID 1 和 Systemd 》,關于父子程序的事,請參看《Unix進階環境程式設計》一書)
接下來就要看看
systemd
為
dockerd
記錄的日志了…… (然而日志隻有3天的了,這3天
dockerd
沒有任何異常)
總結
通過這個調查,可以總結一下,
1) 對于問題調查,需要比較紮實的基礎知識,知道問題的成因和範圍。
2)如果走不下去了,要重新梳理一下,回頭仔細看一下一些蛛絲馬迹,認真推敲每一個細節。
3) 各種診斷工具要比較熟悉,這會讓你事半功倍。
4)系統維護和做清潔比較類似,需要經常看看系統中是否有一些僵屍程序或是一些垃圾東西,這些東西要及時清理掉。
最後,多說一下,很多人都說,
Docker适合放在實體機内運作,這并不完全對,因為他們隻考慮到了性能成本,沒有考慮到運維成本,在這樣512GB中啟動幾百個容器的玩法,其實并不好,因為這本質上是個大單體,因為你一理要重新開機某些關鍵程序或是機器,你的影響面是巨大的。
問題原因
這兩天在自己的環境下測試了一下,發現,隻要是通過
systemctl start/stop docker
這樣的指令來啟停 Docker, 是可以把所有的程序和資源全部幹掉的。這個是沒有什麼問題的。我唯一能重制使用者問題的的操作就是直接
kill -9 <dockerd pid>
但是這個事使用者應該不會幹。而 Docker 如果有 crash 事件時,Systemd 是可以通過
journalctl -u docker
這樣的指令檢視相關的系統日志的。
于是,我找使用者了解一下他們在Docker在啟停時的問題,使用者說,
他們的執行systemctl stop docker
這個指令的時候,發現這個指令不響應了,有可能就直接按了 Ctrl +C
了 !
這個應該就是導緻大量的
docker-containe
程序挂到
PID 1
下的原因了。前面說過,使用者的一台實體機上運作着上百個容器,是以,那個程序樹也是非常龐大的,我想,停服的時候,系統一定是要周遊所有的docker子程序來一個一個發退出信号的,這個過程可能會非常的長。導緻操作員以為指令假死,而直接按了
Ctrl + C
,最後導緻很多容器程序并沒有終止……
其它事宜
有同學問,為什麼我在這個文章裡寫的是
docker-containe
而不是
containd
程序?這是因為被
pstree
給截斷了,用
ps
指令可以看全,隻是程序名的名字有一個
docker-
的字首。
下面是這兩種不同安裝包的程序樹的差别(其中
sleep
是我用
buybox
鏡像啟動的)
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程序組模型就改成上面這個樣子了.
-
是 Docker Engine守護程序,直接面向操作使用者。dockerd
啟動時會啟動dockerd
子程序,他們之前通過RPC進行通信。containerd
-
是containerd
和dockerd
之間的一個中間交流元件。他與runc
的解耦是為了讓Docker變得更為的中立,而支援OCI 的标準 。dockerd
-
是用來真正運作的容器的,每啟動一個容器都會起一個新的shim程序, 它主要通過指定的三個參數:容器id,boundle目錄(containerd的對應某個容器生成的目錄,一般位于:containerd-shim
), 和運作指令(預設為/var/run/docker/libcontainerd/containerID
)來建立一個容器。runc
-
你有可能還會在新版本的Docker中見到這個程序,這個程序是使用者級的代理路由。隻要你用docker-proxy
這樣的指令把其指令行打出來,你就可以看到其就是做端口映射的。如果你不想要這個代理的話,你可以在ps -elf
啟動指令行參數上加上:dockerd
這個參數。--userland-proxy=false
來源:酷殼
作者:陳皓
原文:https://coolshell.cn/articles/18654.html