淺談 K8s Service 網絡機制

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

  1. 概述

Service 作爲 K8s 中的一等公民,其承載了核心容器網絡的訪問管理能力,包括:

無論是作爲應用的開發者還是使用者,一般都需要先經過 Service 纔會訪問到真正的目標 Pod。因此熟悉 Service 網絡管理機制將會使我們更加深入理解 K8s 的容器編排原理,以期更好的服務各類業務。

要理解 K8s 的中的網絡機制,可以從回答以下問題開始:

本文將從 K8s 中容器網絡、Service/Pod 關聯、Service 類型、DNS 解析、kube-proxy 模式、保留源 IP、Ingress 等方面,說明 Service 的網絡機制。

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

2.K8s 網絡訪問方式

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

分別用下圖 ①②③④ 所示:

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

  1. 容器網絡機制

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

Pod 內容器:

根據 Pod 在集羣內的分佈情況,可將 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 中默認網橋名稱爲 docker0,在 K8s 中默認網橋名稱爲 cbr0 或 cni0。

**3.2 跨 Node 間訪問

**

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

小結如下:

上述容器網絡模型爲 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.go
func 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.go
func (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 設計了四種類型,分別是:

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

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: v1
kind: Service
metadata:
  name: headless-service
spec:
  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: v1
kind: Service
metadata:
  name: nodeport-service
spec:
  selector:
    app: nginx
  ports:
    - nodePort: 30800
      port: 8080
      protocol: TCP
      targetPort: 80
  type: NodePort

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

Service NodePort 默認端口範圍:30000-32767,共 2768 個端口。
可通過 kube-apiserver 組件的 --service-node-port-range 參數進行配置。

5.3 LoadBalancer

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

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app.kubernetes.io/name: MyApp
  ports:
    - protocol: TCP
      nodePort: 30931
      port: 80
      targetPort: 9376
  clusterIP: 10.0.171.239
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 192.0.2.127

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

5.4 ExternalName

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

apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: prod
spec:
  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.local
Server:         10.4.7.51
Address:        10.4.7.51:53
Name:   my-svc.my-namespace.svc.cluster.local
Address: 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 類型,分別是:

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

7.Kube-proxy 多種模式

經過上面 Service 介紹,我們知道 Service 最終是需要負載均衡到後端的目標 Pods,在 K8s 中具體是怎麼實現的呢?

答案是 kube-proxy 組件負責 K8s 中 Service 的負載均衡實現。

具體來說,隨着 K8s 版本不斷演進,kube-proxy 分別支持了多種工作模式:

有多種方式可查看 kube-proxy 模式:

1.ps 查看 kube-proxy 進程的 --proxy-mode 參數

ps -ef | grep proxy
root       30676   29773  0  2023 ?        05:35:49 kube-proxy-bin --kubeconfig=/var/lib/kube-proxy/config --proxy-mode=ipvs --ipvs-scheduler=rr ...
  1. 通過 cm 查看配置
kubectl get cm -n kube-system kube-proxy -oyaml | grep -i mode
    mode: iptables
  1. 通過 curl kube-proxy 端口查看
curl localhost:10249/proxyMode
iptables

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-SERVICES
Chain 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-RY7QXPUTX5YFBMZE
Chain 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 子鏈後,可以看到其目的地有兩個:

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)             AGE
demo                        ClusterIP      10.255.1.28    <none>        80/TCP              209d
demo-loadbalancer           LoadBalancer   10.255.0.97    10.5.0.99     80:30015/TCP        209d
demo-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 節點。

  1. 保留源 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
Chain 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 字段來設置內部訪問策略:

9.Ingress

上述介紹的 NodePort/LoadBalancer 類型的 Service 都可以實現外部訪問集羣內部服務,但這兩種方式有一些不足:

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

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

  1. 小結

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

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IIGpMx9auyoVqdPcraEs9A