Kubernetes 網絡模型來龍去脈

容器網絡發端於 Docker 的網絡。Docker 使用了一個比較簡單的網絡模型,即內部的網橋加內部的保留 IP。這種設計的好處在於容器的網絡和外部世界是解耦的,無需佔用宿主機的 IP 或者宿主機的資源,完全是虛擬的。

 

它的設計初衷是:當需要訪問外部世界時,會採用 SNAT 這種方法來借用 Node 的 IP 去訪問外面的服務。比如容器需要對外提供服務的時候,所用的是 DNAT 技術,也就是在 Node 上開一個端口,然後通過 iptable 或者別的某些機制,把流導入到容器的進程上以達到目的。

 

該模型的問題在於,外部網絡無法區分哪些是容器的網絡與流量、哪些是宿主機的網絡與流量。比如,如果要做一個高可用的時候,172.16.1.1 和 172.16.1.2 是擁有同樣功能的兩個容器,此時我們需要將兩者綁成一個 Group 對外提供服務,而這個時候我們發現從外部看來兩者沒有相同之處,它們的 IP 都是借用宿主機的端口,因此很難將兩者歸攏到一起。

 

在此基礎上,Kubernetes 提出了這樣一種機制:即每一個 Pod,也就是一個功能聚集小團伙應有自己的“身份證”,或者說 ID。在 TCP 協議棧上,這個 ID 就是 IP。

 

這個 IP 是真正屬於該 Pod 的,外部世界不管通過什麼方法一定要給它。對這個 Pod IP 的訪問就是真正對它的服務的訪問,中間拒絕任何的變造。比如以 10.1.1.1 的 IP 去訪問 10.1.2.1 的 Pod,結果到了 10.1.2.1 上發現,它實際上借用的是宿主機的 IP,而不是源 IP,這樣是不被允許的。Pod 內部會要求共享這個 IP,從而解決了一些功能內聚的容器如何變成一個部署的原子的問題。

 

剩下的問題是我們的部署手段。Kubernetes 對怎麼實現這個模型其實是沒有什麼限制的,用 underlay 網絡來控制外部路由器進行導流是可以的;如果希望解耦,用 overlay 網絡在底層網絡之上再加一層疊加網,這樣也是可以的。總之,只要達到模型所要求的目的即可。

 

Pod 究竟如何上網

 

容器網絡的網絡包究竟是怎麼傳送的?

 

我們可以從以下兩個維度來看:

 

  • 協議層次
  • 網絡拓撲

 

1. 協議層次

 

它和 TCP 協議棧的概念是相同的,需要從兩層、三層、四層一層層地摞上去,發包的時候從右往左,即先有應用數據,然後發到了 TCP 或者 UDP 的四層協議,繼續向下傳送,加上 IP 頭,再加上 MAC 頭就可以送出去了。收包的時候則按照相反的順序,首先剝離 MAC 的頭,再剝離 IP 的頭,最後通過協議號在端口找到需要接收的進程。

 

2. 網絡拓撲

 

一個容器的包所要解決的問題分爲兩步:第一步,如何從容器的空間 (c1) 跳到宿主機的空間 (infra);第二步,如何從宿主機空間到達遠端。

 

我個人的理解是,容器網絡的方案可以通過接入、流控、通道這三個層面來考慮。

 

  • 第一個是接入,就是說我們的容器和宿主機之間是使用哪一種機制做連接,比如 Veth + bridge、Veth + pair 這樣的經典方式,也有利用高版本內核的新機制等其他方式(如 mac/IPvlan 等),來把包送入到宿主機空間;

 

  • 第二個是流控,就是說我的這個方案要不要支持 Network Policy,如果支持的話又要用何種方式去實現。這裏需要注意的是,我們的實現方式一定需要在數據路徑必經的一個關節點上。如果數據路徑不通過該 Hook 點,那就不會起作用;

 

  • 第三個是通道,即兩個主機之間通過什麼方式完成包的傳輸。我們有很多種方式,比如以路由的方式,具體又可分爲 BGP 路由或者直接路由。還有各種各樣的隧道技術等等。最終我們實現的目的就是一個容器內的包通過容器,經過接入層傳到宿主機,再穿越宿主機的流控模塊(如果有)到達通道送到對端。

 

3. 一個最簡單的路由方案:Flannel-host-gw

 

這個方案採用的是每個 Node 獨佔網段,每個 Subnet 會綁定在一個 Node 上,網關也設置在本地,或者說直接設在 cni0 這個網橋的內部端口上。該方案的好處是管理簡單,壞處就是無法跨 Node 遷移 Pod。就是說這個 IP、網段已經是屬於這個 Node 之後就無法遷移到別的 Node 上。

 

這個方案的精髓在於 route 表的設置,如上圖所示。接下來爲大家一一解讀一下。

 

  • 第一條很簡單,我們在設置網卡的時候都會加上這一行。就是指定我的默認路由是通過哪個 IP 走掉,默認設備又是什麼;

 

  • 第二條是對 Subnet 的一個規則反饋。就是說我的這個網段是 10.244.0.0,掩碼是 24 位,它的網關地址就在網橋上,也就是 10.244.0.1。這就是說這個網段的每一個包都發到這個網橋的 IP 上;

 

  • 第三條是對對端的一個反饋。如果你的網段是 10.244.1.0(上圖右邊的 Subnet),我們就把它的 Host 的網卡上的 IP (10.168.0.3) 作爲網關。也就是說,如果數據包是往 10.244.1.0 這個網段發的,就請以 10.168.0.3 作爲網關。

再來看一下這個數據包到底是如何跑起來的?

 

假設容器 (10.244.0.2) 想要發一個包給 10.244.1.3,那麼它在本地產生了 TCP 或者 UDP 包之後,再依次填好對端 IP 地址、本地以太網的 MAC 地址作爲源 MAC 以及對端 MAC。一般來說本地會設定一條默認路由,默認路由會把 cni0 上的 IP 作爲它的默認網關,對端的 MAC 就是這個網關的 MAC 地址。然後這個包就可以發到橋上去了。如果網段在本橋上,那麼通過 MAC 層的交換即可解決。

 

這個例子中我們的 IP 並不屬於本網段,因此網橋會將其上送到主機的協議棧去處理。主機協議棧恰好找到了對端的 MAC 地址。使用 10.168.0.3 作爲它的網關,通過本地 ARP 探查後,我們得到了 10.168.0.3 的 MAC 地址。即通過協議棧層層組裝,我們達到了目的,將 Dst-MAC 填爲右圖主機網卡的 MAC 地址,從而將包從主機的 eth0 發到對端的 eth0 上去。

 

所以大家可以發現,這裏有一個隱含的限制,上圖中的 MAC 地址填好之後一定是能到達對端的,但如果這兩個宿主機之間不是二層連接的,中間經過了一些網關、一些複雜的路由,那麼這個 MAC 就不能直達,這種方案就是不能用的。當包到達了對端的 MAC 地址之後,發現這個包確實是給它的,但是 IP 又不是它自己的,就開始 Forward 流程,包上送到協議棧,之後再走一遍路由,剛好會發現 10.244.1.0/24 需要發到 10.244.1.1 這個網關上,從而到達了 cni0 網橋,它會找到 10.244.1.3 對應的 MAC 地址,再通過橋接機制,這個包就到達了對端容器。

 

大家可以看到,整個過程總是二層、三層,發的時候又變成二層,再做路由,就是一個大環套小環。這是一個比較簡單的方案,如果中間要走隧道,則可能會有一條 vxlan tunnel 的設備,此時就不填直接的路由,而填成對端的隧道號。

 

Service 究竟如何工作

 

Service 其實是一種負載均衡 (Load Balance) 的機制。

 

我們認爲它是一種用戶側(Client Side) 的負載均衡,也就是說 VIP 到 RIP 的轉換在用戶側就已經完成了,並不需要集中式地到達某一個 NGINX 或者是一個 ELB 這樣的組件來進行決策。

 

它的實現是這樣的:首先是由一羣 Pod 組成一組功能後端,再在前端上定義一個虛 IP 作爲訪問入口。一般來說,由於 IP 不太好記,我們還會附贈一個 DNS 的域名,Client 先訪問域名得到虛 IP 之後再轉成實 IP。Kube-proxy 則是整個機制的實現核心,它隱藏了大量的複雜性。它的工作機制是通過 apiserver 監控 Pod/Service 的變化(比如是不是新增了 Service、Pod)並將其反饋到本地的規則或者是用戶態進程。

 

一個 LVS 版的 Service

 

我們來實際做一個 LVS 版的 Service。LVS 是一個專門用於負載均衡的內核機制。它工作在第四層,性能會比用 iptable 實現好一些。

 

假設我們是一個 Kube-proxy,拿到了一個 Service 的配置,如下圖所示:它有一個 Cluster IP,在該 IP 上的端口是 9376,需要反饋到容器上的是 80 端口,還有三個可工作的 Pod,它們的 IP 分別是 10.1.2.3, 10.1.14.5, 10.1.3.8。

它要做的事情就是:

  • 第 1 步,綁定 VIP 到本地(欺騙內核);

 

首先需要讓內核相信它擁有這樣的一個虛 IP,這是 LVS 的工作機制所決定的,因爲它工作在第四層,並不關心 IP 轉發,只有它認爲這個 IP 是自己的纔會拆到 TCP 或 UDP 這一層。在第一步中,我們將該 IP 設到內核中,告訴內核它確實有這麼一個 IP。實現的方法有很多,我們這裏用的是 ip route 直接加 local 的方式,用 Dummy 啞設備上加 IP 的方式也是可以的。

  • 第 2 步,爲這個虛 IP 創建一個 IPVS 的 virtual server;

 

告訴它我需要爲這個 IP 進行負載均衡分發,後面的參數就是一些分發策略等等。virtual server 的 IP 其實就是我們的 Cluster IP。

  • 第 3 步,爲這個 IPVS service 創建相應的 real server。

 

我們需要爲 virtual server 配置相應的 real server,就是真正提供服務的後端是什麼。比如說我們剛纔看到有三個 Pod,於是就把這三個的 IP 配到 virtual server 上,完全一一對應過來就可以了。Kube-proxy 工作跟這個也是類似的。只是它還需要去監控一些 Pod 的變化,比如 Pod 的數量變成 5 個了,那麼規則就應變成 5 條。如果這裏面某一個 Pod 死掉了或者被殺死了,那麼就要相應地減掉一條。又或者整個 Service 被撤銷了,那麼這些規則就要全部刪掉。所以它其實做的是一些管理層面的工作。

 

啥?負載均衡還分內部外部

 

最後我們介紹一下 Service 的類型,可以分爲以下 4 類。

1. ClusterIP

 

集羣內部的一個虛擬 IP,這個 IP 會綁定到一堆服務的 Group Pod 上面,這也是默認的服務方式。它的缺點是這種方式只能在 Node 內部也就是集羣內部使用。

2. NodePort

 

供集羣外部調用。將 Service 承載在 Node 的靜態端口上,端口號和 Service 一一對應,那麼集羣外的用戶就可以通過 <NodeIP>:<NodePort> 的方式調用到 Service。

3. LoadBalancer

 

給雲廠商的擴展接口。像阿里雲、亞馬遜這樣的雲廠商都是有成熟的 LB 機制的,這些機制可能是由一個很大的集羣實現的,爲了不浪費這種能力,雲廠商可通過這個接口進行擴展。它首先會自動創建 NodePort 和 ClusterIP 這兩種機制,雲廠商可以選擇直接將 LB 掛到這兩種機制上,或者兩種都不用,直接把 Pod 的 RIP 掛到雲廠商的 ELB 的後端也是可以的。

4. ExternalName

 

擯棄內部機制,依賴外部設施,比如某個用戶特別強,他覺得我們提供的都沒什麼用,就是要自己實現,此時一個 Service 會和一個域名一一對應起來,整個負載均衡的工作都是外部實現的。

 

下圖是一個實例。它靈活地應用了 ClusterIP、NodePort 等多種服務方式,又結合了雲廠商的 ELB,變成了一個很靈活、極度伸縮、生產上真正可用的一套系統。

首先我們用 ClusterIP 來做功能 Pod 的服務入口。大家可以看到,如果有三種 Pod 的話,就有三個 Service Cluster IP 作爲它們的服務入口。這些方式都是 Client 端的,如何在 Server 端做一些控制呢?

 

首先會起一些 Ingress 的 Pod(Ingress 是 K8s 後來新增的一種服務,本質上還是一堆同質的 Pod),然後將這些 Pod 組織起來,暴露到一個 NodePort 的 IP,K8s 的工作到此就結束了。

 

任何一個用戶訪問 23456 端口的 Pod 就會訪問到 Ingress 的服務,它的後面有一個 Controller,會把 Service IP 和 Ingress 的後端進行管理,最後會調到 ClusterIP,再調到我們的功能 Pod。前面提到我們去對接雲廠商的 ELB,我們可以讓 ELB 去監聽所有集羣節點上的 23456 端口,只要在 23456 端口上有服務的,就認爲有一個 Ingress 的實例在跑。

 

整個的流量經過外部域名的一個解析跟分流到達了雲廠商的 ELB,ELB 經過負載均衡並通過 NodePort 的方式到達 Ingress,Ingress 再通過 ClusterIP 調用到後臺真正的 Pod。這種系統看起來比較豐富,健壯性也比較好。任何一個環節都不存在單點的問題,任何一個環節也都有管理與反饋。

本文總結

 

本節課的主要內容就到此爲止了,這裏爲大家簡單總結一下:

 

  • 大家要從根本上理解 Kubernetes 網絡模型的演化來歷,理解 PerPodPerIP 的用心在哪裏;

 

  • 網絡的事情萬變不離其宗,按照模型從 4 層向下就是發包過程,反正層層剝離就是收包過程,容器網絡也是如此;

 

  • Ingress 等機制是在更高的層次上(服務<->端口)方便大家部署集羣對外服務,通過一個真正可用的部署實例,希望大家把 Ingress+Cluster IP + PodIP 等概念聯合來看,理解社區出臺新機制、新資源對象的思考。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章