Preface
本文章是屬於 kubernetes service 系列文之一,該系列文希望能夠與大家討論下列兩個觀念
- 什麼是
Kubernetes Service, 為什麼我們需要它? 它能夠幫忙解決什麼問題 Kubernetes Service是怎麼實現的?, 讓我們用 iptables 來徹徹底底的理解他
相關文章: [Kubernetes] What is Service [Kubernetes] How To Implement Kubernetes Service - ClusterIP [Kubernetes] How To Implement Kubernetes Service - SessionAffinity
本文銜接上篇文章,繼續透過對 iptables 的分析來研究 kubernetes service 中 NodePort 的實作原理。
NodePort 的功能就如同字面上的意思一樣,Node Port, 提供了一種透過存取叢集節點上事先定義好的Port Number 就可以輾轉存取到後端的真正服務。
作為一個靠腦力生存的人,每次遇到全新概念的時候,都要問問自己幾個問題
- 這個概念是想要解決什麼問題?
- 什麼時候會用到?
- 如果是我,我會怎麼實作?
Why We Need NortPort
NodePort 本身是屬於 kubernetes service的一環,自然就是要提供一個方式可以讓外部來存取集群內的服務,而且可以不用去理會後面這些容器們的真實IP地址。
既然已經有前面的 ClusterIP 提供了一種叢集內存取的方式,什麼情況下我們會需要 NodePort 這種透過存取節點的方式?
這邊使用一個下列的範例來解釋可能的情況
以下只是一種範例,但是未必是最佳解
假設今天有一個試驗環境,在Cloud Provider(Google/Azure/AWS...etc)中架設了一個kubernetes叢集,裡面透過 nginx 的方式部屬了一個網頁伺服器。
與此同時,我希望該叢集能夠提供下列的特性供我使用
- 我希望管理人員可以不需要去擔心該
nginx的狀態,其網頁服務能夠一直正常運作。 - 我可以在任意地方直接連接到該
nginx提供的網頁伺服器服務
為了滿足第一個條件,我們可以透過 kubernetes deployment 的方式去確保 nginx 的容器處於一種運行的狀態。
為了滿足第二個條件,我們可以透過 kubernetes service 的方式去連接上述的 nginx 容器們並且提供一種接口讓外部存取
在這種情況下,只要kubernetes叢集內的節點擁有一個固定的對外IP地址,同時 kubernetes server 透過 NodePorts 的方式提供該nginx往外存取的能力。
這種情況下,我們就可以在任何地方,透過直接存取該節點的對外IP地址,然後間接透過NodePort的功能存取到集群內的nginx服務。
How It Works
已經有了前述關於 ClusterIP 運作的概念後,其實要探討 NodePort 是如何實現的就非常簡單了。
我們先快速複習一下 ClusterIP 的運作流程

- 封包若來自叢集上的應用程式/節點,則跳到 `KUBE-SVC
- 如果封包的目標
IP地址是ClusterIP所提供的虛擬IP地址, 則跳到KUBE-SVC-XXXX KUBE-SVC-XXX裡面根據機率的方式,選到一個Endpoints,最後跳到KUBE-SEP-XXXKUBE-SEP-XXX裡面執行DNAT, 將封包的目標地址改成真正的容器地址,然後轉發
有了上述的概念,我們如果要支援 NodePort 這種能夠透過節點IP的方式存取的話。
想了一下,其實就是把上述的(1)/(2)改掉就好,能夠跳到 KUBE-SVC-XXX的話,後續就完全一致了。
接下來,我們繼續使用kubeDemo來進行相關的服務部屬以及iptables規則研究。
首先,我們先在環境內部署相關的 nginx 以及 kubernetes service(NodePort)
vortex-dev:06:36:12 [~/go/src/github.com/hwchiu/kubeDemo/services](master)vagrant
$kubectl apply -f deployment/nginx.yml
deployment.apps/k8s-nginx created
vortex-dev:06:36:18 [~/go/src/github.com/hwchiu/kubeDemo/services](master)vagrant
$kubectl apply -f service/nginx-node.yml
service/k8s-nginx-node created
這邊就不再敘述太多跟 service/endpoints 相關的資訊與位置,直接從 iptables 的角度出發。
我個人非常喜歡 kubernetes的一點就是所有的 iptables 的規則都會下註解,所以其實可以很輕易的透過 grep 的方式找到相關的規則。
以上述的範例來說,我們在 default namespace 中部屬了一個 k8s-nginx-node 的 kubernetes service.
所以透過 grep default/k8s-nginx-node 的方式就可以過濾出所有跟這個Service有關的所有規則。
vortex-dev:03:17:35 [~]vagrant
$sudo iptables-save | grep default/k8s-nginx-node
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp --dport 30136 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp --dport 30136 -j KUBE-SVC-RD5DSC6PXE26GCYZ
-A KUBE-SEP-VRKO3GZ2XUCPVWY5 -s 10.244.0.115/32 -m comment --comment "default/k8s-nginx-node:" -j KUBE-MARK-MASQ
-A KUBE-SEP-VRKO3GZ2XUCPVWY5 -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp -j DNAT --to-destination 10.244.0.115:80
-A KUBE-SEP-YNJKNN6SS5424R7C -s 10.244.0.113/32 -m comment --comment "default/k8s-nginx-node:" -j KUBE-MARK-MASQ
-A KUBE-SEP-YNJKNN6SS5424R7C -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp -j DNAT --to-destination 10.244.0.113:80
-A KUBE-SEP-ZGMDZ7UNNV74OV5B -s 10.244.0.114/32 -m comment --comment "default/k8s-nginx-node:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZGMDZ7UNNV74OV5B -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp -j DNAT --to-destination 10.244.0.114:80
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.98.128.179/32 -p tcp -m comment --comment "default/k8s-nginx-node: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.98.128.179/32 -p tcp -m comment --comment "default/k8s-nginx-node: cluster IP" -m tcp --dport 80 -j KUBE-SVC-RD5DSC6PXE26GCYZ
-A KUBE-SVC-RD5DSC6PXE26GCYZ -m comment --comment "default/k8s-nginx-node:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-YNJKNN6SS5424R7C
-A KUBE-SVC-RD5DSC6PXE26GCYZ -m comment --comment "default/k8s-nginx-node:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ZGMDZ7UNNV74OV5B
-A KUBE-SVC-RD5DSC6PXE26GCYZ -m comment --comment "default/k8s-nginx-node:" -j KUBE-SEP-VRKO3GZ2XUCPVWY5
我們快速的掃過所有的規則,根據 custom chain 來看,分成四個部分
- KUBE-NODEPORTS
- KUBE-SEP-XXXX
- KUBE-SERVICES
- KUBE-SVC-XXXX
這邊目前只有 KUBE-NODEPORTS 還沒有看過,剩下的都跟 ClusterIP 是一樣的功能的。
NodePort 的功能基於 ClusterIP 之上再添加新功能,所以本來 Cluster 該有的規則對於 NodePort 來說都不會少
KUBE-NODEPORTS
我們仔細觀察 KUBE-NODEPORTS 相關的兩條規則
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp --dport 30136 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/k8s-nginx-node:" -m tcp --dport 30136 -j KUBE-SVC-RD5DSC6PXE26GCYZ
第一條規則的目標是 -j KUBE-MARK-MASQ, 這部份是跟 SNAT 有關的,這個之後有機會再寫額外的文章來介紹 SNAT 相關的功能以及處理方式。
這邊只要知道這是修改封包的來源IP位址即可
第二條規則比較重要,我們可以觀察到
- 其比對條件是
-m tcp --dport 30136. - 符合條件後執行的行為是
-j KUBE-SVC-RD5DSC6PXE26GCYZ
vortex-dev:03:34:14 [~]vagrant
$kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-nginx-node NodePort 10.98.128.179 <none> 80:30136/TCP 1d
根據查詢 kubernetes svc 的結果,我們可以觀察到透過存取 30136/TCP 的方式就可以存取 NodePOrt.
而這個資訊與我們前面看到的 KUBE-NODEPORTS 這邊的規則完全一樣
最後可以發現當規則一致時,就會跳到 KUBE-SVC-XXX 去進行 endpoints 的挑選以及相關的 DNAT 功能。
哪接下來的問題只剩下一個
到底封包什麼時候會進入到 KUBE-NODEPORTS ? 只要釐清這個問題,剩下的處理方式就都跟 ClusterIP 完全一樣了。
這時候我們就要一條一條 iptables 的規則來慢慢查詢
我偷懶直接使用 -j KUBE-NODEPORTS 的方式來查詢,到底誰會跳入 KUBE-NODEPORT
vortex-dev:03:43:42 [~]vagrant
$sudo iptables-save | grep "\-j KUBE-NODEPORTS"
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
這個規則非常有趣,首先我們可以觀察到,他在 KUBE-SERVICES 這個 custom chain 裡面。 接下來可以觀察他的註解
kubernetes service nodeports; NOTE: this must be the last rule in this chain
然後看一下比對條件以及執行目標
- -m addrtype --dst-type LOCAL
- -j KUBE-NODEPORTS
第一個比對條件我們從文字上來解讀,只要封包的目標IP地址是屬於本節點上的任何網卡IP。
只要符合上述規則,就會跳到 KUBE-NODEPORT 裡面進行比對,然後就按照前述的去處理了。
對於 --dst-type LOCAL 有興趣的人可以嘗試閱讀下列這個檔案 https://elixir.bootlin.com/linux/v4.7.8/source/net/netfilter/xt_addrtype.c#L119 https://elixir.bootlin.com/linux/v4.7.8/source/include/uapi/linux/rtnetlink.h#L203 看看 kernel 內大致上是怎麼處理這系列操作的
到這邊我們整理一下所有的思路。
- NodePort 也是倚賴
KUBE-SERVICES,當封包目標是本地端的IP位置的時候,就會跳到KUBE-NODEPORT裡面去比對protocol/port來進行後續跟ClusterIP相同的處理 - 所有的
kubernetes NodePort service都會共用同一個KUBE-NODEPORT, 因此所有的NodePort使用的Port都不能一樣
我們用下列這張圖來總結 NodePort 的運作

PortBinding
由於 NodePort 會使用到節點上面的 Port 來提供服務
但從 iptables 的規則觀察下,其實 NodePort 所用到的 Port 就是一個虛擬的 Port,譬如上述範例的 30136。
為了避免有任何應用程式之後將 NodePort 要用到的 Port 給拿去使用,導致整個有任何非預期的行為出現
- 譬如某服務想要用 30136 port, 但是所有的封包都被 iptables 導走了,導致該服務一直沒有辦法接收到真正的連線
為了解決這個問題就是不要讓任何應用程式有機會使用到 30136 的連接埠,因此每個節點上面的 kube-proxy 就會幫忙做這件事情。
一旦 NodePort 成功建立後,就會將該 Port 給使用走,讓其他的應用程式沒有機會使用。
這部份我們可以透過 netstat 的指令來觀察
vortex-dev:04:08:18 [~]vagrant
$sudo netstat -ltpn | grep 30136
tcp6 0 0 :::30136 :::* LISTEN 10181/kube-proxy
這點跟 docker 的想法是很類似的,不過 docker 所啟用的 docker-proxy 其實也會幫忙 forward 這些封包,而不是單純的搶占避免服務失效而已。
vortex-dev:01:08:39 [~/go/src/github.com/linkernetworks/vortex/vendor](hwchiu/VX-62)vagrant
$sudo docker run -d -p 5566:80 nginx
f4b6b72ad82c170a92cd6ea272fc8d665b69835b8508d20e1ac2b220b2ba5b31
vortex-dev:01:08:43 [~/go/src/github.com/linkernetworks/vortex/vendor](hwchiu/VX-62)vagrant
$ps axuw | grep docker-p
root 21499 0.0 0.0 59068 2852 ? Sl 01:08 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5566 -container-ip 172.18.0.2 -container-port 80
Summary
本章節我們仔細的討論了 NodePort 各種面向的概念,最後發現其實 NodePort 的規則非常簡單,建立於 ClusterIP 之上。
只要能夠掌握 ClusterIP 是如何運作的,回過頭來看 NodePort 就不難理解這整個過程。
最後繼續使用這張圖作為總結,希望大家這時候都能夠順利的看懂這張圖要表達的一切概念
