使用源 IP

在 Kubernetes 叢集中執行的應用程式透過 Service 抽象來發現彼此並與外部世界通訊。本文件解釋了傳送到不同型別 Service 的資料包的源 IP 會發生什麼,以及如何根據需要切換此行為。

準備工作

術語

本文件使用以下術語:

NAT
網路地址轉換
源 NAT
替換資料包中的源 IP;在本頁中,通常指替換為節點的 IP 地址。
目標 NAT
替換資料包中的目標 IP;在本頁中,通常指替換為Pod的 IP 地址。
VIP
虛擬 IP 地址,例如分配給 Kubernetes 中每個Service的地址。
kube-proxy
在每個節點上協調 Service VIP 管理的網路守護程序。

先決條件

你需要一個 Kubernetes 叢集,並且 kubectl 命令列工具已配置為與你的叢集通訊。建議在本教程中使用至少兩個不充當控制平面主機的節點組成的叢集。如果你還沒有叢集,可以使用minikube建立,或者使用這些 Kubernetes 遊樂場之一:

這些示例使用一個小型 nginx Web 伺服器,該伺服器透過 HTTP 頭部回顯其接收到的請求的源 IP。你可以按如下方式建立它:

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

輸出為:

deployment.apps/source-ip-app created

目標

  • 透過各種型別的 Service 暴露一個簡單的應用程式。
  • 瞭解每種 Service 型別如何處理源 IP NAT。
  • 瞭解保留源 IP 的權衡。

Type=ClusterIP 的 Service 的源 IP

如果 kube-proxy 在 iptables 模式下執行(預設),則從叢集內部發送到 ClusterIP 的資料包永遠不會進行源 NAT。你可以透過在 kube-proxy 執行的節點上獲取 https://:10249/proxyMode 來查詢 kube-proxy 模式。

kubectl get nodes

輸出類似於:

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

獲取其中一個節點上的代理模式(kube-proxy 在埠 10249 上監聽):

# Run this in a shell on the node you want to query.
curl https://:10249/proxyMode

輸出為:

iptables

你可以透過在源 IP 應用程式上建立 Service 來測試源 IP 保留。

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

輸出為:

service/clusterip exposed
kubectl get svc clusterip

輸出類似於:

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

並從同一叢集中的 Pod 訪問 ClusterIP

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

輸出類似於:

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
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
    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
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

……然後使用 wget 查詢本地 Web 伺服器。

# Replace "10.0.170.92" with the IPv4 address of the Service named "clusterip"
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

無論客戶端 Pod 和伺服器 Pod 在同一節點還是不同節點,client_address 始終是客戶端 Pod 的 IP 地址。

Type=NodePort 的 Service 的源 IP

預設情況下,傳送到 Type=NodePort Service 的資料包會進行源 NAT。你可以透過建立 NodePort Service 來測試這一點:

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 }')

如果你在雲提供商上執行,可能需要為上面報告的 nodes:nodeport 開啟防火牆規則。現在你可以嘗試從叢集外部透過上面分配的節點埠訪問 Service。

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

輸出類似於:

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

請注意,這些不是正確的客戶端 IP,它們是叢集內部 IP。這就是發生的情況:

  • 客戶端向 node2:nodePort 傳送資料包。
  • node2 將資料包中的源 IP 地址(SNAT)替換為自己的 IP 地址。
  • node2 將資料包中的目標 IP 替換為 Pod IP。
  • 資料包被路由到節點 1,然後路由到端點。
  • Pod 的回覆被路由回 node2。
  • Pod 的回覆被髮送回客戶端。

視覺化:

source IP nodeport figure 01

圖。使用 SNAT 的源 IP Type=NodePort

為避免這種情況,Kubernetes 具有保留客戶端源 IP 的功能。如果你將 service.spec.externalTrafficPolicy 設定為 Local 值,則 kube-proxy 僅將代理請求代理到本地端點,並且不將流量轉發到其他節點。此方法保留了原始源 IP 地址。如果沒有本地端點,則傳送到節點的資料包將被丟棄,因此你可以在任何可能應用於成功到達端點的資料包的資料包處理規則中依賴正確的源 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=198.51.100.79

請注意,你只收到一個回覆,其中包含正確的客戶端 IP,來自執行端點 Pod 的一個節點。

這就是發生的情況:

  • 客戶端將資料包傳送到 node2:nodePort,該埠沒有任何端點。
  • 資料包被丟棄。
  • 客戶端將資料包傳送到 node1:nodePort,該埠確實有端點。
  • node1 將資料包路由到具有正確源 IP 的端點。

視覺化:

source IP nodeport figure 02

圖。源 IP Type=NodePort 保留客戶端源 IP 地址。

Type=LoadBalancer 的 Service 的源 IP

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

你可以透過負載均衡器暴露 source-ip-app 來測試這一點。

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

輸出為:

service/loadbalancer exposed

打印出 Service 的 IP 地址。

kubectl get svc loadbalancer

輸出類似於:

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

接下來,向此 Service 的外部 IP 傳送請求。

curl 203.0.113.140

輸出類似於:

CLIENT VALUES:
client_address=10.240.0.5
...

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

視覺化:

Source IP with externalTrafficPolicy

你可以透過設定註釋來測試這一點:

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

你應該立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort 欄位。

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

輸出類似於:

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 欄位指向每個節點上在 /healthz 提供健康檢查的埠。你可以測試一下:

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

輸出類似於:

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

使用 curl 獲取不同節點上的 /healthz 端點。

# Run this locally on a node you choose
curl localhost:32122/healthz
1 Service Endpoints found

在不同的節點上,你可能會得到不同的結果。

# Run this locally on a node you choose
curl localhost:32122/healthz
No Service Endpoints Found

執行在控制平面上的控制器負責分配雲負載均衡器。同一控制器還分配指向每個節點上此埠/路徑的 HTTP 健康檢查。等待大約 10 秒,讓沒有端點的 2 個節點健康檢查失敗,然後使用 curl 查詢負載均衡器的 IPv4 地址。

curl 203.0.113.140

輸出類似於:

CLIENT VALUES:
client_address=198.51.100.79
...

跨平臺支援

只有部分雲提供商支援透過 Type=LoadBalancer 的服務保留源 IP。你所執行的雲提供商可能會以幾種不同的方式滿足負載均衡器的請求:

  1. 使用代理,該代理終止客戶端連線並開啟與你的節點/端點的新連線。在這種情況下,源 IP 始終是雲 LB 的 IP,而不是客戶端的 IP。

  2. 使用資料包轉發器,使從客戶端傳送到負載均衡器 VIP 的請求到達具有客戶端源 IP 的節點,而不是中間代理。

第一類負載均衡器必須使用負載均衡器與後端之間約定的協議來通訊真實的客戶端 IP,例如 HTTP ForwardedX-FORWARDED-FOR 頭部,或者 代理協議。第二類負載均衡器可以透過建立指向 Service 中 service.spec.healthCheckNodePort 欄位中儲存的埠的 HTTP 健康檢查來利用上述功能。

清理

刪除 Service。

kubectl delete svc -l app=source-ip-app

刪除 Deployment、ReplicaSet 和 Pod。

kubectl delete deployment source-ip-app

下一步

上次修改時間:2024 年 9 月 8 日下午 5:21 PST:更新 source-ip.md (9c58a926d5)