天天看點

記一次 K8S HostPort 引發的服務故障排錯指南

最近排查了一個 kubernetes 中使用了 hostport 後遇到比較坑的問題,奇怪的知識又增加了。

問題背景

叢集環境為 k8s v1.15.9,cni 指定了 flannel-vxlan 跟 portmap, kube-proxy 使用 mode 為 ipvs,叢集 3 台 master,同時也是 node,這裡以 node-1,node-2,node-3 來表示。

叢集中有 2 個 mysql, 部署在兩個 ns 下,mysql 本身不是問題重點,這裡就不細說,這裡以 mysql-a,mysql-b 來表示。

mysql-a 落在 node-1 上,mysql-b 落在 node-2 上, 兩個資料庫 svc 名跟使用者、密碼完全不相同

出現詭異的現象這裡以一張圖來說明會比較清楚一些:

記一次 K8S HostPort 引發的服務故障排錯指南

其中綠線的表示通路沒有問題,紅線表示連接配接 mysql-a 提示使用者名密碼錯誤

特别詭異的是,當在 node-2 上通過 svc 通路 mysql-a 時,輸入 mysql-a 的使用者名跟密碼提示密碼錯誤,密碼确認無疑,但當輸入 mysql-b 的使用者名跟密碼,居然能夠連接配接上,看了下資料,連上的是 mysql-b 的資料庫,給人的感覺就是請求轉到了 mysql-a, 最後又轉到了 mysql-b,當時讓人大跌眼鏡

碰到詭異的問題那就排查吧,排查的過程倒是不費什麼事,最主要的是要通過這次踩坑機會挖掘一些奇怪的知識出來。

排查過程

既然在 node-1 上連接配接 mysql-a/mysql-b 都沒有問題,那基本可以排查是 mysql-a 的問題

經實驗,在 node-2 上所有的服務想要連 mysql-a 時,都有這個問題,但是通路其它的服務又都沒有問題,說明要麼是 mysql-a 的 3306 這個端口有問題,通過上一步應該排查了 mysql-a 的問題,那問題隻能出在 node-2 上

在 k8s 中像這樣的請求轉發出現詭異現象,當排除了一些常見的原因之外,最大的嫌疑就是 iptables 了,作者遇到過多次

這次也不例外,雖然目前叢集使用的 ipvs, 但還是照例看下 iptables 規則,檢視 node-2 上的 iptables 與 node-1 的 iptables 比對,結果有蹊跷, 在 node-2 上發現有以下的規則在其它節點上沒有

其中 10.224.0.222 為 mysql-b 的 pod ip, xxxxxxxxxxxxx 經查實為 mysql-b 對應的 pause 容器的 id

從上面的規則總結一下就是目的為 3306 端口的請求都會轉發到 10.224.0.222 這個位址,即 mysql-b

看到這裡,作者明白了為什麼在 node-2 上去通路 node-1 上 mysql-a 的 3306 會提示密碼錯誤而輸入 mysql-b 的密碼卻可以正常通路

雖然兩個 mysql 的 svc 名不一樣,但上面的 iptables 隻要目的端口是 3306 就轉發到 mysql-b 了,當請求到達 mysql 後,使用正确的使用者名密碼自然可以登入成功

原因是找到了,但是又引出來了更多的問題?

這幾條規則是誰入到 iptables 中的?

怎麼解決呢,是不是删掉就可以?

問題複現

同樣是 mysql,為何 mysql-a 沒有呢? 那麼比對一下這兩個 mysql 的部署差異

比對發現, 除了使用者名密碼,ns 不一樣外,mysql-b 部署時使用了 hostport=3306, 其它的并無異常

難道是因為 hostport?

作者日常會使用 nodeport,倒卻是沒怎麼在意 hostport,也就停留在 hostport 跟 nodeport 的差别在于 nodeport 是所有 node 上都會開啟端口,而 hostport 隻會在運作機器上開啟端口,由于 hostport 使用的也少,也就沒太多關注,網上短暫搜了一番,描述的也不是很多,看起來大家也用的不多

那到底是不是因為 hostport 呢?

talk is cheap, show me the code

通過實驗來驗證,這裡簡單使用了三個 nginx 來說明問題, 其中兩個使用了 hostport,這裡特意指定了不同的端口,其它的都完全一樣,釋出到叢集中,yaml 檔案如下

finally,問題複現:

記一次 K8S HostPort 引發的服務故障排錯指南

可以肯定,這些規則就是因為使用了 hostport 而寫入的,但是由誰寫入的這個問題還是沒有解決?

罪魁禍首

作者開始以為這些 iptables 規則是由 kube-proxy 寫入的, 但是檢視 kubelet 的源碼并未發現上述規則的關鍵字

再次實驗及結合網上的探索,可以得到以下結論:

首先從 kubernetes 的官方發現以下描述:

也就是如果使用了 hostport, 是由 portmap 這個 cni 提供 portmapping 能力,同時,如果想使用這個能力,在配置檔案中一定需要開啟 portmap,這個在作者的叢集中也開啟了,這點對應上了

另外一個比較重要的結論是:

參考: https://ubuntu.com/security/cve-2019-9946

翻譯過來就是使用 hostport 後,會在 iptables 的 nat 鍊中插入相應的規則,而且這些規則是在 kube- services 規則之前插入的,也就是說會優先比對 hostport 的規則,我們常用的 nodeport 規則其實是在 kube- services 之中,也排在其後

從 portmap 的源碼中果然是可以看到相應的代碼

記一次 K8S HostPort 引發的服務故障排錯指南

是以,最終是調用 portmap 寫入的這些規則。

端口占用

進一步實驗發現,hostport 可以通過 iptables 指令檢視到, 但是無法在 ipvsadm 中檢視到

使用 lsof/netstat 也檢視不到這個端口,這是因為 hostport 是通過 iptables 對請求中的目的端口進行轉發的,并不是在主機上通過端口監聽

記一次 K8S HostPort 引發的服務故障排錯指南

既然 lsof 跟 netstat 都查不到端口資訊,那這個端口相當于沒有處于 listen 狀态?

如果這時再部署一個 hostport 提定相同端口的應用會怎麼樣呢?

結論是: 使用 hostport 的應用在排程時無法排程在已經使用過相同 hostport 的主機上,也就是說,在排程時會考慮 hostport

如果強行讓其排程在同一台機器上,那麼就會出現以下錯誤,如果不删除的話,這樣的錯誤會越來越多,吓的作者趕緊删了.

記一次 K8S HostPort 引發的服務故障排錯指南

如果這個時候建立一個 nodeport 類型的 svc, 端口也為 31123,結果會怎麼樣呢?

記一次 K8S HostPort 引發的服務故障排錯指南

可以發現,nodeport 是可以成功建立的,同時監聽的端口也出現了.

從這也可以說明使用 hostposrt 指定的端口并沒有 listen 主機的端口,要不然這裡就會提示端口重複之類

那麼問題又來了,同一台機器上同時存在有 hostport 跟 nodeport 的端口,這個時候如果 curl 31123 時, 通路的是哪一個呢?

經多次使用 curl 請求後,均是使用了 hostport 那個 nginx pod 收到請求

原因還是因為 kube-node-port 規則在 kube-service 的鍊中是處于最後位置,而 hostport 通過 portmap 寫入的規則排在其之前

記一次 K8S HostPort 引發的服務故障排錯指南

是以會先比對到 hostport 的規則,自然請求就被轉到 hostport 所在的 pod 中,這兩者的順序是沒辦法改變的,是以無論是 hostport 的應用釋出在前還是在後都無法影響請求轉發

另外再提一下,hostport 的規則在 ipvsadm 中是查詢不到的,而 nodeport 的規則則是可以使用 ipvsadm 查詢得到

問題解決

要想把這些規則删除,可以直接将 hostport 去掉,那麼規則就會随着删除,比如下圖中去掉了一個 nginx 的 hostport

記一次 K8S HostPort 引發的服務故障排錯指南

另外使用較多的 port-forward 也是可以進行端口轉發的,它又是個什麼情況呢? 它其實使用的是 socat 及 netenter 工具,網上看到一篇文章,原理寫的挺好的,感興趣的可以看一看

參考: https://vflong.github.io/sre/k8s/2020/03/15/how-the-kubectl-port-forward-command-works.html

生産建議

一句話,生産環境除非是必要且無他法,不然一定不要使用 hostport,除了會影響排程結果之外,還會出現上述問題,可能造成的後果是非常嚴重的