淺談 K8s Service 網絡機制
王成,騰訊雲研發工程師,Kubernetes member,從事數據庫產品容器化、資源管控等工作,關注 Kubernetes、Go、雲原生領域。
- 概述
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
分別用下圖 ①②③④ 所示:
以上多種訪問方式會在下文中講解到,理解本文內容後將會對此有更加清晰地理解和認識。
- 容器網絡機制
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.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 設計了四種類型,分別是:
-
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: 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,區別如下:
-
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: 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 類型,分別是:
-
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 proxy
root 30676 29773 0 2023 ? 05:35:49 kube-proxy-bin --kubeconfig=/var/lib/kube-proxy/config --proxy-mode=ipvs --ipvs-scheduler=rr ...
- 通過 cm 查看配置
kubectl get cm -n kube-system kube-proxy -oyaml | grep -i mode
mode: iptables
- 通過 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 子鏈後,可以看到其目的地有兩個:
-
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) 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 節點。
- 保留源 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 字段來設置內部訪問策略:
-
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/)
- 小結
本文通過介紹 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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IIGpMx9auyoVqdPcraEs9A