Kubernetes網路難懂?可能是沒看到這篇文章

語言: CN / TW / HK

網路是非常複雜的,擁有許多概念,對於不熟悉這個領域的使用者來說,這可能會有一定的難度,這裡面有很多概念需要理解,並且還需要把這些概念整合起來形成一個連貫的整體,比如網路名稱空間、虛擬介面、IP 轉發、NAT 等概念。

Kubernetes 中對任何網路實現都規定了以下的一些要求:

  • 所有 Pod 都可以在不使用 NAT 的情況下與所有其他 Pod 進行通訊

  • 所有節點都可以在沒有 NAT 的情況下與所有 Pod 進行通訊

  • Pod 自己的 IP 與其他 Pod 看到的 IP 是相同的

鑑於這些限制,我們需要解決幾個不同的網路問題:

  1. 容器到容器的網路

  2. Pod 到 Pod 的網路

  3. Pod 到 Service 的網路

  4. 網際網路到 Service 的網路

接下來我們將來討論這些問題及其解決方案。

容器到容器網路

通常情況下我們將虛擬機器中的網路通訊視為直接與乙太網裝置進行互動,如圖1所示。

圖1.網路裝置的理想檢視

實際的情況肯定比這要複雜,在 Linux 中,每個正在執行的程序都在一個網路名稱空間內進行通訊,該名稱空間提供了一個具有自己的路由、防火牆規則和網路裝置的邏輯網路棧,從本質上講,網路名稱空間為名稱空間內的所有程序提供了一個全新的網路堆疊。

Linux 使用者可以使用 ip 命令建立網路名稱空間。例如,以下命令將建立一個名為 ns1 的網路名稱空間。

$ ip netns add ns1 

名稱空間建立後,會在 /var/run/netns 下面為其建立一個掛載點,即使沒有附加任何程序,名稱空間也是可以保留的。

你可以通過列出 /var/run/netns 下的所有掛載點或使用 ip 命令來列出可用的名稱空間。

$ ls /var/run/netns
ns1
$ ip netns
ns1

預設情況下,Linux 將為每個程序分配到 root network namespace,以提供訪問外部的能力,如圖2所示。

圖2.root network namespace

對於 Docker 而言,一個 Pod 會被構建成一組共享網路名稱空間的 Docker 容器,Pod 中的容器都有相同的 IP 地址和埠空間,它們都是通過分配給 Pod 的網路名稱空間來分配的,並且可以通過 localhost 訪問彼此,因為它們位於同一個名稱空間中。這是使用 Docker 作為 Pod 容器來實現的,它持有網路名稱空間,而應用容器則通過 Docker 的 -net=container:sandbox-container 功能加入到該名稱空間中,圖3顯示了每個 Pod 如何由共享網路名稱空間內的多個 Docker 容器(ctr*)組成的。

圖3.每個 Pod 的網路名稱空間

此外 Pod 中的容器還可以訪問共享卷,這些卷被定義為 Pod 的一部分,並且可以掛載到每個容器的檔案系統中。

Pod 到 Pod 網路

在 Kubernetes 中,每個 Pod 都有一個真實的 IP 地址,每個 Pod 都使用該 IP 地址與其他 Pod 進行通訊。接下來我們將來了解 Kubernetes 如何使用真實的 IP 來實現 Pod 與 Pod 之間的通訊的。我們先來討論同一節點上的 Pod 通訊的方式。

從 Pod 的角度來看,它存在於自己的網路名稱空間中,需要與同一節點上的其他網路名稱空間進行通訊。值得慶幸的時候,名稱空間可以使用 Linux 虛擬乙太網裝置或由兩個虛擬介面組成的 veth 對進行連線,這些虛擬介面可以分佈在多個名稱空間上。要連線 Pod 名稱空間,我們可以將 veth 對的的一側分配給 root network namespace,將另一側分配給 Pod 的網路名稱空間。每個 veth 對就像一根網線,連線兩側並允許流量在它們之間流動。這種設定可以複製到節點上的任意數量的 Pod。圖4顯示了連線虛擬機器上每個 Pod 的 root network namespace 的 veth 對。

圖4.Pod 的 veth 對

現在 Pod 都有自己的網路名稱空間,這樣它們就有自己的網路裝置和 IP 地址,並且它們連線到節點的 root 名稱空間,現在我們希望 Pod 能夠通過 root 名稱空間進行通訊,那麼我們將要使用一個網路 bridge(網橋)來實現。

Linux bridge 是用純軟體實現的虛擬交換機,有著和物理交換機相同的功能,例如二層交換,MAC 地址學習等。因此我們可以把 veth pair 等裝置繫結到網橋上,就像是把裝置連線到物理交換機上一樣。bridge 的工作方式是通過檢查通過它的資料包目的地,並決定是否將資料包傳遞給連線到網橋的其他網段,從而在源和目的地之間維護一個轉發表。bridge 通過檢視網路中每個乙太網裝置的唯一 MAC 地址來決定是橋接資料還是丟棄資料。

Bridges 實現了 ARP 協議來發現與指定 IP 地址關聯的鏈路層 MAC 地址。當 bridge 接收到資料幀的時候,bridge 將該幀廣播給所有連線的裝置(原始傳送者除外),響應該幀的裝置被儲存在一個查詢表中,未來具有相同 IP 地址的通訊使用查詢表來發現正確的 MAC 地址來轉發資料包。

圖5.使用橋接連線名稱空間

同節點 Pod 通訊

網路名稱空間將每個 Pod 隔離到自己的網路堆疊中,虛擬乙太網裝置將每個名稱空間連線到根名稱空間,以及一個將名稱空間連線在一起的網橋,這樣我們就準備好在同一節點上的 Pod 之間傳送流量了,如下圖6所示。

同節點上的Pod間的資料包移動

這上圖中,pod1 向自己的網路裝置 eth0 傳送了一個數據包,對於 pod1 來說,eth0 通過虛擬網路裝置連線到 root netns 的 veth0(1),網橋 cbr0 被配置為與 veth0 一端相連,一旦資料包到達網橋,網橋就會使用 ARP 協議將資料包傳送到 veth1(3)。當資料包到達虛擬裝置 veth1 時,它被直接轉發到 pod2 的名稱空間內的 eth0(4) 裝置。這整個過程中,每個 Pod 僅與 localhost 上的 eth0 進行通訊,流量就會被路由到正確的 Pod。

Kubernetes 的網路模型決定了 Pod 必須可以通過其 IP 地址跨節點訪問,也就是說,一個 Pod 的 IP 地址始終對網路中的其他 Pod 是可見的,每個 Pod 看待自己的 IP 地址的方式與其他 Pod 看待它的方式是相同的。接下來我們來看看不同節點上的 Pod 之間的流量路由問題。

跨節點 Pod 通訊

在研究瞭如何在同一節點上的 Pod 之間路由資料包之後,接下來我們來看下不同節點上的 Pod 之間的通訊。Kubernetes 網路模型要求 Pod 的 IP 是可以通過網路訪問的,但它並沒有規定必須如何來實現。

通常叢集中的每個節點都分配有一個 CIDR,用來指定該節點上執行的 Pod 可用的 IP 地址。一旦以 CIDR 為目的地的流量到達節點,節點就會將流量轉發到正確的 Pod。圖7展示了兩個節點之間的網路通訊,假設網路可以將 CIDR 中的流量轉發到正確的節點。

圖7.不同節點上的Pod間通訊

上圖一樣和圖6相同的地方開始請求,但是這次目標 Pod(綠色標註)與源 Pod(藍色標註)位於不同的節點上。資料包首先通過 pod1 的網路裝置傳送,該裝置與 root netns(1)中的虛擬網路裝置配對,最終資料包到達 root netns 的網橋(2)上。這個時候網橋上的 ARP 會失敗,因為與網橋相連的沒有正確的資料包 MAC 地址。一旦失敗,網橋會將資料包傳送到預設路由上 - root netns 的 eth0 裝置,此時就會路由離開節點,進入網路(3)。我們現在假設網路可以根據分配給節點的 CIDR 將資料包路由到正確的節點(4)。資料包進入目標節點的 root netns(VM2 上的 eth0),這那裡它通過網橋路由到正確的虛擬裝置(5)。最後,路由通過位於 pod4 的名稱空間(6)中的虛擬裝置 eth0 來完成。一般來說,每個節點都知道如何將資料包傳遞給其內部執行的 Pod,一旦資料包到達目標節點,資料包的流動方式與同一節點上的 Pod 間通訊方式一樣。

我們這裡沒有介紹如何配置網路來將 Pod IPs 的流量路由到負責這些 IP 的正確節點,這和特定的網路有關係,比如 AWS 就維護了一個 Kubernetes 容器網路外掛,該外掛允許在 AWS 的 VPC 環境中使用 [容器網路介面(CNI)外掛](https://github.com/aws/amazon-vpc-cni-k8s)來進行節點到節點的網路通訊。

在 EC2 中,每個例項都繫結到一個彈性網路介面 (ENI),並且所有 ENI 都連線在一個 VPC 內 —— ENI 無需額外操作即可相互訪問。預設情況下,每個 EC2 例項部署一個 ENI,但你可以建立多個 ENI 並將它們部署到 EC2 例項上。Kubernetes 的 AWS CNI 外掛會為節點上的每個 Pod 建立一個新的 ENI,因為 VPC 中的 ENI 已經連線到了現有 AWS 基礎設施中,這使得每個 Pod 的 IP 地址可以在 VPC 內自然定址。當 CNI 外掛被部署到叢集時,每個節點(EC2 例項)都會建立多個彈性網路介面,併為這些例項分配 IP 地址,從而為每個節點形成了一個 CIDR 塊。當部署 Pod 時,有一個小的二進位制檔案會作為 DaemonSet 部署到 Kubernetes 叢集中,從節點本地的 kubelet 程序接收任何新增 Pod 到網路的請求,這個二進位制檔案會從節點的可用 ENI 池中挑選一個可用的 IP 地址,並通過在 Linux 核心中連線虛擬網路裝置和網橋將其分配給 Pod,和在同一節點內容的 Pod 通訊一樣,有了這個,Pod 的流量就可以跨叢集內的節點進行通訊了。

Pod 到 Service

上面我們已經介紹瞭如何在 Pod 和它們相關的 IP 地址之間的通訊。但是 Pod 的 IP 地址並不是固定不變的,會隨著應用的擴縮容、應用崩潰或節點重啟而出現或消失,這些都可能導致 Pod IP 地址發生變化,Kubernetes 中可以通過 Service 物件來解決這個問題。

Kubernetes Service 管理一組 Pod,允許你跟蹤一組隨時間動態變化的 Pod IP 地址,Service 作為對 Pod 的抽象,為一組 Pod 分配一個虛擬的 VIP 地址,任何發往 Service VIP 的流量都會被路由到與其關聯的一組 Pod。這就允許與 Service 相關的 Pod 集可以隨時變更 - 客戶端只需要知道 Service VIP 即可。

建立 Service 時候,會建立一個新的虛擬 IP(也稱為 clusterIP),這叢集中的任何地方,發往虛擬 IP 的流量都將負載均衡到與 Service 關聯的一組 Pod。實際上,Kubernetes 會自動建立並維護一個分散式叢集內的負載均衡器,將流量分配到 Service 相關聯的健康 Pod 上。接下來讓我們仔細看看它是如何工作的。

netfilter 與 iptables

為了在叢集中執行負載均衡,Kubernetes 會依賴於 Linux 內建的網路框架 - netfilter。Netfilter 是 Linux 提供的一個框架,它允許以自定義處理程式的形式實現各種與網路相關的操作,Netfilter 為資料包過濾、網路地址轉換和埠轉換提供了各種功能和操作,它們提供了引導資料包通過網路所需的功能,以及提供禁止資料包到達計算機網路中敏感位置的能力。

iptables 是一個使用者空間程式,它提供了一個基於 table 的系統,用於定義使用 netfilter 框架操作和轉換資料包的規則。在 Kubernetes 中,iptables 規則由 kube-proxy 控制器配置,該控制器會 watch kube-apiserver 的變更,當對 Service 或 Pod 的變化更新了 Service 的虛擬 IP 地址或 Pod 的 IP 地址時,iptables 規則會被自動更新,以便正確地將指向 Service 的流量路由到支援 Pod。iptables 規則會監聽發往 Service VIP 的流量,並且在匹配時,從可用 Pod 集中選擇一個隨機 Pod IP 地址,並且 iptables 規則將資料包的目標 IP 地址從 Service 的 VIP 更改為所選的 Pod IP。當 Pod 啟動或關閉時,iptables 規則集也會更新以反映叢集的變化狀態。換句話說,iptables 已經在節點上做了負載均衡,以將指向 Service VIP 的流量路由到實際的 Pod 的 IP 上。

在返回路徑上,IP 地址來自目標 Pod,在這種情況下,iptables 再次重寫 IP 頭以將 Pod IP 替換為 Service 的 IP,以便 Pod 認為它一直只與 Service 的 IP 通訊。

IPVS

Kubernetes 新版本已經提供了另外一個用於叢集負載均衡的選項:IPVS, IPVS 也是構建在 netfilter 之上的,並作為 Linux 核心的一部分實現了傳輸層的負載均衡。IPVS 被合併到了 LVS(Linux 虛擬伺服器)中,它在主機上執行並充當真實伺服器叢集前面的負載均衡器,IPVS 可以將基於 TCP 和 UDP 的服務請求定向到真實伺服器,並使真實伺服器的服務作為虛擬服務出現在一個 IP 地址上。這使得 IPVS 非常適合 Kubernetes 服務。

這部署 kube-proxy 時,可以指定使用 iptables 或 IPVS 來實現叢集內的負載均衡。IPVS 專為負載均衡而設計,並使用更高效的資料結構(雜湊表),與 iptables  相比允許更大的規模。在使用 IPVS 模式的 Service 時,會發生三件事:在 Node 節點上建立一個虛擬 IPVS 介面,將 Service 的 VIP 地址繫結到虛擬 IPVS 介面,併為每個 Service VIP 地址建立 IPVS 伺服器。

Pod 到 Service 通訊

圖8. Pod 與 Service 之間通訊

當這 Pod 和 Service 之間路由一個數據包時,流量和以前開始的方式一樣,資料包首先通過連線到 Pod 的網路名稱空間(1)的 eth0 離開 Pod,。然後它通過虛擬網路裝置到達網橋(2)。網橋上執行的 ARP 是不知道 Service 地址的,所以它通過預設路由 eth0(3)將資料包傳輸出去。到這裡會有一些不同的地方了,在 eth0 接收之前,該資料包會被 iptables 過濾,在收到資料包後,iptables 使用 kube-proxy 在節點上安裝的規則來響應 Service 或 Pod 事件,將資料包的目的地從 Service VIP 改寫為特定的 Pod IP(4)。該資料包現在就要到達 pod4 了,而不是 Service 的 VIP,iptables 利用核心的 conntrack 工具來記錄選擇的 Pod,以便將來的流量會被路由到相同的 Pod。從本質上講,iptables 直接從節點上完成了叢集內的負載均衡,然後流量流向 Pod,剩下的就和前面的 Pod 到 Pod 通訊一樣的了(5)。

Service 到 Pod 通訊

圖9.在 Service 和 Pod 之間通訊

相應的回包的時候,收到該資料包的 Pod 將響應,將源 IP 標記為自己的 IP,將目標 IP 標記為最初發送資料包的 Pod(1)。進入節點後,資料包流經 iptables,它使用 conntrack 記住它之前所做的選擇,並將資料包的源重寫為 Service 的 VIP 而不是現在 Pod 的 IP(2)。從這裡開始,資料包通過網橋流向與 Pod 的名稱空間配對的虛擬網路裝置 (3),然後流向我們之前看到的 Pod 的虛擬網路裝置 (4)。

外網到 Service 通訊

到這裡我們已經瞭解了 Kubernetes 叢集內的流量是如何路由的,但是更多的時候我們需要將服務暴露到外部去。這個時候會涉及到兩個主要的問題:

  • 將流量從 Kubernetes 服務路由到網際網路上去

  • 將流量從網際網路傳到你的 Kubernetes 服務

接下來我們就來討論這些問題。

出流量

從節點到公共 Internet 的路由流量也是和特定的網路有關係的,這取決於你的網路如何配置來發布流量的。這裡我們以 AWS VPC 為例來進行說明。

在 AWS 中,Kubernetes 叢集在 VPC 中執行,每個節點都分配有一個私有 IP 地址,該地址可從 Kubernetes 叢集內訪問。要從叢集外部訪問服務,你可以在 VPC 上附加一個外網閘道器。外網閘道器有兩個用途:在你的 VPC 路由表中為可路由到外網的流量提供目標,以及為已分配公共 IP 地址的例項執行網路地址轉換 (NAT)。NAT 轉換負責將叢集節點的內部 IP 地址更改為公網中可用的外部 IP 地址。

有了外網閘道器,VM 就可以自由地將流量路由到外網。不過有一個小問題,Pod 有自己的 IP 地址,與執行 Pod 的節點 IP 地址不同,並且外網閘道器的 NAT 轉換僅適用於 VM IP 地址,因為它不知道哪些 Pod 在哪些 VM 上執行 —— 閘道器不支援容器。讓我們看看 Kubernetes 是如何使用 iptables 來解決這個問題的。

在下圖中,資料包源自 Pod 的名稱空間 (1),並經過連線到根名稱空間 (2) 的 veth 對。一旦進入根名稱空間,資料包就會從網橋移動到預設裝置,因為資料包上的 IP 與連線到網橋的任何網段都不匹配。在到達根名稱空間的網路裝置 (3) 之前,iptables 會破壞資料包 (3)。在這種情況下,資料包的源 IP 地址是 Pod,如果我們將源保留為 Pod,外網閘道器將拒絕它,因為閘道器 NAT 只瞭解連線到 VM 的 IP 地址。解決方案是讓 iptables 執行源 NAT —— 更改資料包源,使資料包看起來來自 VM 而不是 Pod。有了正確的源 IP,資料包現在可以離開 VM (4) 併到達外網閘道器 (5) 了。外網閘道器將執行另一個 NAT,將源 IP 從 VM 內部 IP 重寫為公網IP。最後,資料包將到達網際網路上 (6)。在返回的路上,資料包遵循相同的路徑,並且任何源 IP 的修改都會被取消,這樣系統的每一層都會接收到它理解的 IP 地址:節點或 VM 級別的 VM 內部,以及 Pod 內的 Pod IP名稱空間。

圖10.從Pod到網際網路通訊

入流量

讓流量進入你的叢集是一個非常難以解決的問題。同樣這也和特定的網路環境有關係,但是一般來說入流量可以分為兩種解決方案:

  • Service LoadBalancer

  • Ingress 控制器

LoadBalancer

當你建立一個 Kubernetes Service時,你可以選擇指定一個 LoadBalancer 來使用它。LoadBalancer 有為你提供服務的雲供應商負責建立負載均衡器,建立服務後,它將暴露負載均衡器的 IP 地址。終端使用者可以直接通過該 IP 地址與你的服務進行通訊。

LoadBalancer 到 Service

在部署了 Service 後,你使用的雲提供商將會為你建立一個新的 LoadBalancer(1)。因為 LoadBalancer 不支援容器,所以一旦流量到達 LoadBalancer,它就會分佈在叢集的各個節點上(2)。每個節點上的 iptables 規則會將來自 LoadBalancer 的傳入流量路由到正確的 Pod 上(3)。從 Pod 到客戶端的響應將返回 Pod 的 IP,但客戶端需要有 LoadBalancer 的 IP 地址。正如我們之前看到的,iptables 和 conntrack 被用來在返回路徑上正確重寫 IP 地址。

下圖展示的就是託管 Pod 的三個節點前面的負載均衡器。傳入流量(1)指向 Service 的 LoadBalancer,一旦 LoadBalancer 接收到資料包(2),它就會隨機選擇一個節點。我們這裡的示例中,我們選擇了沒有執行 Pod 的節點 VM2(3)。在這裡,執行在節點上的 iptables 規則將使用 kube-proxy 安裝到叢集中的內部負載均衡規則,將資料包轉發到正確的 Pod。iptables 執行正確的 NAT 並將資料包轉發到正確的 Pod(4)。

圖11.外網訪問 Service

Ingress 控制器

在七層網路上 Ingress 在 HTTP/HTTPS 協議範圍內執行,並建立在 Service 之上。啟用 Ingress 的第一步是使用 Kubernetes 中的 NodePort 型別的 Service,如果你將 Service 設定成 NodePort 型別,Kubernetes master 將從你指定的範圍內分配一個埠,並且每個節點都會將該埠代理到你的 Service,也就是說,任何指向節點埠的流量都將使用 iptables 規則轉發到 Service。

將節點的埠暴露在外網,可以使用一個 Ingress 物件,Ingress 是一個更高級別的 HTTP 負載均衡器,它將 HTTP 請求對映到 Kubernetes Service。根據控制器的實現方式,Ingress 的使用方式會有所不同。HTTP 負載均衡器,和四層網路負載均衡器一樣,只瞭解節點 IP(而不是 Pod IP),因此流量路由同樣利用由 kube-proxy 安裝在每個節點上的 iptables 規則提供的內部負載均衡。

在 AWS 環境中,ALB Ingress 控制器使用 AWS 的七層應用程式負載均衡器提供 Kubernetes 入口。下圖詳細介紹了此控制器建立的 AWS 元件,它還演示了 Ingress 流量從 ALB 到 Kubernetes 叢集的路由。

圖12.Ingress 控制器

建立後,(1) Ingress Controller 會 watch 來自 Kubernetes APIServer 的 Ingress 事件。當它找到滿足其要求的 Ingress 資源時,它會開始建立 AWS 資源。AWS 將 Application Load Balancer (ALB) (2) 用於 Ingress 資源。負載均衡器與用於將請求路由到一個或多個註冊節點的 TargetGroup一起工作。(3) 在 AWS 中為 Ingress 資源描述的每個唯一 Kubernetes Service 建立 TargetGroup。(4) Listener 是一個 ALB 程序,它使用你配置的協議和埠檢查連線請求。Listener 由 Ingress 控制器為你的 Ingress 資源中描述的每個埠建立。最後,為 Ingress 資源中指定的每個路徑建立 TargetGroup 規則。這可以保證到特定路徑的流量被路由到正確的 Kubernetes 服務上 (5)。

Ingress 到 Service

流經 Ingress 的資料包的生命週期與 LoadBalancer 的生命週期非常相似。主要區別在於 Ingress 知道 URL 的路徑(可以根據路徑將流量路由到 Service)Ingress 和節點之間的初始連線是通過節點上為每個服務暴露的埠。

部署 Service 後,你使用的雲提供商將為你建立一個新的 Ingress 負載均衡器 (1)。因為負載均衡器不支援容器,一旦流量到達負載均衡器,它就會通過為你的服務埠分佈在組成叢集 (2) 的整個節點中。每個節點上的 iptables 規則會將來自負載均衡器的傳入流量路由到正確的 Pod (3)。Pod 到客戶端的響應將返回 Pod 的 IP,但客戶端需要有負載均衡器的 IP 地址。正如我們之前看到的,iptables 和 conntrack 用於在返回路徑上正確重寫 IP。

圖13.從 Ingress 到 Service

總結

本文介紹了 Kubernetes 網路模型以及如何實現常見網路任務。網路知識點既廣泛又很深,所以我們這裡不可能涵蓋所有的內容,但是你可以以本文為起點,然後去深入瞭解你感興趣的主題。