使用源 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。你可以按如下方式建立它:
注意
以下命令中的映象僅在 AMD64 架構上執行。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 的回覆被髮送回客戶端。
視覺化:
圖。使用 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 的端點。
視覺化:
圖。源 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 端點的節點透過故意使健康檢查失敗來從有資格進行負載均衡流量的節點列表中移除自己。
視覺化:
你可以透過設定註釋來測試這一點:
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。你所執行的雲提供商可能會以幾種不同的方式滿足負載均衡器的請求:
使用代理,該代理終止客戶端連線並開啟與你的節點/端點的新連線。在這種情況下,源 IP 始終是雲 LB 的 IP,而不是客戶端的 IP。
使用資料包轉發器,使從客戶端傳送到負載均衡器 VIP 的請求到達具有客戶端源 IP 的節點,而不是中間代理。
第一類負載均衡器必須使用負載均衡器與後端之間約定的協議來通訊真實的客戶端 IP,例如 HTTP Forwarded 或 X-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
下一步
- 瞭解更多關於透過服務連線應用程式的資訊。
- 閱讀如何建立外部負載均衡器。