1. 程式人生 > >記一次KUBERNETES/DOCKER網路排障

記一次KUBERNETES/DOCKER網路排障

https://coolshell.cn/articles/18654.html

總結在前面:

  1.kill -9殺死docker程序,系統一定是要遍歷所有的docker子程序來一個一個發退出訊號的,這個過程可能會非常的長,結果客戶沒等到遍歷完直接Ctrl+C結束了kill,導致一些docker的程序被殺死,這些docker的子程序就被掛載到PID 1下了,其中docker的NAT程序一直還在,所以出現的現象就是IP地址衝突。

  2.上面的問題,重啟必然能解決~~所以重啟是萬能法寶

  3.一臺物理伺服器512GB記憶體,運行了上百個docker,一旦某些出問題,導致伺服器重啟,則會影響巨大。這時候使用KVM,將不同的docker放在不同的KVM中則顯得尤其重要。~~把雞蛋放在不同KVM籃子裡面,重啟原則還是奏效。哈哈~

 

 

昨天週五晚上,臨下班的時候,使用者給我們報了一個比較怪異的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

我問一下使用者這兩個IP所在的位置,知道了,10.233.14.129 是 docker010.233.14.145 是容器內的IP。所以,這基本上可以排除了所有和kubernets或是flannel的問題,這就是本地的Docker上的網路的問題。

對於這樣被直接 Reset 的情況,在 telnet 上會顯示 connection refused 的錯誤資訊,對於我個人的經驗,這種 SYN完直接返回 RST, ACK的情況只會有三種情況:

  1.  TCP連結不能建立,不能建立連線的原因基本上是標識一條TCP連結的那五元組不能完成,絕大多數情況都是服務端沒有相關的埠號。
  2. TCP連結建錯誤,有可能是因為修改了一些TCP引數,尤其是那些預設是關閉的引數,因為這些引數會導致TCP協議不完整。
  3. 有防火牆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地址。使用了下面的命令:

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 {}&nbsp;-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,其就是要監管所有程序的子子孫孫,居然也沒有管理好,說明是個非常規的問題。(注,關於 systemd,請參看《Linux PID 1 和 Systemd 》,關於父子程序的事,請參看《Unix高階環境程式設計》一書)

接下來就要看看 systemd 為 dockerd 記錄的日誌了…… (然而日誌只有3天的了,這3天dockerd沒有任何異常)

總結

通過這個調查,可以總結一下,

1) 對於問題調查,需要比較紮實的基礎知識,知道問題的成因和範圍。

2)如果走不下去了,要重新梳理一下,回頭仔細看一下一些蛛絲馬跡,認真推敲每一個細節。

3) 各種診斷工具要比較熟悉,這會讓你事半功倍。

4)系統維護和做清潔比較類似,需要經常看看系統中是否有一些殭屍程序或是一些垃圾東西,這些東西要及時清理掉。

最後,多說一下,很多人都說,Docker適合放在物理機內執行,這並不完全對,因為他們只考慮到了效能成本,沒有考慮到運維成本,在這樣512GB中啟動幾百個容器的玩法,其實並不好,因為這本質上是個大單體,因為你一理要重啟某些關鍵程序或是機器,你的影響面是巨大的

 

———————— 更新 2018/12/10 —————————

問題原因

這兩天在自己的環境下測試了一下,發現,只要是通過 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 映象啟動的)

CENTOS 系統安裝包
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}]
DOCKER 官方安裝包
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 是dockerdrunc之間的一箇中間交流元件。他與 dockerd 的解耦是為了讓Docker變得更為的中立,而支援OCI 的標準 。
  • containerd-shim  是用來真正執行的容器的,每啟動一個容器都會起一個新的shim程序, 它主要通過指定的三個引數:容器id,boundle目錄(containerd的對應某個容器生成的目錄,一般位於:/var/run/docker/libcontainerd/containerID), 和執行命令(預設為 runc)來建立一個容器。
  • docker-proxy 你有可能還會在新版本的Docker中見到這個程序,這個程序是使用者級的代理路由。只要你用 ps -elf 這樣的命令把其命令列打出來,你就可以看到其就是做埠對映的。如果你不想要這個代理的話,你可以在 dockerd 啟動命令列引數上加上:  --userland-proxy=false 這個引數。

更多的細節,大家可以自行Google。這裡推薦兩篇文章:

(全文完)