最近排查了一個 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 名跟使用者、密碼完全不相同
出現詭異的現象這裡以一張圖來說明會比較清楚一些:

其中綠線的表示通路沒有問題,紅線表示連接配接 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,問題複現:
可以肯定,這些規則就是因為使用了 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 的源碼中果然是可以看到相應的代碼
是以,最終是調用 portmap 寫入的這些規則。
端口占用
進一步實驗發現,hostport 可以通過 iptables 指令檢視到, 但是無法在 ipvsadm 中檢視到
使用 lsof/netstat 也檢視不到這個端口,這是因為 hostport 是通過 iptables 對請求中的目的端口進行轉發的,并不是在主機上通過端口監聽
既然 lsof 跟 netstat 都查不到端口資訊,那這個端口相當于沒有處于 listen 狀态?
如果這時再部署一個 hostport 提定相同端口的應用會怎麼樣呢?
結論是: 使用 hostport 的應用在排程時無法排程在已經使用過相同 hostport 的主機上,也就是說,在排程時會考慮 hostport
如果強行讓其排程在同一台機器上,那麼就會出現以下錯誤,如果不删除的話,這樣的錯誤會越來越多,吓的作者趕緊删了.
如果這個時候建立一個 nodeport 類型的 svc, 端口也為 31123,結果會怎麼樣呢?
可以發現,nodeport 是可以成功建立的,同時監聽的端口也出現了.
從這也可以說明使用 hostposrt 指定的端口并沒有 listen 主機的端口,要不然這裡就會提示端口重複之類
那麼問題又來了,同一台機器上同時存在有 hostport 跟 nodeport 的端口,這個時候如果 curl 31123 時, 通路的是哪一個呢?
經多次使用 curl 請求後,均是使用了 hostport 那個 nginx pod 收到請求
原因還是因為 kube-node-port 規則在 kube-service 的鍊中是處于最後位置,而 hostport 通過 portmap 寫入的規則排在其之前
是以會先比對到 hostport 的規則,自然請求就被轉到 hostport 所在的 pod 中,這兩者的順序是沒辦法改變的,是以無論是 hostport 的應用釋出在前還是在後都無法影響請求轉發
另外再提一下,hostport 的規則在 ipvsadm 中是查詢不到的,而 nodeport 的規則則是可以使用 ipvsadm 查詢得到
問題解決
要想把這些規則删除,可以直接将 hostport 去掉,那麼規則就會随着删除,比如下圖中去掉了一個 nginx 的 hostport
另外使用較多的 port-forward 也是可以進行端口轉發的,它又是個什麼情況呢? 它其實使用的是 socat 及 netenter 工具,網上看到一篇文章,原理寫的挺好的,感興趣的可以看一看
參考: https://vflong.github.io/sre/k8s/2020/03/15/how-the-kubectl-port-forward-command-works.html
生産建議
一句話,生産環境除非是必要且無他法,不然一定不要使用 hostport,除了會影響排程結果之外,還會出現上述問題,可能造成的後果是非常嚴重的