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-XXX
KUBE-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
就不難理解這整個過程。
最後繼續使用這張圖作為總結,希望大家這時候都能夠順利的看懂這張圖要表達的一切概念