K8s叢集中如何保留負載均衡後的請求源IP
Categories:
引言
應用部署不一定總是簡單的安裝和運行,有時候還需要考慮網路的問題。本文將介紹如何在k8s叢集中使服務能獲取到請求的源IP。
應用提供服務一般依賴輸入資訊,輸入資訊如果不依賴五元組(源 IP、源端口、目的 IP、目的端口、協議),那麼該服務和網路耦合性低,不需要關心網路細節。
因此,對多數人來說都沒有閱讀本文的必要,如果你對網路感興趣,或者希望拓展一點視野,可以繼續閱讀下文,了解更多的服務場景。
本文基於 k8s v1.29.4,文中部分敘述混用了 pod 和 endpoint,本文場景下可以視為等價。
如果有錯誤,歡迎指正,我會及時更正。
為什麼源 IP 資訊會丟失?
我們首先明確源 IP 是什麼,當 A 向 B 發送請求,B 將請求轉發給 C,雖然 C 看到的 IP 協議的源 IP 是 B 的 IP,但本文把A的IP看作源 IP。
主要有兩類行為會導致源資訊丟失:
- 網路地址轉換(NAT),目的是節省公網 IPv4,負載均衡等。將導致服務端看到的源 IP 是 NAT 設備的 IP,而不是真實的源 IP。
- 代理(Proxy),反向代理(RP, Reverse Proxy)和負載均衡(LB, Load Balancer)都屬於這一類,下文統稱代理伺服器。這類代理服務會將請求轉發給後端服務,但是會將源 IP 替換為自己的 IP。
- NAT 簡單來說是以端口空間換IP空間,IPv4 地址有限,一個 IP 地址可以映射 65535 個端口,絕大多數時候這些端口沒有用完,因而可以多個子網 IP 共用一個公網 IP,在端口上區分不同的服務。其使用形式是:
public IP:public port -> private IP_1:private port,更多內容請自行參閱網路地址轉換 - 代理服務是為了隱藏或暴露,代理服務會將請求轉發給後端服務,同時將源 IP 替換為自己的 IP,以此來隱藏後端服務的真實 IP,保護後端服務的安全。代理服務的使用形式是:
client IP -> proxy IP -> server IP,更多內容請自行參閱代理
NAT和代理伺服器都非常常見,多數服務都無法獲得請求的源 IP。
這是常見的兩類修改源 IP 的途徑,如有其它歡迎補充。
如何保留源 IP?
以下是一個 HTTP 請求的例子:
| 欄位 | 長度(位元組) | 位偏移 | 描述 |
|---|---|---|---|
| IP 首部 | |||
源 IP | 4 | 0-31 | 發送方的 IP 地址 |
| 目的 IP | 4 | 32-63 | 接收方的 IP 地址 |
| TCP 首部 | |||
| 源端口 | 2 | 0-15 | 發送端口號 |
| 目的端口 | 2 | 16-31 | 接收端口號 |
| 序列號 | 4 | 32-63 | 用於標識發送方發送的資料的位元組流 |
| 確認號 | 4 | 64-95 | 如果設置了 ACK 旗標,則為下一個期望收到的序列號 |
| 資料偏移 | 4 | 96-103 | 資料起始位置相對於 TCP 首部的位元組數 |
| 保留 | 4 | 104-111 | 保留欄位,未使用,設置為 0 |
| 旗標位 | 2 | 112-127 | 各種控制旗標,如 SYN、ACK、FIN 等 |
| 視窗大小 | 2 | 128-143 | 接收方可以接收的資料量 |
| 檢驗和 | 2 | 144-159 | 用於檢測資料是否在傳輸過程中發生了錯誤 |
| 緊急指標 | 2 | 160-175 | 發送方希望接收方盡快處理的緊急資料的位置 |
| 選項 | 可變 | 176-… | 可能包括時間戳、最大報文段長度等 |
| HTTP 首部 | |||
| 請求行 | 可變 | …-… | 包括請求方法、URI 和 HTTP 版本 |
頭部欄位 | 可變 | …-… | 包含各種頭部欄位,如 Host、User-Agent 等 |
| 空行 | 2 | …-… | 用於分隔頭部和主體部分 |
| 主體 | 可變 | …-… | 選用的請求或回應正文 |
瀏覽以上 HTTP 請求結構,可以發現,有TCP選項、請求行、頭部欄位、主體是可變的,其中TCP選項空間有限,一般不會用來傳遞源 IP,請求行攜帶資訊固定不能擴展,HTTP主體加密後不能修改,只有HTTP 頭部欄位適合擴展傳遞源 IP。
HTTP header 中可以增加X-REAL-IP欄位,用來傳遞源 IP,這個操作通常放在代理伺服器上,然後代理伺服器會將請求發送給後端服務,後端服務就可以通過這個欄位獲取到源 IP 資訊。
注意,需要保證代理伺服器在NAT設備之前,這樣才能獲取到真實的請求的源 whoami。我們可以在阿里雲的產品中看到負載均衡器這個商品單獨品類,它在網路中的位置不同於普通的應用伺服器。
K8S 操作指導
以whoami項目為例進行部署。
創建 Deployment
首先創建服務:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: docker.io/traefik/whoami:latest
ports:
- containerPort: 8080
這步會創建一個Deployment,裡面包含 3 個Pod,每個 pod 包含一個容器,該容器會運行whoami服務。
創建 Service
可以創建NodePort或者LoadBalancer類型的服務,支持外部訪問,或者創建ClusterIP類型的服務,僅支持叢集內部訪問,再增加Ingress服務,通過Ingress服務暴露外部訪問。
NodePort既可以通過NodeIP:NodePort訪問,也可以通過Ingess服務訪問,方便測試,本節使用NodePort服務。
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
創建服務後,以curl whoami.example.com:30002訪問,可以看到返回的 IP 是NodeIP,而不是請求的源 whoami。
請注意,這並不是正確的客戶端 IP,它們是叢集的內部 IP。這是所發生的事情:
- 客戶端發送資料包到 node2:nodePort
- node2 使用它自己的 IP 地址替換資料包的源 IP 地址(SNAT)
- node2 將資料包上的目標 IP 替換為 Pod IP
- 資料包被路由到 node1,然後到端點
- Pod 的回覆被路由回 node2
- Pod 的回覆被發送回給客戶端
用圖表示:

配置 externalTrafficPolicy: Local
為避免這種情況,Kubernetes 有一個特性可以保留客戶端源 IP。如果將 service.spec.externalTrafficPolicy 設置為 Local,kube-proxy 只會將代理請求代理到本地端點,而不會將流量轉發到其他節點。
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
使用curl whoami.example.com:30002進行測試,當whoami.example.com映射到叢集多個 node 的 IP 時,有一定比例的幾率無法訪問。需要確認域名記錄只含有 endpoint(pod)所在 node(節點)的 ip。
這個配置有其代價,那就是失去了叢集內的負載均衡能力,客戶端只有訪問部署了 endpoint 的 node 才會得到回應。

當客戶端訪問 Node 2 時,不會有回應。
創建 Ingress
多數服務提供給使用者時使用 http/https,https://ip:port的形式可能讓使用者感到陌生。一般會使用Ingress將上文創建的NodePort服務負載到一個域名的 80/443 端口下。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
namespace: default
spec:
ingressClassName: external-lb-default
rules:
- host: whoami.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami-service
port:
number: 80
應用後,使用curl whoami.example.com訪問測試,可以看到 ClientIP 總是 endpoint 所在節點上的Ingress Controller的 Pod IP。
root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...
root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7 1/1 Running 0 3d2h 10.42.1.10 k3s-agent-1 <none> <none>
使用Ingress反向代理NodePort服務,也就是在 endpoint 前套了兩層 service,下圖展示了二者區別。
graph LR
A[Client] -->|whoami.example.com:80| B(Ingress)
B -->|10.43.38.129:32123| C[Service]
C -->|10.42.1.1:8080| D[Endpoint]graph LR
A[Client] -->|whoami.example.com:30001| B(Service)
B -->|10.42.1.1:8080| C[Endpoint]在路徑 1 中,外部訪問 Ingress 時,流量先到達的 endpoint 是Ingress Controller,然後再到達 endpoint whoami。
而Ingress Controller實質是一個LoadBalancer的服務,
kubectl -n ingress-nginx get svc
NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
default echoip-ingress nginx ip.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 18h
default whoami-ingress nginx whoami.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 16h
因此,可以通過將前文提到的externalTrafficPolicy設置到 Ingress Controller 中來保留源 IP。
同時還需要設置ingress-nginx-controller的configmap中的use-forwarded-headers為true,以便Ingress Controller能夠識別X-Forwarded-For或X-REAL-IP欄位。
apiVersion: v1
data:
allow-snippet-annotations: "false"
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
NodePort服務與ingress-nginx-controller服務的區別主要在於,NodePort的後端通常不部署在每台 node 上,而ingress-nginx-controller的後端通常部署在每台對外暴露的 node 上。
與NodePort服務中設置externalTrafficPolicy會導致跨 node 的請求無回應不同,Ingress可以將請求先設置 HEADER 之後再進行代理轉發,實現了保留源 IP和負載均衡的兩種能力。
總結
- 地址轉換(NAT),代理(Proxy)、反向代理(Reverse Proxy),**負載均衡(Load Balance)**會導致源 IP 丟失。
- 為防止源 IP 丟失,可以代理伺服器轉發時將真實 IP 設置在 HTTP 頭部欄位
X-REAL-IP中,通過代理服務傳遞。如果使用多層代理,則可以使用X-Forwarded-For欄位,該欄位以堆疊的形式記錄了源 IP 及代理路徑的 IP list。 - 叢集NodePort服務設置
externalTrafficPolicy: Local可以保留源 IP,但會失去負載均衡能力。 - ingress-nginx-controller以daemonset形式部署在所有loadbalancer角色 node 上的前提下,設置
externalTrafficPolicy: Local可以保留源 IP,且保留負載均衡能力。