最近排查了一個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, 部署在兩個namespace下,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上發現有以下的規則在其它節點上沒有
-A CNI-DN-xxxx -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.224.0.222:3306
-A CNI-HOSTPORT-DNAT -m comment --comment "dnat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-DN-xxx
-A CNI-HOSTPORT-SNAT -m comment --comment "snat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-SN-xxx
-A CNI-SN-xxx -s 127.0.0.1/32 -d 10.224.0.222/32 -p tcp -m tcp --dport 80 -j MASQUERADE
其中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文件如下
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-hostport2
labels:
k8s-app: nginx-hostport2
spec:
replicas: 1
selector:
matchLabels:
k8s-app: nginx-hostport2
template:
metadata:
labels:
k8s-app: nginx-hostport2
spec:
nodeName: spring-38
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
hostPort: 31123
Finally,問題復現:
可以肯定,這些規則就是因為使用了hostPort而寫入的,但是由誰寫入的這個問題還是沒有解決?
罪魁禍首
作者開始以為這些iptables規則是由kube-proxy寫入的, 但是查看kubelet的源碼並未發現上述規則的關鍵字
再次實驗及結合網上的探索,可以得到以下結論:
首先從kubernetes的官方發現以下描述:
The CNI networking plugin supports hostPort
. You can use the official portmap[1] plugin offered by the CNI plugin team or use your own plugin with portMapping functionality.
If you want to enable hostPort
support, you must specify portMappings capability
in your cni-conf-dir
. For example:
{
"name": "k8s-pod-network",
"cniVersion": "0.3.0",
"plugins": [
{
# ...其它的plugin
}
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}
參考官網的Network-plugins[2]
也就是如果使用了hostPort, 是由portmap這個cni提供portMapping能力,同時,如果想使用這個能力,在配置文件中一定需要開啟portmap,這個在作者的集群中也開啟了,這點對應上了
另外一個比較重要的結論是:The CNI 'portmap' plugin, used to setup HostPorts for CNI, inserts rules at the front of the iptables nat chains; which take precedence over the KUBE- SERVICES chain. Because of this, the HostPort/portmap rule could match incoming traffic even if there were better fitting, more specific service definition rules like NodePorts later in the chain
參考: https://ubuntu.com/security/CVE-2019-9946
翻譯過來就是使用hostPort后,會在iptables的nat鏈中插入相應的規則,而且這些規則是在KUBE-SERVICES規則之前插入的,也就是說會優先匹配hostPort的規則,我們常用的NodePort規則其實是在KUBE-SERVICES之中,也排在其后
從portmap的源碼中果然是可以看到相應的代碼
感興趣的可以的plugins[3]項目的meta/portmap/portmap.go
中查看完整的源碼
所以,最終是調用portmap寫入的這些規則.
端口占用
進一步實驗發現,hostport可以通過iptables命令查看到, 但是無法在ipvsadm中查看到
使用lsof/netstat也查看不到這個端口,這是因為hostport是通過iptables對請求中的目的端口進行轉發的,並不是在主機上通過端口監聽
既然lsof跟netstat都查不到端口信息,那這個端口相當於沒有處於listen狀態?
如果這時再部署一個hostport指定相同端口的應用會怎么樣呢?
結論是: 使用hostPort的應用在調度時無法調度在已經使用過相同hostPort的主機上,也就是說,在調度時會考慮hostport
如果強行讓其調度在同一台機器上,那么就會出現以下錯誤,如果不刪除的話,這樣的錯誤會越來越多,嚇的作者趕緊刪了.
如果這個時候創建一個nodePort類型的svc, 端口也為31123,結果會怎么樣呢?
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-nodeport2
labels:
k8s-app: nginx-nodeport2
spec:
replicas: 1
selector:
matchLabels:
k8s-app: nginx-nodeport2
template:
metadata:
labels:
k8s-app: nginx-nodeport2
spec:
nodeName: spring-38
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport2
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 31123
selector:
k8s-app: nginx-nodeport2
可以發現,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,除了會影響調度結果之外,還會出現上述問題,可能造成的后果是非常嚴重的。