學習 Kubernetes 的路上 Service 可以說是再常見不過的元件之一。我們都知道 Service 的目的就是提供單一的 endpoint 給 clients 去訪問其背後所代理的 Pods ,而今天我們就來探索 Kubernetes 到底如何實作它的吧!
初識 Service 原理
可以從 Kubernetes 官網得知每個節點上都會運行一個 kube-proxy 。它主要負責監控 K8s control plane 上 Service 資源的新增與刪除,並根據以上的動作對本機的 iptables 進行 iptables rules 的修改。那麼這些 iptables rules 就是負責攔截前往 Service (ClusterIP:port) 的網路流量,並重新導向到 Service 所代理的其中一個 endpoint (Pod)。
(圖一)
由上述官方對 Service 的解釋,我們可以得知黑魔法就隱藏在 iptables 裏頭。也就是說不論網路封包如何的路由都逃不過 iptables flow chart(如圖二),至於細節請參考鳥哥的文章。
(圖二)
問題情境
基本上在使用 Kubernetes 的 Service 時有上面的觀念就游刃有餘了,但是如果要對它進行 debug 或是深入探討其實還是得打開 iptables 瞧瞧。最近就遇到一個神秘的問題 - Nodeport Service 的 ExternalTrafficPolicy 設為 Local 時,如果從沒有 Pod 的節點(worker2)上去訪問節點自身的 IP 和 NodePort (worker2_IP:node_port) 時竟然也可以訪問的到服務。
如果理解 externalTrafficPolicy: Local 這個參數就知道,流量打進節點的 IP 和 NodePort 時不會使用預設的 random 方式去選擇 Pod ,而是就地存取該節點上 Service 所代理的 Pod ,那怎麼還會訪問的到呢?因此我就不得不打開節點的 iptables 來一探究竟,去追蹤 kube-proxy 到底設定了哪些 rules !
1. -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
2. -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
4. -A KUBE-XLB-L65ENXXZWWSAPRCR -s 10.244.0.0/16 -m comment --comment "Redirect pods trying to reach external loadbalancer VIP to clusterIP" -j KUBE-SVC-L65ENXXZWWSAPRCR -A KUBE-XLB-L65ENXXZWWSAPRCR -m comment --comment "masquerade LOCAL traffic for default/my-nginx LB IP" -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ -A KUBE-XLB-L65ENXXZWWSAPRCR -m comment --comment "route LOCAL traffic for default/my-nginx LB IP to service chain" -m addrtype --src-type LOCAL -j KUBE-SVC-L65ENXXZWWSAPRCR
5. -A KUBE-XLB-L65ENXXZWWSAPRCR -m comment --comment "default/my-nginx has no local endpoints" -j KUBE-MARK-DROP
6. -A KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000
7. -A KUBE-FIREWALL -m comment --comment "kubernetes firewall for dropping marked packets" -m mark --mark 0x8000/0x8000 -j DROP
在封包正式進入主機之前還會經過 filter table 的 input chain 做過濾
6. 任何封包進入主機前都先進 KUBE-FIREWALL chain 做檢查
7. 這條規則很明確的表示凡帶有 mark 為 0x8000/0x8000 的封包都會被 DROP ,因此在 5. ~ 6. 所執行的標記就會導致該封包被丟棄
從上面的 iptables chains flow 可以了解到 externalTrafficPolicy: Local 是如何實作請求發送到沒有提供服務的節點的情況,其實就是把封包丟棄而已。
Case 2 : 任意主機對 worker3 發送請求
1 2 3 4 5 6 7 8 9 10 11 12
# worker3 # NAT table - PREROUTING chain
1. -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
2. -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
1. -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
2. -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
6. -A KUBE-XLB-L65ENXXZWWSAPRCR -m comment --comment "route LOCAL traffic for default/my-nginx LB IP to service chain" -m addrtype --src-type LOCAL -j KUBE-SVC-L65ENXXZWWSAPRCR
由上面 iptables chains flow 可以很清楚的看到,像這樣子的訪問會經歷和 ClusterIP Service 一樣的路由方式。也就是使用機率抽籤,將去到 service/my-nginx 的網路流量隨機的路由到該 Service 所代理的 Pod 。
(圖五)
總結
在 NodePort Service 為 ExternalTrafficPolicy: Local 時 :
沒有服務的伺服器對本機 發送的請求會和 ClusterIP Service 一樣,經過 KUBE-SVC-XXX chain 進行抽籤來選擇 endpoint
任意伺服器對沒有服務的伺服器 發送的請求的封包會被 DROP
任意伺服器對有服務的伺服器 發送的請求會路由到本地的 endpoint
心得
Kubernetes 本身涵蓋許多的知識領域,而我個人認為網路的部分不論是在學習或是除錯上都相對的不容易。事實上,本篇只提及 Service 是如何透過 kube-proxy 和 iptables 來幫助我們實現封包的過濾和修改,至於封包如何透過 CNI 在 Pod 或是 server 之間路由又是另一個故事了!