K8s叢集中如何保留負載均衡後的請求源IP

引言

應用部署不一定總是簡單的安裝運行,有時候還需要考慮網路的問題。本文將介紹如何在k8s叢集中使服務能獲取到請求的源IP

應用提供服務一般依賴輸入資訊,輸入資訊如果不依賴五元組(源 IP、源端口、目的 IP、目的端口、協議),那麼該服務和網路耦合性低,不需要關心網路細節。

因此,對多數人來說都沒有閱讀本文的必要,如果你對網路感興趣,或者希望拓展一點視野,可以繼續閱讀下文,了解更多的服務場景。

本文基於 k8s v1.29.4,文中部分敘述混用了 pod 和 endpoint,本文場景下可以視為等價。

如果有錯誤,歡迎指正,我會及時更正。

為什麼源 IP 資訊會丟失?

我們首先明確源 IP 是什麼,當 A 向 B 發送請求,B 將請求轉發給 C,雖然 C 看到的 IP 協議的源 IP 是 B 的 IP,但本文把A的IP看作源 IP。

主要有兩類行為會導致源資訊丟失:

  1. 網路地址轉換(NAT),目的是節省公網 IPv4,負載均衡等。將導致服務端看到的源 IP 是 NAT 設備的 IP,而不是真實的源 IP。
  2. 代理(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 首部
源 IP40-31發送方的 IP 地址
目的 IP432-63接收方的 IP 地址
TCP 首部
源端口20-15發送端口號
目的端口216-31接收端口號
序列號432-63用於標識發送方發送的資料的位元組流
確認號464-95如果設置了 ACK 旗標,則為下一個期望收到的序列號
資料偏移496-103資料起始位置相對於 TCP 首部的位元組數
保留4104-111保留欄位,未使用,設置為 0
旗標位2112-127各種控制旗標,如 SYN、ACK、FIN 等
視窗大小2128-143接收方可以接收的資料量
檢驗和2144-159用於檢測資料是否在傳輸過程中發生了錯誤
緊急指標2160-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/httpshttps://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-controllerconfigmap中的use-forwarded-headerstrue,以便Ingress Controller能夠識別X-Forwarded-ForX-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-controllerdaemonset形式部署在所有loadbalancer角色 node 上的前提下,設置externalTrafficPolicy: Local可以保留源 IP,且保留負載均衡能力。

參考