記一次KUBERNETES/DOCKER網絡排障
昨天周二晚上,臨下班的時候,用戶給我們報了一個比較怪異的Kubernetes集群下的網絡不能正常訪問的問題,讓我們幫助查看一下,我們從下午5點半左右一直跟進到晚上十點左右,在遠程不能訪問用戶機器只能遠程遙控用戶的情況找到了的問題。這個問題比較有意思,我個人覺得其中的調查用到的的命令以及排障的一些方法可以分享一下,所以寫下了這篇文章。
問題的癥狀
用戶直接在微信裏說,他們發現在Kuberbnetes下的某個pod被重啟了幾百次甚至上千次,於是開啟調查這個pod,發現上面的服務時而能夠訪問,時而不能訪問,也就是有一定概率不能訪問,不知道是什麽原因。而且並不是所有的pod出問題,而只是特定的一兩個pod出了網絡訪問的問題。用戶說這個pod運行著Java程序,為了排除是Java的問題,用戶用 docker exec -it
我們大概知道用戶的集群是這樣的版本,Kuberbnetes 是1.7,網絡用的是flannel的gw模式,Docker版本未知,操作系統CentOS 7.4,直接在物理機上跑docker,物理的配置很高,512GB內存,若幹CPU核,上面運行著幾百個Docker容器。
問題的排查
問題初查
首先,我們排除了flannel的問題,因為整個集群的網絡通信都正常,只有特定的某一兩個pod有問題。而用 telnet ip port
的命令手工測試網絡連接時有很大的概率出現 connection refused
當時,我們讓用戶抓個包看看,然後,用戶抓到了有問題的TCP連接是收到了 SYN
後,立即返回了 RST, ACK
我問一下用戶這兩個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
網卡上,中間再也沒有什麽網絡設備了!
於是這個事把我們逼到了最後一種情況 —— IP地址沖突了!
Linux下看IP地址沖突還不是一件比較簡單事的,而在用戶的生產環境下沒有辦法安裝一些其它的命令,所以只能用已有的命令,這個時候,我們發現用戶的機器上有 arping
於是我們用這個命令來檢測有沒有沖突的IP地址。使用了下面的命令:
1 2 |
$ arping -D -I docker0 -c 2 10.233.14.145
$ echo $?
|
根據文檔,-D
參數是檢測IP地址沖突模式,如果這個命令的退狀態是 0
那麽就有沖突。結果返回了 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。
- 首先,我們到
/var/run/netns
目錄下查看系統的network namespace,發現什麽也沒有。 - 然後,我們到
/var/run/docker/netns
目錄下查看Docker的namespace,發現有好些。 - 於是,我們用指定位置的方式查看Docker的network namespace裏的IP地址
這裏要動用 nsenter
命令,這個命令可以進入到namespace裏執行一些命令。比如
1 |
$ nsenter --net= /var/run/docker/netns/421bdb2accf1 ifconfig -a
|
上述的命令,到 var/run/docker/netns/421bdb2accf1
這個network namespace裏執行了 ifconfig -a
命令。於是我們可以用下面 命令來遍歷所有的network namespace。
1 |
$ ls /var/run/docker/netns | xargs -I {} nsenter --net= /var/run/docker/netns/ {} ip addr
|
然後,我們發現了比較詭異的事情。
10.233.14.145
我們查到了這個IP,說明,docker的namespace下還有這個IP。10.233.14.137
,這個IP沒有在docker的network namespace下查到。
有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
於是我寫下了如下的命令:
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
列出所有開了network namespace的進程,其第4列是進程PID- 把所有開過network namespace的進程PID拿出來,轉給
xargs
命令 - 由
xargs
命令把這些PID 依次傳給nsenter
命令,xargs -t
的意思是會把相關的執行命令打出來,這樣我知道是那個PID。xargs -I {}
是聲明一個占位符來替換相關的PID
最後,我們發現,雖然在 /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,其就是要監管所有進程的子子孫孫,居然也沒有管理好,說明是個非常規的問題。
總結
通過這個調查,可以總結一下,
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
鏡像啟動的)
1 2 3 4 5 6 |
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}]
|
1 2 3 4 5 6 |
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進程組模型就改成上面這個樣子了.
dockerd
是 Docker Engine守護進程,直接面向操作用戶。dockerd
啟動時會啟動containerd
子進程,他們之前通過RPC進行通信。containerd
是dockerd
和runc
之間的一個中間交流組件。他與dockerd
的解耦是為了讓Docker變得更為的中立,而支持OCI 的標準 。containerd-shim
是用來真正運行的容器的,每啟動一個容器都會起一個新的shim進程, 它主要通過指定的三個參數:容器id,boundle目錄(containerd的對應某個容器生成的目錄,一般位於:/var/run/docker/libcontainerd/containerID
), 和運行命令(默認為runc
)來創建一個容器。docker-proxy
你有可能還會在新版本的Docker中見到這個進程,這個進程是用戶級的代理路由。只要你用ps -elf
這樣的命令把其命令行打出來,你就可以看到其就是做端口映射的。如果你不想要這個代理的話,你可以在dockerd
啟動命令行參數上加上:--userland-proxy=false
這個參數。
記一次KUBERNETES/DOCKER網絡排障