更新時間:2020-04-04 來源:黑馬程序員 瀏覽量:
容器網絡發端于 Docker 的網絡。Docker 使用了一個比較簡單的網絡模型,即內部的網橋加內部的保留 IP。這種設計的好處在于容器的網絡和外部世界是解耦的,無需占用宿主機的 IP 或者宿主機的資源,完全是虛擬的。
它的設計初衷是:當需要訪問外部世界時,會采用 SNAT 這種方法來借用 Node 的 IP 去訪問外面的服務。比如容器需要對外提供服務的時候,所用的是 DNAT 技術,也就是在 Node 上開一個端口,然后通過 iptable 或者別的某些機制,把流導入到容器的進程上以達到目的。
該模型的問題在于,外部網絡無法區分哪些是容器的網絡與流量、哪些是宿主機的網絡與流量。比如,如果要做一個高可用的時候,172.16.1.1 和 172.16.1.2 是擁有同樣功能的兩個容器,此時我們需要將兩者綁成一個 Group 對外提供服務,而這個時候我們發現從外部看來兩者沒有相同之處,它們的 IP 都是借用宿主機的端口,因此很難將兩者歸攏到一起。推薦了解傳智播客linux云計算+運維開發課程。
在此基礎上,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 一一對應,那么集群外的用戶就可以通過 : 的方式調用到 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 等概念聯合來看,理解社區出臺新機制、新資源對象的思考。
猜你喜歡: