0%

Kubernetes 之 Service 與 kube-proxy 深入理解

前言

學習 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 !

(圖三)

K8s cluster 系統環境描述

以我的環境為範例使用 kubectl get no,svc,po -owide 得到以下資訊

deployment 和 service 的 yaml 範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NAME           STATUS   ROLES                  AGE    VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE            KERNEL-VERSION     CONTAINER-RUNTIME
node/master1 Ready control-plane,master 5d1h v1.21.0 172.20.20.12 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2
node/master2 Ready control-plane,master 5d1h v1.21.0 172.20.20.13 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2
node/master3 Ready control-plane,master 5d1h v1.21.0 172.20.20.14 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2
node/worker1 Ready <none> 5d1h v1.21.0 172.20.20.15 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2
node/worker2 Ready <none> 5d1h v1.21.0 172.20.20.16 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2
node/worker3 Ready <none> 5d1h v1.21.0 172.20.20.17 <none> Ubuntu 20.04.2 LTS 5.4. 0-73-generic docker://20.10.2

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d1h <none>
service/my-nginx NodePort 10.104.89.245 <none> 80:30294/TCP 4d20h run=my-nginx

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESSGATES
pod/my-nginx-5b56ccd65f-8rww7 1/1 Running 0 5d 10.244.4.3 worker3 <none> <none>
pod/my-nginx-5b56ccd65f-bxh9v 1/1 Running 0 5d 10.244.5.4 worker1 <none> <none>

分析不同請求 iptables flow chart 的流程

現在不僅要探討上面的問題,也藉此來深入理解 Service 是如何透過 iptables 來達成的吧!因此這裡主要分析 client request 使用以下三種方式來訪問 service/my-nginx 時,整個 iptables flow 到底經過哪些 chains (以我的 K8s cluster 為範例) :

  1. worker2 以外的主機對 worker2 發送的請求
    curl 172.20.20.16:30294

  2. 任意主機對 worker3 發送請求
    curl 172.20.20.17:30294

  3. 從 worker2 對本機發送的請求
    curl 172.20.20.16:30294

(圖四)

(下面案例的 iptables chains 已經重新排序並省略不在討論內的規則)

Case 1 : worker2 以外的主機對 worker2 發送的請求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# worker2
# 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

3. -A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx" -m tcp --dport 30294 -j KUBE-XLB-L65ENXXZWWSAPRCR

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

(iptables flow chart of any host except worker2 to worker2)

  • 1. ~ 2. 封包符合目的位址為 LOCAL 所以會進入 KUBE-NODEPORTS chain

  • 3. 封包符合目的埠號為 30294 所以會進入 KUBE-XLB-L65ENXXZWWSAPRCR chain

  • 4. 封包並不符合來源位址是 10.244.0.0/16 或是本機,因此會直接跳過這些歸則

  • 5. ~ 6. 封包符合來自 worker2 或是 10.244.0.0/16 以外的封包,所以會進入 KUBE-MARK-DROP chain 並且被標記上 0x8000/0x8000

1
2
3
4
5
6
# worker2
# FILTER table - INPUT chain

6. -A INPUT -j KUBE-FIREWALL

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

3. -A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx" -m tcp --dport 30294 -j KUBE-XLB-L65ENXXZWWSAPRCR

4. -A KUBE-XLB-L65ENXXZWWSAPRCR -m comment --comment "Balancing rule 0 for default/my-nginx" -j KUBE-SEP-HL43S2RPGOP5RW4C

5. -A KUBE-SEP-HL43S2RPGOP5RW4C -p tcp -m comment --comment "default/my-nginx" -m tcp -j DNAT --to-destination 10.244.4.3:80

iptables flow chart of any host to worker3

  • 1. ~ 2. 封包符合目的位址為 LOCAL 所以會先進 KUBE-SERVICES 再進 KUBE-NODEPORTS chain

  • 3. ~ 4. 封包符合目的埠號為 30294 所以會進入 KUBE-SEP-HL43S2RPGOP5RW4C 。這條規則很明確的將封包路由到 KUBE-SEP-HL43S2RPGOP5RW4C (service endpoint) 而非 KUBE-SVC-L65ENXXZWWSAPRCR 。因此不會從所有的 service endpoints 中隨機路由到其中一個。

  • 5. 直接將封包 DNAT 到 worker3 的 Pod

最後一個 case 的 iptables chains flow 也完美解答了 externalTrafficPolicy: Local 是如何實作不經過 KUBE-SVC-XXXX 而直接路由到本地可以提供服務的 Pod 上。

Case 3 : worker2 對本機發送的請求

worker2 to worker2 的封包流向根據(圖四)是由 nat table 的 output chain 開始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# worker2 iptables
# NAT tables - OUTPUT chain

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

3. -A KUBE-NODEPORTS -s 127.0.0.0/8 -p tcp -m comment --comment "default/my-nginx" -m tcp --dport 30294 -j KUBE-MARK-MASQ

4. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

5. -A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx" -m tcp --dport 30294 -j KUBE-XLB-L65ENXXZWWSAPRCR

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

7. -A KUBE-SVC-L65ENXXZWWSAPRCR -m comment --comment "default/my-nginx" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-HL43S2RPGOP5RW4C
-A KUBE-SVC-L65ENXXZWWSAPRCR -m comment --comment "default/my-nginx" -j KUBE-SEP-CUCTBSPZ5432UVNG

8. -A KUBE-SEP-HL43S2RPGOP5RW4C -p tcp -m comment --comment "default/my-nginx" -m tcp -j DNAT --to-destination 10.244.4.3:80
-A KUBE-SEP-CUCTBSPZ5432UVNG -p tcp -m comment --comment "default/my-nginx" -m tcp -j DNAT --to-destination 10.244.5.4:80

(iptables flow chart of worker2 to worker2)

  • 1. ~ 4. 封包符合來源位址為 127.0.0.0/8 、傳輸層使用 tcp 以及目的埠號為 30294 ,因此封包會標記上 (mark) 0x4000/0x4000mark 是為了將封包分類以方便之後的處理

  • 5. ~ 6. 封包符合來源為 LOCAL 並且目的埠號為 30294,所以封包會進入KUBE-XLB-L65ENXXZWWSAPRCR 再進入 KUBE-SVC-L65ENXXZWWSAPRCR (也就是 service/my-nginx)

  • 7. 第一個規則表明只有 50% 的機會會從 KUBE-SVC-L65ENXXZWWSAPRCR 路由到 KUBE-SEP-HL43S2RPGOP5RW4C ,如果沒抽中則會路由到 KUBE-SEP-CUCTBSPZ5432UVNG。這兩個 KUBE-SEP-XXXX 事實上就是 service/my-nginx 所代理的兩個 Pods

  • 8. 最後兩條規則會依據 7. 是執行哪條規則來將封包 DNAT 到 worker3 或是 worker1 上的 Pod

Kube-proxy 如何處理沒有 Pod 的節點訪問自身

由上面 iptables chains flow 可以很清楚的看到,像這樣子的訪問會經歷和 ClusterIP Service 一樣的路由方式。也就是使用機率抽籤,將去到 service/my-nginx 的網路流量隨機的路由到該 Service 所代理的 Pod

(圖五)

總結

NodePort ServiceExternalTrafficPolicy: Local 時 :

  • 沒有服務的伺服器對本機 發送的請求會和 ClusterIP Service 一樣,經過 KUBE-SVC-XXX chain 進行抽籤來選擇 endpoint

  • 任意伺服器對沒有服務的伺服器 發送的請求的封包會被 DROP

  • 任意伺服器對有服務的伺服器 發送的請求會路由到本地的 endpoint

心得

Kubernetes 本身涵蓋許多的知識領域,而我個人認為網路的部分不論是在學習或是除錯上都相對的不容易。事實上,本篇只提及 Service 是如何透過 kube-proxy 和 iptables 來幫助我們實現封包的過濾和修改,至於封包如何透過 CNIPod 或是 server 之間路由又是另一個故事了!