Kubernetes網路揭秘:一個HTTP請求的旅程
- 2020 年 2 月 20 日
- 筆記
客座撰稿人:Karen Bruner,StackRox技術專員。原文可以在這裡找到。
https://www.stackrox.com/post/2020/01/kubernetes-networking-demystified/
Kubernetes集群網路可能會讓人感到非常困惑,即使對於有使用虛擬網路和請求路由實踐經驗的工程師來說也是如此。在這篇文章中,我們將介紹Kubernetes網路的複雜性,通過跟蹤HTTP請求到運行在基本Kubernetes集群上的服務過程。
我們將使用帶有兩個Linux節點的標準GoogleKubernetes引擎(GKE)集群作為示例,並說明在其他平台上細節可能有所不同。
一個HTTP請求的旅程
以瀏覽網頁的人為例。他們點擊一個鏈接,發生了一些事情,頁面就會載入。

我們需要把這些問號填一下。
在下一個圖中,請求通過Internet發送到一個非常大的雲提供商,然後發送到位於雲提供商基礎設施中的Kubernetes集群。

Kubernetes網路政策指南
當我們放大到Kubernetes集群時,我們看到雲提供商負載均衡器向Kubernetes服務(Service)資源發送請求,然後將請求路由到Kubernetes副本集(ReplicaSet)中的pod。

我們可以部署以下YAML來創建Kubernetes服務(Service,svc)和副本集(ReplicaSet,rs):
apiVersion: apps/v1 kind: ReplicaSet metadata: name: hello-world labels: app: hello-world spec: selector: matchLabels: app: hello-world replicas: 2 template: metadata: labels: app: hello-world spec: containers: - name: hello-world image: gcr.io/google-samples/node-hello:1.0 imagePullPolicy: Always ports: - containerPort: 8080 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: hello-world spec: selector: app: hello-world ports: - port: 80 targetPort: 8080 protocol: TCP type: LoadBalancer externalTrafficPolicy: Cluster
這些清單應導致在hello-world ReplicaSet中創建兩個Pod,並在雲提供商和群集網路支援的情況下,創建帶有面向外部的負載平衡器的hello-world服務資源。它還應該創建一個Kubernetes端點(Endpoint)資源,該資源在host:port表示法中有兩個條目,每個Pod都有一個,其中Pod IP為主機值和埠8080。
在我們的GKE集群上,使用kubectl查詢這些資源類型將返回以下內容:

作為參考,我們的集群具有以下IP網路:
- 節點 – 10.138.15.0/24
- 群集 – 10.16.0.0/14
- 服務 – 10.19.240.0/20
我們的服務在群集CIDR塊中具有10.19.240.1的虛擬IP地址(Virtual IP,VIP)。
現在,我們準備好從負載均衡器開始,按照請求進入Kubernetes集群的過程。
負載均衡器
雖然Kubernetes通過原生控制器和通過入口控制器提供了多種暴露服務的方法,但我們將使用LoadBalancer類型的標準Service資源。我們的hello-world服務需要GCP網路負載平衡器。每個GKE集群都有一個雲控制器,該雲控制器在集群和自動創建集群資源(包括我們的負載均衡器)所需的GCP服務的API端點之間進行連接。 (所有雲提供商都提供具有不同選項和特性的不同類別的負載均衡器。)
要查看外部負載均衡器的位置,首先我們需要從另一個角度看待集群。

kube-proxy
每個節點都有一個kube-proxy容器進程。 (在Kubernetes參考框架中,該kube-proxy容器位於kube-system命名空間的pod中。)kube-proxy管理將定址到群集Kubernetes服務對象的虛擬IP地址(VIP)的流量轉發到適當的後端Pod。kube-proxy當前支援三種不同的操作模式:
- 用戶空間(User space):此模式之所以得名,是因為服務路由發生在用戶進程空間的kube-proxy中,而不是在內核網路堆棧中。它不常用,因為它運行緩慢且過時。
- iptables:此模式使用Linux內核級Netfilter規則為Kubernetes Services配置所有路由。在大多數平台上,此模式是kube-proxy的默認模式。在為多個後端容器進行負載平衡時,它使用非加權循環調度。
- IPVS(IP虛擬伺服器):基於Netfilter框架,IPVS在Linux內核中實現第4層負載均衡,支援多種負載均衡演算法,包括最少的連接和最短的預期延遲。這種kube-proxy模式通常在Kubernetes 1.11中可用,但是它需要Linux內核載入IPVS模組。各種kubernetes網路項目也沒有像iptables模式那樣廣泛地支援它。
GKE集群中的kube-proxy在iptables模式下運行,因此我們將研究該模式的工作方式。
如果我們查看創建的hello-world服務,我們可以看到已為其分配了30510的節點埠(用於節點IP地址的網路埠)。節點網路上動態分配的埠允許群集中託管的多個Kubernetes服務在其端點中使用相同的面向Internet的埠。如果我們的服務已部署到標準的Amazon Elastic Kubernetes服務(EKS)集群,則將由Elastic Load Balancer提供服務,該服務會將傳入的連接發送到具有服務pod的節點埠。但是,Google Cloud Platform(GCP)網路負載均衡器僅將流量轉發到與負載均衡器上傳入埠位於同一埠上的目標,也即是到負載均衡器上埠80的流量將發送到目標後端上的埠80實例。 hello-world pod絕對不會在節點的埠80上偵聽。如果在節點上運行netstat,我們將看到沒有進程在該埠上偵聽。

那麼,如何通過負載平衡器建立成功的連接請求?如果kube-proxy在用戶空間模式下運行,則實際上是代理到後端Pod的連接。不過,在iptables模式下,kube-proxy配置了Netfilter鏈,因此該連接被節點的內核直接路由到後端容器的端點。
iptables
在我們的GKE集群中,如果我們登錄到其中一個節點並運行iptables,則可以看到這些規則。

藉助規則注釋,我們可以獲得與服務的負載平衡器到hello-world服務的傳入連接匹配的篩選器鏈的名稱,並遵循該鏈的規則。(在沒有規則注釋的情況下,我們仍然可以將規則的源IP地址與服務的負載均衡器進行匹配。)

我們還可以可視化網路堆棧中用於評估和修改數據包的鏈和規則,以查看我們在集群中創建的服務如何將流量定向到副本集成員。

KUBE-FW-33X6KPGSXBPETFQV鏈具有三個規則,每個規則都添加了另一個鏈來處理數據包。
- KUBE-MARK-MASQ將Netfilter標記添加到發往群集網路外部的,用於hello-world服務的數據包。帶有此標記的數據包將按照POSTROUTING規則進行更改,以使用源IP地址作為節點IP地址的源網路地址轉換(SNAT)。
- KUBE-SVC-33X6KPGSXBPETFQV鏈適用於為我們的hello-world服務綁定的所有流量,無論其來源如何,並且對每個服務端點(在本例中為兩個pod)都有規則。使用哪個端點鏈是完全隨機確定的。
- KUBE-SEP-ALRUKLHE5DT3R34X
- 如果需要的話,KUBE-MARK-MASQ再次在數據包中添加一個用於SNAT的Netfilter標記
- DNAT規則使用10.16.0.11:8080端點作為目標來設置目標NAT。
- KUBE-SEP-X7DMMHFVFOT4JLHD
- 如果需要的話,KUBE-MARK-MASQ再次在數據包中添加一個用於SNAT的Netfilter標記
- DNAT規則使用10.16.1.8:8080端點作為目標來設置目標NAT。
- KUBE-SEP-ALRUKLHE5DT3R34X
- KUBE-MARK-DROP向此點未啟用目標NAT的數據包添加Netfilter標記。這些數據包將在KUBE-FIREWALL鏈中被丟棄。
請注意,即使我們的集群有兩個節點,每個節點都有一個hello-world pod,但此路由方法並未顯示優先選擇路由到從雲負載平衡器接收請求的節點上的Pod。但是,如果我們將服務規範中的externalTrafficPolicy更改為Local,那將會改變。如果存在請求,請求不僅會轉到接收請求的節點上的Pod,而且這意味著沒有服務Pod的節點將拒絕連接。因此,通常需要將Local策略與Kubernetes守護程式集一起使用,該守護程式集會在集群中的每個節點上調度一個Pod。儘管指定本地交付顯然會減少請求的平均網路延遲,但可能導致服務Pod的負載不均衡。
Pod網路
這篇文章不會詳細介紹Pod網路,但是在我們的GKE集群中,pod網路有自己的CIDR塊,與節點的網路分開。Kubernetes網路模型要求集群中的所有Pod能夠直接相互定址,而不管其主機節點如何。GKE群集使用kubenet CNI,它在每個節點上創建到Pod網路的網橋介面,為每個節點提供自己的Pod IP地址專用CIDR塊,以簡化分配和路由。Google Compute Engine(GCE)網路可以在VM之間路由此pod網路流量。
HTTP請求
這就是我們獲取HTTP 200響應程式碼的方式。

路由變數
這篇文章提到了各種Kubernetes平台提供的不同選項可以更改路由的一些方式。這是一個不全面的列表:
- 容器網路介面(Container Network Interface,CNI)插件:每個雲提供商默認使用與其VM網路模型兼容的CNI實施。本文以默認設置的GKE群集為例。Amazon EKS中的示例看起來會有很大不同,因為AWS VPC CNI將容器直接放置在節點的VPC網路上。
- Kubernetes網路策略:Calico是實施網路策略的最受歡迎的CNI插件之一,它在節點上為每個Pod創建一個虛擬網路介面,並使用Netfilter規則來實施其防火牆規則。
- 儘管仍然使用Netfilter,但kube-proxy IPVS路由模式在大多數情況下將服務路由和NAT移出了Netfilter規則。
- 可以將流量直接發送到服務的節點埠的外部負載平衡器或其他來源,將與iptables中的其他鏈(KUBE-NODEPORTS)匹配。
- Kubernetes入口控制器可以通過多種方式更改邊緣服務路由。
- 諸如Istio之類的服務網格可能會繞過kube-proxy,並直接連接服務容器之間的內部路由。
保護服務
- 不存在將Kubernetes Service資源創建的用於向雲負載均衡器添加防火牆限制的通用方法。一些雲提供商會遵守Service規範中的loadBalancerSourceRanges欄位,該欄位可讓您提供允許連接到負載均衡器的IP CIDR塊的白名單。如果雲提供商不遵守此欄位,它將被靜默忽略,因此請務必驗證外部負載均衡器的網路配置。對於不支援loadBalancerSourceRanges欄位的提供程式,除非您在雲提供程式級別採取措施來鎖定負載均衡器和運行它們的雲網路,否則應該假定負載均衡器上的服務端點將對世界開放。雲提供商負載平衡器產品的默認防火牆設置千差萬別,取決於許多因素。一些雲提供商還可能支援對Service對象的注釋,以配置負載均衡器的安全性。
- 請注意,我們沒有通過在GKE集群中啟用Kubernetes網路策略支援來安裝Calico CNI,因為Calico創建了大量其他iptables規則,在視覺上跟蹤到Pod的虛擬路由時增加了額外的步驟。但是,我們強烈建議您使用在生產集群中實現NetworkPolicy API的CNI,並創建限制Pod流量的策略。
- 啟用HostNetwork屬性創建的Pod將共享節點的網路空間。儘管存在一些這樣做的有效用例,但通常大多數Pod都不需要位於主機網路上,尤其是對於具有root特權運行的Pod,這可能會使受感染的容器嗅探網路流量。如果您需要在節點的網路上暴露容器埠,而使用Kubernetes Service節點埠無法滿足您的需求,則可以選擇在PodSpec中為容器指定hostPort。
- 使用主機網路的Pod不應與NET_ADMIN功能一起運行,這將使它們能夠讀取和修改節點的防火牆規則。
Kubernetes網路需要大量的移動部件。它非常複雜,但是對集群中發生的事情有基本的了解將有助於您更有效地監視和保護它。
資料來源和進一步閱讀
- https://kubernetes.io/docs/concepts/services-networking/service/
- https://kubernetes.io/docs/concepts/cluster-administration/networking/
- https://twitter.com/thockin/status/1191766983735296000
- https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/
- https://netfilter.org/documentation/HOWTO/NAT-HOWTO-6.html點擊文末<<閱讀原文>>進入網頁了解更多。