淺談 K8s Service 網路機制

技术颜良發表於2024-07-01

淺談 K8s Service 網路機制

雲原生運維圈 2024-07-01 12:03 上海 1人聽過

以下文章來源於騰訊雲原生 ,作者王成

騰訊雲原生.

雲原生技術交流陣地,匯聚雲原生最新技術資訊、文章、活動,以及雲原生產品及使用者最佳實踐內容。

王成,騰訊雲研發工程師,Kubernetes member,從事資料庫產品容器化、資源管控等工作,關注 Kubernetes、Go、雲原生領域。

目錄

1.概述

2.K8s 網路訪問方式

3. 容器網路機制

3.1 同一個 Node 內訪問3.2 跨 Node 間訪問

4. Service 與 Pod 關係

5. Service 網路型別

5.1 ClusterIP5.2 NodePort5.3 LoadBalancer5.4 ExternalName

6. DNS 解析機制

6.1 CoreDNS 工作機制6.2 DNS Policy

7. Kube-proxy 多種模式

7.1 userspace7.2 iptables7.3 ipvs7.4 nftables7.5 kernelspace

8. 保留源 IP

9. Ingress

10. 小結

1.概述

Service 作為 K8s 中的一等公民,其承載了核心容器網路的訪問管理能力,包括:
  • 暴露/訪問一組 Pod 的能力
  • Pod 訪問叢集內、叢集外服務
  • 叢集外客戶端訪問叢集內 Pod 的服務
無論是作為應用的開發者還是使用者,一般都需要先經過 Service 才會訪問到真正的目標 Pod。因此熟悉 Service 網路管理機制將會使我們更加深入理解 K8s 的容器編排原理,以期更好的服務各類業務。要理解 K8s 的中的網路機制,可以從回答以下問題開始:
  • K8s 中容器網路模型是怎樣的?
  • Service/Pod/Endpoints 三者之間是怎樣關聯的?
  • Service 有哪些型別?它們之間的區別是什麼?
  • K8s 中 DNS 域名解析是怎樣實現的?
  • kube-proxy 有哪些模式?各自區別是怎樣的?
  • 如何實現客戶端訪問保留源 IP?
  • 如何自定義域名、路徑訪問後端服務?
本文將從 K8s 中容器網路、Service/Pod 關聯、Service 型別、DNS 解析、kube-proxy 模式、保留源 IP、Ingress 等方面,說明 Service 的網路機制。

本文基於 K8s v1.29,不同版本 Service API 略有不同。

2.K8s網路訪問方式

根據訪問者所處角度,本文將 K8s 網路訪問方式分為四種:

  • 同一個 Node 內 Pod 訪問
  • 跨 Node 間 Pod 訪問
  • Pod 訪問叢集外服務
  • 叢集外訪問叢集內 Pod
分別用下圖 ①②③④ 所示:

圖片

以上多種訪問方式會在下文中講解到,理解本文內容後將會對此有更加清晰地理解和認識。

3.容器網路機制

K8s 將一組邏輯上緊密相關的容器,統一抽象為 Pod 概念,以共享 Pod Sandbox 的基礎資訊,如 Namespace 名稱空間、IP 分配、Volume 儲存(如 hostPath/emptyDir/PVC)等,因此討論容器的網路訪問機制,實際上可以用 Pod 訪問機制代替。

Pod 內容器:
- 共享 Network Namespace (default)
- 共享 Pid Namespace (optional)
- 不共享 Mnt Namespace (default)
根據 Pod 在叢集內的分佈情況,可將 Pod 的訪問方式主要分為兩種:
  • 同一個 Node 內 Pod 訪問
  • 跨 Node 間 Pod 訪問

3.1 同一個 Node 內訪問

同一個 Node 內訪問,表示兩個或多個 Pod 落在同一個 Node 宿主機上,這種 Pod 彼此間訪問將不會跨 Node,透過本機網路即可完成通訊。具體來說,Node 上的執行時如 Docker/containerd 會在啟動後建立預設的網橋 cbr0 (custom bridge),以連線當前 Node 上管理的所有容器 (containers)。當 Pod 建立後,會在 Pod Sandbox 初始化基礎網路時,呼叫 CNI bridge 外掛建立 veth-pair(兩張虛擬網路卡),一張預設命名 eth0 (如果 hostNetwork = false,則後續呼叫 CNI ipam 外掛分配 IP)。另一張放在 Host 的 Root Network Namespace 內,然後連線到 cbr0。當 Pod 在同一個 Node 內通訊訪問的時候,直接透過 cbr0 即可完成網橋轉發通訊。

圖片

小結如下:
  • 首先執行時如 Docker/containerd 建立 cbr0;
  • Pod Sandbox 初始化呼叫 CNI bridge 外掛建立 veth-pair(兩張虛擬網路卡);
  • 一張放在 Pod Sandbox 所在的 Network Namespace 內(CRI containerd 預設傳的引數為 eth0);
  • 另一張放在 Host 的 Root Network Namespace 內,然後連線到 cbr0;
  • Pod 在同一個 Node 內訪問直接透過 cbr0 網橋轉發;

在 Docker 中預設網橋名稱為 docker0,在 K8s 中預設網橋名稱為 cbr0 或 cni0。

3.2 跨 Node 間訪問

跨 Node 間訪問,Pod 訪問流量透過 veth-pair 打到 cbr0,之後轉發到宿主機 eth0,之後透過 Node 之間的路由表 Route Table 進行轉發。到達目標 Node 後進行相同的過程,最終轉發到目標 Pod 上。

圖片

小結如下:

  • Pod-1 eth0 -> veth-1 -> Node-1 cbr0;

  • Node-1 cbr0 -> Node-1 eth0;

  • Node-1 eth0 -> route table -> Node-2 eth0;

  • Node-2 eth0 -> Node-2 cbr0 -> veth-3 -> Pod-3 eth0;

上述容器網路模型為 K8s 中常見網路模型的一般抽象,具體實現可參考社群 CNI 實現,如 Flannel, Calico, Weave, Cilium 等。

4.Service與Pod關係

在 K8s 中,Pod 與 Pod 之間訪問,最終都是要找到目標 Pod 的 IP 地址。但 Pod 會因為多種原因如機器異常、封鎖 (cordon)、驅逐 (drain)、資源不足等情況,發生 Pod 重建,重建後則會重新分配 IP(固定 IP 除外),因此 Pod 之間訪問需要 DNS 域名方式解決 Pod IP 變更問題。DNS 具體實現機制請參考下文。

另外,為了提高服務的高可用性 (HA),一般都需要部署多副本,也就是對應多個 Pod,因此需要在多個 RS (Real Server,真實的後端伺服器) Pods 之間提供負載均衡 (Load Balance) 訪問模式,在 K8s 中則透過 Service 方式實現。

核心實現邏輯為:Service 透過指定選擇器 (selector) 去選擇與目標 Pod 匹配的標籤 (labels),找到目標 Pod 後,建立對應的 Endpoints 物件。當感知到 Service/Endpoints/Pod 物件變化時,建立或更新 Service 對應的 Endpoints,使得 Service selector 與 Pod labels 始終達到匹配的狀態。

圖片

Q:為什麼要設計 Endpoints (ep) 物件?
A:為了實現 Service 能負載均衡後端多個 Pods,需要將 Pod IP 列表放到一個均衡池子裡,因此需要一個 ep 物件來承載;另外,當 ep 對應的 IP 不是 Pod IP 的時候,也可以將流量轉發到目標 IP 上,提供了一種更加靈活的流量控制方式。

Q:EndpointSlice 又是什麼?
A:當一個 Service 對應的後端 Pods 太多時(幾百或幾千個),對應的 Endpoints 物件裡面的 IP 條目將會很多,Endpoints 物件很大會影響效能,極端情況下會導致大物件無法更新。因此在 K8s v1.21 新增了 EndpointSlice。預設情況下,一旦到達 100 個 Endpoints,該 EndpointSlice 將被視為“已滿”,屆時將建立其他 EndpointSlices 來儲存任何其他 Endpoints。
可以使用 kube-controller-manager 的 --max-endpoints-per-slice 標誌設定此值,最大值為 1000。

在 K8s 中的原始碼如下:

EndpointController 透過 Inform 機制 (ListAndWatch),分別監聽 Service/Endpoints/Pod 相關的事件變化,觸發對應的 Service 調諧 (reconcile)。

// kubernetes/pkg/controller/endpoint/endpoints_controller.gofunc NewEndpointController(ctx context.Context, podInformer coreinformers.PodInformer, serviceInformer coreinformers.ServiceInformer,    endpointsInformer coreinformers.EndpointsInformer, client clientset.Interface, endpointUpdatesBatchPeriod time.Duration) *Controller {    ...    // 透過 Inform 機制(ListAndWatch),分別監聽 Service/Endpoints/Pod 相關的事件變化    serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{       AddFunc: e.onServiceUpdate,       UpdateFunc: func(old, cur interface{}) {          e.onServiceUpdate(cur)       },       DeleteFunc: e.onServiceDelete,    })
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: e.addPod, UpdateFunc: e.updatePod, DeleteFunc: e.deletePod, })
endpointsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ DeleteFunc: e.onEndpointsDelete, }) ... return e}

Q:為什麼 ep 只需要關心 Delete 事件?
A:理解是為了讓使用者能夠自己指定 service 對應的 ep 物件(無 selector 的 Service 可手動建立同名的 ep 關聯),或者更新一些新的目標 IP 條目;
當刪除 ep 的時候,需要看下這個 Service 是不是需要 ep,若有 selector 就需要自動補齊 ep。

透過 syncService 方法實現 Service 調諧:建立或更新 Service 對應的 Endpoints。

// kubernetes/pkg/controller/endpoint/endpoints_controller.gofunc (e *Controller) syncService(ctx context.Context, key string) error {
...
// 上面的程式碼:準備要建立或更新的 Endpoints 物件 if createEndpoints { // No previous endpoints, create them _, err = e.client.CoreV1().Endpoints(service.Namespace).Create(ctx, newEndpoints, metav1.CreateOptions{}) } else { // Pre-existing _, err = e.client.CoreV1().Endpoints(service.Namespace).Update(ctx, newEndpoints, metav1.UpdateOptions{}) }
...
return nil}

5.Service網路型別

在 K8s 中,為了滿足服務對內、對外多種訪問方式,Service 設計了四種型別,分別是:

  • ClusterIP [default]

  • NodePort

  • LoadBalancer

  • ExternalName

它們之間的主要差異如圖所示:

圖片

5.1 ClusterIP

ClusterIP 表示在 K8s 叢集內部透過 service.spec.clusterIP 進行訪問,之後經過 kube-proxy 負載均衡到目標 Pod。

無頭服務 (Headless Service):
當指定 Service 的 ClusterIP = None 時,則建立的 Service 不會生成 ClusterIP,這樣 Service 域名在解析的時候,將直接解析到對應的後端 Pod (一個或多個),某些業務如果不想走 Service 預設的負載均衡,則可採用此種方式 直連 Pod。

service.spec.publishNotReadyAddresses:表示是否將沒有 ready 的 Pods 關聯到 Service,預設為 false。設定此欄位的主要場景是為 StatefulSet 的 Service 提供支援,使之能夠為其 Pod 傳播 SRV DNS 記錄,以實現對等發現。

apiVersion: v1kind: Servicemetadata:  name: headless-servicespec:  selector:    app: nginx  ports:    - protocol: TCP      port: 80      targetPort: 80  type: ClusterIP # 預設型別,可省略  clusterIP: None # 指定 ClusterIP = None  publishNotReadyAddresses: true # 是否關聯未 ready pods

當沒有指定 service.type 時,預設型別為 ClusterIP。

5.2 NodePort

當業務需要從 K8s 叢集外訪問內部服務時,透過 NodePort 方式可以先將訪問流量轉發到對應的 Node IP,然後再透過 service.spec.ports[].nodePort 埠,透過 kube-proxy 負載均衡到目標 Pod。

apiVersion: v1kind: Servicemetadata:  name: nodeport-servicespec:  selector:    app: nginx  ports:    - nodePort: 30800      port: 8080      protocol: TCP      targetPort: 80  type: NodePort

這裡可以看到有多個 port,區別如下:

  • nodePort:NodePort/LoadBalancer 型別的 Service 在 Node 節點上動態(預設)或指定建立的埠,負責將節點上的流量轉發到容器。

  • port:Service 自身關聯的埠,透過 Service 的 ClusterIP:port 進行叢集內的流量轉發。

  • targetPort:目標 Pod 暴露的埠,承載 Service 轉發過來的流量;未指定時與 port 相同。

  • containerPort:Pod 內具體 Container 暴露的埠,表示程序真實監聽的埠。

Service NodePort 預設埠範圍:30000-32767,共 2768 個埠。
可透過 kube-apiserver 元件的 --service-node-port-range 引數進行配置。

5.3 LoadBalancer

上面的 NodePort 方式訪問內部服務,需要依賴具體的 Node 高可用,如果節點掛了則會影響業務訪問,LoadBalancer 可以解決此問題。

apiVersion: v1kind: Servicemetadata:  name: my-servicespec:  selector:    app.kubernetes.io/name: MyApp  ports:    - protocol: TCP      nodePort: 30931      port: 80      targetPort: 9376  clusterIP: 10.0.171.239  type: LoadBalancerstatus:  loadBalancer:    ingress:    - ip: 192.0.2.127

具體來說,LoadBalancer 型別的 Service 建立後,由具體是雲廠商或使用者實現 externalIP (service.status.loadBalancer) 的分配,業務直接透過訪問 externalIP,然後負載均衡到目標 Pod。

5.4 ExternalName

當業務需要從 K8s 內部訪問外部服務的時候,可以透過 ExternalName 的方式實現。Demo 如下:

apiVersion: v1kind: Servicemetadata:  name: my-service  namespace: prodspec:  type: ExternalName  externalName: my.database.example.com

ExternalName Service:無 selector、無 endpoints。

具體來說,service.spec.externalName 欄位值會被解析為 DNS 對應的 CNAME 記錄,之後就可以訪問到外部對應的服務了。

6.DNS解析機制

6.1 CoreDNS 工作機制

在 K8s 中訪問 Service 一般透過域名方式如 my-svc.my-namespace.svc.cluster.local,從域名結構可以看出對應 Service 的 namespace + name,透過這樣的域名方式可以大大簡化叢集內部 Service-Pod 之間的訪問配置,並且域名遮蔽了後端 Pod IP 的變更,是 K8s 內部高可用的一個典型實現。

那上述域名具體是怎麼解析的呢?

答案是 kube-dns 元件負責 K8s 中域名的解析。

具體來說,K8s 中經歷了從早期 kube-dns 到 CoreDNS 的版本演進,從 K8s 1.12 版本開始,kube-dns 被 CoreDNS 替代成為了預設的 DNS 解決方案。

圖片

CoreDNS 工作機制:
CoreDNS 透過以 Pod 方式執行在叢集中,當其 Pod 啟動時將透過 Informer 機制從 kube-apiserver 拉取全部的 Service 資訊,然後建立對應的 DNS 記錄。當需要訪問對應的 Service 域名時,第一步透過 CoreDNS 解析拿到對應的 Service ClusterIP,之後透過上述 Service 負載均衡機制訪問目標 Pod。

bash-5.1# nslookup my-svc.my-namespace.svc.cluster.localServer:         10.4.7.51Address:        10.4.7.51:53

Name: my-svc.my-namespace.svc.cluster.localAddress: 10.4.7.201 # 對應 my-svc Service 的 ClusterIP

可透過 kubelet 上的 --cluster-dns=CoreDNS-IP--cluster-domain=cluster.local 配置叢集的 DNS 根域名和 IP。
CoreDNS-IP 可透過 Service kube-dns (為了相容老版本,所以名字還是叫 kube-dns) 的 ClusterIP 欄位獲取。

6.2 DNS Policy

在 K8s 中,為了滿足 Pod 對內、對外多種訪問方式,設計了四種 DNS Policy 型別,分別是:

  • ClusterFirst [default]

  • ClusterFirstWithHostNet

  • Default

  • None

它們之間的主要差異如圖所示:

圖片

7.Kube-proxy多種模式

經過上面 Service 介紹,我們知道 Service 最終是需要負載均衡到後端的目標 Pods,在 K8s 中具體是怎麼實現的呢?答案是 kube-proxy 元件負責 K8s 中 Service 的負載均衡實現。具體來說,隨著 K8s 版本不斷演進,kube-proxy 分別支援了多種工作模式:
  • userspace

  • iptables [default]

  • ipvs

  • nftables

  • kernelspace

有多種方式可檢視 kube-proxy 模式:1.ps 檢視 kube-proxy 程序的 --proxy-mode 引數
ps -ef | grep proxyroot       30676   29773  0  2023 ?        05:35:49 kube-proxy-bin --kubeconfig=/var/lib/kube-proxy/config --proxy-mode=ipvs --ipvs-scheduler=rr ...

2.透過 cm 檢視配置

kubectl get cm -n kube-system kube-proxy -oyaml | grep -i mode    mode: iptables

3.透過 curl kube-proxy 埠檢視

curl localhost:10249/proxyModeiptables

7.1 userspace

在 K8s v1.2 版本之前的預設模式,這種模式下 Service 的請求會先從使用者空間進入核心 iptables,然後再回到使用者空間,由 kube-proxy 完成後端 Endpoints 的選擇和代理工作。

這樣流量從使用者空間進出核心帶來的效能損耗是不可接受的,因此從 v1.2 版本之後預設改為 iptables 模式。

7.2 iptables

K8s 中當前預設的 kube-proxy 模式,核心邏輯是使用 iptables 中 PREROUTING 鏈 nat 表,實現 Service => Endpoints (Pod IP) 的負載均衡。

圖片

具體來說,訪問 Service 的流量到達 Node 後,首先在 iptables PREROUTING 鏈中 KUBE-SERVICES 子鏈進行過濾。示例如下:

iptables -t nat -nvL PREROUTING
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 312M 16G KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ 117M 5839M CNI-HOSTPORT-DNAT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

接著,KUBE-SERVICES 鏈中會將所有 Service 建立對應的 KUBE-SVC-XXX 子鏈規則,若 Service 型別是 NodePort,則命中最下面的 KUBE-NODEPORTS 子鏈。示例如下:

iptables -t nat -nvL KUBE-SERVICESChain KUBE-SERVICES (2 references) pkts bytes target     prot opt in     out     source               destination             0     0 KUBE-SVC-RY7QXPUTX5YFBMZE  tcp  --  *      *       0.0.0.0/0            11.166.11.141        /* default/demo cluster IP */ tcp dpt:80    0     0 KUBE-SVC-ADYGLFGQTCWPF2GM  tcp  --  *      *       0.0.0.0/0            11.166.26.45         /* default/demo-nodeport cluster IP */ tcp dpt:8081    0     0 KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  *      *       0.0.0.0/0            11.166.0.1           /* default/kubernetes:https cluster IP */ tcp dpt:443 4029  329K KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  *      *       0.0.0.0/0            11.166.127.254       /* kube-system/kube-dns:dns cluster IP */ udp dpt:53    ...    0     0 KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

KUBE-SVC-XXX 或其他 KUBE-XXX 相關的 Chain,後面的 XXX 是統一將部分欄位(如 servicePortName + protocol)經過 Sum256 + encode 後取其前 16 位得到。

接著,某個 Service 對應的 KUBE-SVC-XXX 子鏈的目的地 (target) 將指向 KUBE-SEP-XXX,表示 Service 對應的 Endpoints,一條 KUBE-SEP-XXX 子鏈代表一條後端 Endpoint IP。

下面的示例 KUBE-SVC-XXX 鏈包含三條 KUBE-SEP-XXX 子鏈,表示這個 Service 對應有三個 Endpoint IP,也就是對應三個後端 Pods。

iptables -t nat -nvL KUBE-SVC-RY7QXPUTX5YFBMZEChain KUBE-SVC-RY7QXPUTX5YFBMZE (1 references) pkts bytes target     prot opt in     out     source               destination             0     0 KUBE-SEP-BHJRQ3WTIY7ZLGKU  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/demo */ statistic mode random probability 0.33333333349    0     0 KUBE-SEP-6A63LY7MM76RHUDL  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/demo */ statistic mode random probability 0.50000000000    0     0 KUBE-SEP-RWT4WRVSMJ5NGBM3  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/demo */

【說明】K8s 中 iptables 透過 statistic mode random 設定多個後端 RS 被負載均衡的機率,上面示例展示了三個 Pod 各自均分 1/3 流量的規則,具體如下:
第一條 probability 0.33333333349,表示第一個 Endpoint (Pod IP) 有 1/3 的機率被負載均衡到;
第二條 probability 0.50000000000 表示剩下的 2/3 中再按 1/2 分配就是 0.5 機率;
第三條沒寫機率,則表示剩下的 1/3 都落到其上面。

繼續檢視某個 KUBE-SEP-XXX 子鏈的規則如下:表示透過均分機率命中某個 KUBE-SEP-XXX 子鏈後,可以看到其目的地有兩個:
  • KUBE-MARK-MASQ:流量從目標 Pod 出去的 SNAT 轉換,表示將 Pod IP -> Node IP。

  • DNAT:流量進入目標 Pod 的 DNAT 轉換,表示將 Node IP -> Pod IP。

Q:MASQUERADE 與 SNAT 的區別是?
A:KUBE-MARK-MASQ 是 K8s 中使用 iptables MASQUERADE 動作的一種方式,先進行標記 MARK (0x4000),然後在 POSTROUTING 鏈根據 MARK 進行真正的 MASQUERADE。
可以簡單理解為 MASQUERADE 是 SNAT 的一個特例,表示 從 Pod 訪問出去的時候偽裝 Pod 源地址。
MASQUERADE 與 SNAT 的區別:SNAT 需要指定轉換的網路卡 IP,而 MASQUERADE 可以自動獲取到傳送資料的網路卡 IP,不需要指定 IP,特別適合 IP 動態分配或會發生變化的場景。

7.3 ipvs

ipvs (IP Virtual Server) 是 LVS (Linux Virtual Server) 核心模組的一個子模組,建立於 Netfilter 之上的高效四層負載均衡器,支援 TCP 和 UDP 協議,成為 kube-proxy 使用的理想選擇。在這種模式下,kube-proxy 將規則插入到 ipvs 而非 iptables。

ipvs 具有最佳化的查詢演算法(雜湊),複雜度為 O(1)。這意味著無論插入多少規則,它幾乎都提供一致的效能。ipvs 支援多種負載均衡策略,如輪詢 (rr)、加權輪詢 (wrr)、最少連線 (lc)、源地址雜湊 (sh)、目的地址雜湊 (dh)等,K8s 中預設使用了 rr 策略。

圖片

儘管它有優勢,但是 ipvs 可能不在所有 Linux 系統中都存在。與幾乎每個 Linux 作業系統都有的 iptables 相比,ipvs 可能不是所有 Linux 系統的核心功能。如果叢集中 Service 數量不太多,iptables 應該就夠用了。

ipvs 透過如下示例來說明,首先檢視當前 svc:

k get svc                      NAME                            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)             AGEdemo                        ClusterIP      10.255.1.28    <none>        80/TCP              209ddemo-loadbalancer           LoadBalancer   10.255.0.97    10.5.0.99     80:30015/TCP        209ddemo-nodeport               NodePort       10.255.1.101   <none>        80:30180/TCP        209d...

在 Node 上,可以看到多了一個虛擬網路裝置 kube-ipvs0,型別為 dummy。

Q:什麼是 dummy 型別裝置?
A:dummy 網路卡 (dummy network interface):用於在斷網的環境下,假裝網路可以通,仍然可以透過類似 192.168.1.1 這樣的IP 訪問服務。
與環回 (loopback) 介面一樣,它是一個純虛擬介面,允許將資料包路由到指定的 IP 地址。與環回不同,IP 地址可以是任意的,並且不限於 127.0.0.0/8 範圍。

ip -d link show kube-ipvs0
3: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default link/ether xxx brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 0 maxmtu 0 dummy addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

接著,可以看到所有 Service 的 ClusterIP 都設定到了 kube-ipvs0 裝置上面,這樣訪問叢集中任意 Service 直接透過此 kube-ipvs0 裝置轉發即可。

ip a    ...3: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default     link/ether xxx brd ff:ff:ff:ff:ff:ff    inet 10.255.1.28/32 scope global kube-ipvs0       valid_lft forever preferred_lft forever    inet 10.255.0.97/32 scope global kube-ipvs0       valid_lft forever preferred_lft forever    inet 10.255.1.101/32 scope global kube-ipvs0       valid_lft forever preferred_lft forever    ...

那麼,流量到了上面的 kube-ipvs0 的某個 Service ClusterIP 後,又是怎麼負載均衡轉發後端具體某個 Pod 的呢?

可以透過 ipvsadm 客戶端工具檢視,示例如下:

ipvsadm 安裝:yum install ipvsadm -y

# 檢視上面 Service demo-nodeport (埠 30180)ipvsadm -ln | grep 30180 -A 5
TCP HostIP/VIP:30180 rr -> 10.5.0.87:18080 Masq 1 0 0 -> 10.5.0.88:18080 Masq 1 0 0 -> 10.5.0.89:18080 Masq 1 0 0 TCP ClusterIP:80 rr -> 10.5.0.87:18080

【注意】ipvs 模式下,還是會依賴 iptables 做流量過濾以及 MASQUERADE (SNAT):
PREROUTING -> KUBE-SERVICES,只不過這些規則是全域性共享的,不會隨著 Service 或 Pod 數量增加而增加。

# 全域性的 PREROUTING 鏈iptables -t nat -nvL PREROUTING
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 500M 27G KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
# 全域性的 KUBE-SERVICES 鏈iptables -t nat -nvL KUBE-SERVICES
Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-LOAD-BALANCER all -- * * 0.0.0.0/0 0.0.0.0/0 /* Kubernetes service lb portal */ match-set KUBE-LOAD-BALANCER dst,dst 0 0 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0 /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP src,dst 402 21720 KUBE-NODE-PORT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 match-set KUBE-LOAD-BALANCER dst,dst

7.4 nftables

nftables 是在 K8s v1.29 [alpha] 支援的 kube-proxy 最新模式,其目的是用來最終替代 iptables 模式。

Linux 上預設的 kube-proxy 實現目前基於 iptables。多年來,iptables 一直是 Linux 核心中首選的資料包過濾和處理系統(從 2001 年的 2.4 核心開始),但 iptables 存在無法修復的效能問題,隨著規則集大小的增加,效能損耗不斷增加。

iptables 的問題導致了後繼者 nftables(基於 Netfilter)的開發,nftables 於 2014 年首次在 3.13 核心中提供,並且從那時起它的功能和可用性日益增強,可以作為 iptables 的替代品。iptables 的開發大部分已經停止,新功能和效能改進主要進入 nftables。

Red Hat 已宣佈 iptables 在 RHEL 9 中已棄用,並且可能在幾年後的 RHEL 10 中完全刪除。其他發行版在同一方向上邁出了較小的步伐,例如 Debian 從 Debian 11 (Bullseye) 中的“必需”軟體包集中刪除了 iptables。

7.5 kernelspace

kernelspace 是當前 Windows 系統中支援的一種模式,類似早期的 userspace 模式,但工作在核心空間。

kube-proxy 在 Windows VFP (Virtual Filtering Platform 虛擬過濾平臺,類似於 Linux iptables 或 nftables 等工具) 中配置資料包過濾規則。這些規則處理節點級虛擬網路內的封裝資料包,並重寫資料包,以便目標 IP 地址正確,從而將資料包路由到正確的目的地。

kernelspace 模式僅適用於 Windows 節點。

8.保留源IP

在 K8s 中的有狀態服務如資料庫,授權方式一般都會限制客戶端來源 IP,因此在 Service 轉發流量過程中需要保留源 IP。

Service 型別 NodePort/LoadBalancer 在進行流量負載均衡時,當發現目標 Pod 不在本節點時,kube-proxy 預設 (service.spec.externalTrafficPolicy = Cluster) 會進行 SNAT 訪問到目標 Node,再訪問到目標 Pod,此時 Pod 看到的源 IP 其實是節點 IP (下圖 Node-1),獲取不到真實的客戶端源 IP。

Service 透過設定 service.spec.externalTrafficPolicy = Local 來實現源 IP 保留,之後 kube-proxy 對應的 iptables/ipvs 規則只會生成在目標 Pod 所在 Node,這樣客戶端訪問到 Node 後直接就轉發到本節點 Pod,不需要跨節點因此沒有 SNAT,因此可以保留源 IP。

圖片

如果訪問的 Node 上沒有目標 Pod,則訪問包直接被丟棄。此時,則需要在客戶端訪問側做負載均衡,將訪問流量打到含有目標 Pod 的 Node 上。

如下示例,KUBE-MARK-DROP 則表示本節點不包含目標 Pod,訪問直接被 DROP。

iptables-t nat -nvL KUBE-XLB-AANU2CJXJILSCCKLChain KUBE-XLB-AANU2CJXJILSCCKL (1 references) pkts bytes target     prot opt in     out     source               destination             0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* masquerade LOCAL traffic for default/demo-local: LB IP */ ADDRTYPE match src-type LOCAL    0     0 KUBE-SVC-AANU2CJXJILSCCKL  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* route LOCAL traffic for default/demo-local: LB IP to service chain */ ADDRTYPE match src-type LOCAL    0     0 KUBE-MARK-DROP  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/demo-local: has no local endpoints */

KUBE-XLB-XXX 表示從 K8s 叢集外部 (external) 訪問內部服務的規則,在 K8s v1.24 更改為 KUBE-EXT-XXX,更為清晰表達 external 之意。

另外,Service 還可以透過 spec.internalTrafficPolicy 欄位來設定內部訪問策略:

  • Cluster:預設值,表示從內部 Pod 訪問 Service,將透過 kube-proxy 負載均衡到所有目標 Pod(s)。

  • Local:表示從內部 Pod 訪問 Service,將透過 kube-proxy 只負載均衡到與訪問 Pod 相同的節點上的目標 Pod(s),流量不會轉發到其他節點上的 Pod(s)。

9.Ingress

上述介紹的 NodePort/LoadBalancer 型別的 Service 都可以實現外部訪問叢集內部服務,但這兩種方式有一些不足:
  • 埠占用:NodePort Service 需要在 Node 節點上佔用埠,預設 30000-32767 不能滿足太多的 Service 數量,且容易出現埠占用衝突;
  • VIP 數量:LoadBalancer Service 需要為每個 Service 分配一個 externalIP (VIP),資源比較浪費且依賴底層 VIP 實現;
  • 域名訪問:一般情況下,客戶端訪問後端服務,一般都是透過 HTTP/HTTPS 域名的方式,而不是直接 NodeIP:port 或 VIP:port;
  • 針對上述痛點問題,K8s 設計了 Ingress 資源物件(顯示定義各種域名、路徑訪問規則),透過 Ingress Controller 如 Nginx Controller 實現 Ingress 物件具體規則的轉換,可根據不同域名、同域名、不同路徑等多種組合方式,分發到不同的 Service (ClusterIP 型別),最終轉發到目標 Pod。

圖片

另外,為了支援更多的協議(如 TCP/UDP、GRPC、TLS)和更加靈活、豐富的網路管理能力,當前社群已經停止 Ingress 新特性開發,轉向 Gateway API 代替 Ingress。Gateway API 透過可擴充套件的、面向角色的、協議感知的配置機制來提供網路服務。

(Gateway API:https://kubernetes.io/docs/concepts/services-networking/gateway/)

10.小結

本文透過介紹 K8s 中容器網路、Service/Pod 關聯、Service 型別、DNS 解析、kube-proxy 模式、保留源 IP、Ingress 等方面,說明了 Service 的網路機制。小結如下:

  • 容器網路:分為同一個 Node 內訪問、跨 Node 間訪問,透過 cbr0/cni0 與 veth-pair 連線實現;

  • Service/Pod 關聯:透過選擇器 (selector) 與目標 Pod 的標籤 (labels) 匹配關聯,由 Endpoints 物件承載;

  • Service 型別:支援 ClusterIP/NodePort/LoadBalancer/ExternalName 四種型別,提供內部、外部多種訪問能力;

  • DNS 解析:透過 CoreDNS 動態管理 (create/update/delete) 叢集中 Service/Pod 的域名解析;

  • kube-proxy 模式:支援 iptables/ipvs/nftables/kernelspace 等多種代理模式,可按需選擇;

  • 保留源 IP:透過設定 externalTrafficPolicy = Local 來實現源 IP 保留,滿足特定業務需求;

  • Ingress:透過 Ingress Controller 實現 Ingress 規則的轉換,提供不同域名、不同路徑等多種網路管理能力;

參考資料

  • K8s Service 介紹:

    https://kubernetes.io/docs/concepts/services-networking/service/

  • K8s 原始碼:

    https://github.com/kubernetes/kubernetes

  • K8s 容器網路:

    https://cloud.tencent.com/developer/article/1540581

  • K8s DNS 解析:

    https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/

  • kube-proxy 模式:

    https://kubernetes.io/docs/reference/networking/virtual-ips/

  • CNI 規範:

    https://github.com/containernetworking/cni

  • KUBE-XLB 改為 EXT:

    https://github.com/kubernetes/kubernetes/pull/109060

  • 保留源 IP:

    https://kubernetes.io/docs/tutorials/services/source-ip/

  • K8s Ingress:

    https://kubernetes.io/docs/concepts/services-networking/ingress/

  • kube-proxy nftables KEP:

    https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/3866-nftables-proxy/README.md

    圖片
新增👇下面微信,拉你進群與大佬一起探討雲原生!

圖片

閱讀 203

相關文章