Skip to content

antony@notes:~/kubernetes$ cat "理解-K8s-如何處理不同類型-service-的封包來源-IP.md"

理解 K8s 如何處理不同類型 service 的封包來源 IP

2024-12-04· kubernetes

理解 K8s 如何處理不同類型 service 的封包來源 IP

在 Kubernetes 叢集中執行的應用程式會透過服務抽象(Service abstraction)來尋找彼此並與外界溝通。 本文件將說明傳送至不同類型服務的封包的來源 IP 會如何處理,以及您如何根據需求切換此行為。

在 Kubernetes 叢集中運行的應用程式,透過 Service(服務)的抽象概念來找到彼此並進行溝通,也可以和外部世界連接。這份文件將解釋,當封包發送到不同類型的 Service 時,來源 IP(Source IP)會發生什麼變化,以及如何根據需求調整這種行為。

術語

NAT 網路位址轉換

Source NAT 取代封包上的來源 IP;在本頁中,通常是指取代為 node 的 IP 位址。

Destination NAT 取代封包上的目的地 IP;在本頁中,這通常是指取代為 Pod 的 IP 位址

VIP 虛擬 IP 位址,例如指定給 Kubernetes 中每個 Service 的虛擬 IP 位址

kube-proxy

一個網路背景程序,負責在每個 node 上協調管理 Service VIP(虛擬 IP)。

目標

  • 透過各種不同類型的 Service 將簡單應用程式對外公開
  • 瞭解每種 Service 類型如何處理來源 IP 的 NAT(網路地址轉換)
  • 瞭解在保留來源 IP 時涉及的權衡取捨

建立測試的應用程式

$ kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10

執行結果 : 

deployment.apps/source-ip-app created

Source IP for Services with Type=ClusterIP

如果您在叢集中運行 kube-proxy 並使用預設的 iptables mode,從叢集內部發送到 ClusterIP 的封包,不會進行來源 NAT(Source NAT)。您可以透過查詢 kube-proxy 所在節點上的網址 http://localhost:10249/proxyMode,來確認 kube-proxy 的運行模式。

$ curl http://localhost:10249/proxyMode

執行結果 :

ipvs

如果使用 Cilium 取代 kube-proxy,則此命令將不適用。

您可以透過為來源 IP 測試應用程式建立一個 Service,來測試來源 IP 的保留功能 :

$ kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

執行結果 :

service/clusterip exposed

查看 Service 資訊

$ kubectl get svc clusterip

執行結果 :

NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
clusterip   ClusterIP   172.100.53.4   <none>        80/TCP    28s

在同一個 K8s 叢集中,從某個 Pod 發送請求到 ClusterIP 的 Service

$ kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

輸出與此類似:

If you don't see a command prompt, try pressing enter.
/ #

然後,您可以在 Pod 內執行指令:

# Run this inside the terminal from "kubectl run"
ip addr

執行結果 :

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1480 qdisc noqueue qlen 1000
    link/ether 2e:2b:01:66:08:1b brd ff:ff:ff:ff:ff:ff
    inet 172.99.236.23/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::2c2b:1ff:fe66:81b/64 scope link
       valid_lft forever preferred_lft forever

…然後使用 wget 查詢本機 webserver

# Replace "172.100.53.4" with the IPv4 address of the Service named "clusterip"
wget -qO - 172.100.53.4

執行結果 :

...

Request Information:
        client_address=172.99.236.23
        method=GET
...

無論客戶端 Pod 和 server 端 Pod 是否位於同一個節點,client_address(客戶端位址)始終會顯示為客戶端 Pod 的 IP 地址。這表示,當您從叢集中的一個 Pod 向 ClusterIP 發送請求時,來源 IP 並未被修改

在 node 使用 wget 查詢本機 webserver

curl 172.100.53.4

執行結果 :

...
Request Information:
        client_address=172.20.1.11
        method=GET
...

Source IP for Services with Type=NodePort

傳送至 Type=NodePort 服務的封包預設為來源 NAT。 您可以建立一個 NodePort 服務來測試:

$ kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

執行結果 :

service/nodeport exposed

現在您可以嘗試從群集外部透過上述分配的節點連接埠連線到服務。

$ NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)

$ NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

$ for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

輸出類似於:

client_address=172.20.22.11

請注意,這些不是正確的用戶端 IP,而是群集內部 IP。 這就是所發生的事:

  • 用戶端傳送封包到 node2:nodePort
  • 節點 2 以自己的 IP 位址取代封包中的來源 IP 位址 (SNAT)
  • 節點 2 以 pod IP 取代封包上的目的地 IP
  • 封包會傳送到節點 1,然後傳送到端點
  • pod 的回覆會傳回到節點 2
  • pod 的回覆會傳回到用戶端

image

為了避免這種情況,Kubernetes 有一個保留 Client 端 source IP 的功能。 如果您將 service.spec.externalTrafficPolicy 設定為 Local 值,kube-proxy 只會 proxies proxy requests to local endpoints,而不會將流量轉送至其他節點。 此方法可保留原始來源 IP 位址。 如果沒有 local endpoints,傳送到節點的封包會被丟棄,因此您可以在任何封包處理規則中依賴正確的原始 IP 位址,這些規則可能會應用於傳送到端點的封包。

設定 service.spec.externalTrafficPolicy 欄位如下:

$ kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

輸出類似於:

service/nodeport patched

現在,重新執行測試:

$ for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

輸出類似於:

client_address=172.20.22.11

請注意,您只從端點 Pod 執行的節點得到一個回覆,並且是正確的 Client IP。

這就是發生的事情:

  • 客戶端傳送封包到 node2:nodePort,而 node2:nodePort 沒有任何 endpoints
  • 封包被丟棄
  • 用戶端傳送封包到 node1:nodePort,而 node1:nodePort 確實有 endpoints
  • node1 路由封包到具有正確來源 IP 的端點

image


Source IP for Services with Type=LoadBalancer

傳送至 Type=LoadBalancer 的服務的封包,預設會進行來源 NAT,因為所有處於 Ready 狀態的可排程 Kubernetes 節點,都符合負載平衡流量的資格。 因此,如果封包到達沒有端點的節點,系統會將其代理到有端點的節點,並以節點的 IP 取代封包上的來源 IP (如上一節所述)。

您可以透過負載平衡器暴露來源 IP 應用程式來測試:

$ kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

輸出為 :

service/loadbalancer exposed

列印服務的 IP 位址:

$ kubectl get svc loadbalancer

輸出與此類似 :

NAME           TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
loadbalancer   LoadBalancer   10.43.205.52   172.20.22.199   80:30729/TCP   8m24s

接下來,傳送一個要求到此服務的 external-ip:

curl 172.20.22.199

輸出與此類似:

client_address=172.20.22.11

但是,如果您在 Google Kubernetes Engine/GCE 上執行,將相同的 service.spec.externalTrafficPolicy 欄位設定為 Local,就會強制沒有 Service 端點的節點,透過故意使健康檢查失敗的方式,將自己從符合負載平衡流量資格的節點清單中移除。

image

您可以透過設定註解來測試:

$ kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

您應該會立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort 欄位:

$ kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

輸出與此類似:

  healthCheckNodePort: 30865

service.spec.healthCheckNodePort 欄位會指向每個節點上為 /healthz 提供健康檢查服務的連接埠。 您可以測試這一點:

$ kubectl get pod -o wide -l app=source-ip-app

輸出與此類似:

NAME                             READY   STATUS    RESTARTS   AGE   IP            NODE             NOMINATED NODE   READINESS GATES
source-ip-app-75c58c94c6-m57pb   1/1     Running   0          99m   10.42.0.132   antony-rancher   <none>           <none>

使用 curl 在不同節點上取得 /healthz endpoint :

# Run this locally on a node you choose
$ curl localhost:30865/healthz

輸出與此類似:

{
        "service": {
                "namespace": "default",
                "name": "loadbalancer"
        },
        "localEndpoints": 1,
        "serviceProxyHealthy": true
}

在不同的節點上,您可能會得到不同的結果:

# Run this locally on a node you choose
$ curl localhost:30865/healthz

輸出與此類似:

No Service Endpoints Found

Cleaning up

# Delete the Services:
$ kubectl delete svc -l app=source-ip-app

# Delete the Deployment, ReplicaSet and Pod:
$ kubectl delete deployment source-ip-app