Kubernetes網路一年發展動態與未來趨勢
應用上雲,怎能沒有容器!點擊上方
容器魔方
關注我本文根據華為工程師DJ在Cloud Native Days成都站上的演講整理:
Kubernetes網路模型與CNI
談到K8S的網路模型,就不能不提它著名的
「單Pod單IP」模型
,即每個Pod都有一個獨立的IP,Pod內所有容器共享網路namespace(同一個網路協議棧和IP)。
「單Pod單IP」網路模型為我們勾勒了一個K8S扁平網路的藍圖,在這個網路世界裡:容器之間直接通信,不需要額外的NAT(網路地址轉換);Node與容器之間,同樣不需要額外的NAT;在其他容器和容器自身看到的IP也一樣的。扁平化網路的優點在於:沒有NAT的性能損耗,可追溯源地址進而為後面的網路策略做鋪墊,易排錯等。
總體而言,如果集群內要訪問Pod,走Service,至於集群外要訪問Pod,走的是Ingress。
Service和Ingress是K8S專門的抽象出來的和服務發現相關的概念,後面會做詳細討論。
類似於
CR
I
之於K8S的runtime,K8S使用CNI(container network interface)作為Pod網路配置的標準介面。
需要注意的是,
CNI並不支持docker網路
,也就是說docker0網橋會被CNI的各類插件「視而不見」。
上圖描繪了當用戶在K8S里創建了一個Pod後,CRI和CNI協同創建所有容器並為他們初始化網路棧的全過程。
具體過程如下:
當用戶在K8S的master那邊創建了一個Pod後,Kubelet觀察到新Pod的創建,於是首先調用CRI(後面的Runtime實現,比如:dockershim,containerd等)創建Pod內的若干個容器。在這些容器裡面,第一個被創建的Pause容器是比較特殊的,這是K8S系統「贈送」的容器,裡面跑著一個功能十分簡單的go語言程序,具體邏輯是一啟動就去select一個空的go語言channel,自然就永遠阻塞在那裡了。一個永遠阻塞而且沒有實際業務邏輯的pause容器到底有什麼用呢?用處大了。我們知道容器的隔離功能利用的是Linux內核的namespace機制,而只要是一個進程,不管這個進程是否處於運行狀態(掛起亦可),它都能「占」著一個namespace。
因此,每個Pod內的第一個系統容器Pause的作用就是為佔用一個Linux的network namespace,而Pod內其他用戶容器通過加入到這個network namespace的方式來共享同一個network namespace。用戶容器和pause容器之間的關係有點類似於寄居蟹和海螺的關係。
因此,Container Runtime創建Pod內所有容器時,調用的都是同一個命令:
$ docker run --net=none
意思是只創建一個network namespace,而不初始化網路協議棧。如果這個時候通過nsenter方式進入到容器,會看到裡面只有一個本地迴環設備lo。那麼容器的eth0是怎麼創建出來的呢?答案是CNI。
CNI主要負責容器的網路設備初始化工作。
Kubelet目前支持兩個網路驅動,分別是:kubenet和CNI。
Kubenet是一個歷史產物,即將廢棄,因此這裡也不準備過多介紹。CNI有多個實現,官方自帶的插件就有p2p,bridge等,這些插件負責初始化pause容器的網路設備,也就是給eth0分配IP等,到時候Pod內其他容器就用這個IP與外界通信。Flanne,calico這些第三方插件解決Pod之間的跨機通信問題。容器之間的跨機通信,可以通過bridge網路或overlay網路來完成。
上圖是一個bridge網路的示意圖。Node1上Pod的網段是10.1.1.0/24,接的Linux網橋是10.1.1.1,Node2上Pod的網段是10.1.2.0/24,接的Linux網橋是10.1.2.1,接在同一個網橋上的Pod通過區域網廣播通信。我們發現,
Node1上的路由表的第二條是:
10.1.1.0/24 dev cni0
意思是所有目的地址是本機上Pod的網路包,都發到cni0這個Linux網橋去,進而廣播給Pod。
注意看第三條路由規則:
10.1.2.0/24 via 192.168.1.101
10.1.2.0/24是Node2上Pod的網段,192.168.1.101又恰好是Node2的IP。意思是,目的地址是10.1.2.0/24的網路包,發到Node2上。
這時候我們觀察Node2上
面的第二條路由信息:
10.1.2.0/24 dev cni0
就會知道這個包會被接著發給Node2上的Linux網橋cni0,然後再廣播給目標Pod。
回程報文同理走一條逆向的路徑。因此,我們可以得出一個小小的結論:bridge網路本身不解決容器的跨機通信問題,需要顯式地書寫主機路由表,映射目標容器網段和主機IP的關係,集群內如果有N個主機,需要N-1條路由表項。
至於overlay網路,它是構建在物理網路之上的一個虛擬網路,其中VXLAN是當前最主流的overlay標準。VXLAN就是用UDP包頭封裝二層幀,即所謂的MAC in UDP。
上圖即一個典型overlay網路的拓撲圖。和bridge網路類似,Pod同樣接在Linux網橋上,目的地址是本機Pod的網路包同樣發給Linux網橋cni0。不一樣的是,目的Pod在其他節點上的路由表規則,例如:
10.1.0.0/16 dev tun0
這次是直接發給本機的TAP設備tun0,而tun0就是overlay隧道網路的入口。我們注意到,集群內所有機器都只需要這麼一條路由表,而不需要像bridge網路那樣,寫N-1條路由表項。那如何才能將網路包正確地傳遞到目標主機的隧道口另一端呢?一般情況下,例如flannel的實現,會藉助一個分散式的資料庫,用於記錄目的容器IP與所在主機的IP的映射關係,而且每個節點上都會運行一個agent,例如flanneld,會監聽在tun0上,進行封包和解包操作。例如:Node1上的容器發包給Node2上的容器,flanneld會在tun0處將一個目的地址是192.168.1.101:8472的UDP包頭(校驗和置成0)封裝到這個包的外層,然後接著主機網路的東風順利到達Node2。監聽在Node2的tun0上的flanneld捕獲這個特殊的UDP包(檢驗和為0),知道這是一個overlay的封包,於是解開UDP包頭,將它發給本機的Linux網橋cni0,進而廣播給目的容器。
接下來我們來了解下什麼是CNI?CNI是Container Network Interface的縮寫,是容器網路的標準化,試圖通過JSON來描述一個容器網路配置。
從上圖可以看出,CNI是K8S與底層網路插件之間的一個抽象層,為K8S屏蔽了底層網路實現的複雜度,同時也解耦了K8S的具體網路插件實現。
CNI主要有兩類介面,
分別是在創建容器時調用的配置網路介面:
和刪除容器時調用的清理網路介面:
不論是配置網路介面還是刪除網路介面,都有兩個入參,分別是網路配置和runtime配置。網路配置很好理解,rumtime配置則主要是容器運行時傳入的網路namespace信息。
符合CNI標準的默認/第三方網路插件有:
其中CNI-Genie是一個開源的多網路的容器解決方案,感興趣的讀者可以自行去Github上搜索。
下面我們將舉幾個CNI網路插件的例子。
上圖是一個
host-local + bridge
插件組合的例子,在這麼一個JSON文件中,我們定義了一個名為mynet的網路,是一個bridge模型,而IP地址管理(ipam)使用的是host-local(在本地用一個文件記錄已經分配的容器IP地址)且可供分配的容器網段是10.10.0.0/16。至於K8S如何使用它們?Kubelet和CNI約好了兩個默認的文件系統路徑,分別是/etc/cni/net.d用來存儲CNI配置文件和/opt/cni/bin目錄用來存放CNI插件的二進位文件,在我們這個例子中,至少要提前放好bridge和host-local這兩個插件的二進位,以及10-mynet.conf配置文件(叫什麼名字隨意,Kubelet只解析*.conf文件)。由於主流的網路插件都集成了bridge插件並提供了各自的ipam功能,因此在實際K8S使用過程中我們並不需要操心上面過程,也無需做額外配置。
再來看一個最近K8S V1.11版本合到社區主幹的帶寬控制插件的使用方法。
當我們書寫了以下Pod配置時:
K8S就會自動為我們這個Pod分別限制上傳和下載的帶寬為最高10Mb/s。注意,由於這個特性較新,我們需要自己在
/etc/cni/net.d
目錄下寫一個配置文件,例如
my-net.conf:
{
"type": "bandwidth",
"capabilities": {"bandwidth": true}
}
這個配置文件會告訴Kubelet去調用CNI的默認bandwidth插件,然後根據Pod annotation裡面關於帶寬的ingress/egress值進行容器上行/下行帶寬的限制。當然,CNI插件最後調用的還是Linux tc工具。
Kubernetes Service機制
容器網路模型講完後,我們再看下K8S集群內訪問的Service機制。先從一個簡單的例子講起,客戶端訪問容器應用,最簡單的方式莫過於直接容器IP+埠了。
但,簡單的生活總是暫時的。
當有多個後端實例,如何做到負載均衡?如何保持會話親和性?容器遷移,IP發生變化如何訪問?健康檢查怎麼做?怎麼通過域名訪問?
K8S提供的解決方案是在客戶端和後端Pod之間引入一個抽象層:Service。什麼是K8S的Service呢?
K8S的Service有時候也稱為K8S的微服務,代表的是K8S後端服務的入口,它注意包含服務的訪問IP(虛IP)和埠,因此工作在L4。既然Service只存儲服務入口信息,那如何關聯後端Pod呢?Service通過Label Selector選擇與之匹配的Pod。
那麼被Service選中的Pod,當它們Running且Ready後,K8S的Endpoints Controller會生成一個新的Endpoints對象,記錄Pod的IP和埠,這就解決了前文提到的後端實例健康檢查問題。另外,Service的訪問IP和Endpoint/Pod IP都會在K8S的DNS伺服器里存儲域名和IP的映射關係,因此用戶可以在集群內通過域名的方式訪問Service和Pod。
K8S Service的定義如下所示:
其中,spec.ClusterIP就是Service的訪問IP,俗稱虛IP,spec.ports[].port是Service的訪問埠,而與之對應的spec.ports[].targetPort是後端Pod的埠,K8S內部會自動做一次映射。
K8S Endpoints的定義如下所示:
其中,subsets[].addresses[].ip是後端Pod的IP,subsets[].ports是後端Pod的埠,與Service的targetPort對應。
下面我們來看下K8S Service的工作原理。
如上圖所示,當用戶創建Service和對應後端Pod時,Endpoints Controller會觀察Pod的狀態變化,當Pod處於Running且Ready狀態時,Endpoints Controller會生成Endpoints對象。運行在每個節點上的Kube-proxy會觀察Service和Endpoints的更新,並調用其load balancer在主機上模塊刷新轉發規則。當前主流的load balancer實現有iptables和IPVS,iptables因為擴展性和性能不好,越來越多的廠商正開始使用IPVS模式。
K8S Service有這麼幾種類型:ClusterIP,NodePort和Load Balancer。其中,ClusterIP是默認類型,自動分配集群內部可以訪問的虛IP——Cluster IP。
NodePort
為Service在Kubernetes集群的每個Node上分配一個埠,即NodePort,集群內/外部可基於任何一個NodeIP:NodePort的形式來訪問Service。因此NodePort也成為「乞丐版」的Load Balancer,對於那些沒有打通容器網路和主機網路的用戶,NodePort成了他們從外部訪問Service的首選。LoadBalancer類型的Service需要Cloud Provider的支持,因為Service Controller會自動為之創建一個外部LB並配置安全組,K8S原生支持的Cloud Provider就那麼幾個:GCE,AWS。除了「外用」,Load Balancer還可以「內服」,即如果要在集群內訪問Load Balancer類型的Service,kube-proxy用iptables或ipvs實現了雲服務提供商LB(一般都是L7的)的部分功能:L4轉發,安全組規則等。
K8S Service創建好了,那麼如何使用,即如何進行服務發現呢?K8S提供了兩種方式:環境變數和域名。
環境變數即Kubelet為每個Pod注入所有Service的環境變數信息,形如:
這種方式的缺點是:
容易環境變數洪泛,docker啟動參數過長影響性能甚至直接導致容器啟動失敗。
域名的方式是,假設Service(my-svc)在namespace(my-ns)中,暴露名為http的TCP埠,那麼在K8S的DNS伺服器會生成兩種記錄,分別是A記錄:域名(my-svc.my-ns)到Cluster IP的映射和SRV記錄,例如:_http._tcp.my-svc.my-ns到一個http埠號的映射。我們會在下文Kube-dns一節做更詳細的介紹。
前文提到,
Service的load balancer模塊有iptables和IPVS實現。
下面會一一進行分析。
Iptables
是用戶空間應用程序,通過配置Netfilter規則表( Xtables )來構建linux內核防火牆。下面就是K8S利用iptables的DNAT模塊,實現了Service入口地址(10.20.30.40:80)到Pod實際地址(172.17.0.2:8080)的轉換。
IPVS是LVS的負載均衡模塊,亦基於netfilter,但比iptables性能更高,具備更好的可擴展性。
如上所示,一旦創建一個Service和Endpoints,
Kube-proxy的IPVS模式會做三樣事情:
1)
確保一塊dummy網卡(kube-ipvs0)存在。至於為什麼要創建dummy網卡,因為IPVS的netfilter鉤子掛載INPUT chain,我們需要把Service的訪問IP綁定在dummy網卡上讓內核「覺得」虛IP就是本機IP,進而進入到INPUT chain。
2)
把Service的訪問IP綁定在dummy網卡上。
3)
通過socket調用,創建IPVS的virtual server和real server,分別對應K8S的Service和Endpoints。
好了,
都說IPVS性能要好於iptables,無圖無真相,上實測數據!
通過上圖我們可以發現,
IPVS刷新規則的時延明顯要低iptables幾個數量級
。
從上圖我們又可以發現,
IPVS相較於iptables,端到端的吞吐率和平均時延均由不小的優化。
注意,這是端到端的數據,包含了底層容器網路的RTT,還能有30%左右的性能提升。
上圖是
iptables和IPVS在資源消耗方面的對比
,孰優孰劣,不言而喻。
最後,問個開放性的問題。如何從集群外訪問Kubernetes Service?
前文已經提到,可以使用NodePort類型的Service,但這種「屌絲」的做法除了要求集群內Node有對外訪問IP外,還有一些已知的性能問題,
具體請參考本公眾號另外一篇乾貨文章
《記一次Docker/Kubernetes上無法解釋的超時原因探尋之旅》
。使用LoadBalancer類型的Service?它又要求在特定的雲服務上跑K8S。而且Service只提供L4負載均衡功能,而沒有L7功能,一些高級的,L7的轉發功能,比如:基於HTTP essay-header,cookie,URL的轉發就做不了。
在K8S中,L7的轉發功能,集群外訪問Service,這些功能是專門交給Ingress的。
在後續的文章中,我們會重點介紹
K8S的Ingress,DNS,Network Policy以及K8S網路未來
的工作,敬請關注!
推薦閱讀
深度剖析Kubernetes API Server三部曲 - part 3
Jenkins搭載k8s實現高可用
點擊
閱讀原文
,立即報名
推薦閱讀:
※中國的未來不能操縱於各家激進派手中丨對話許紀霖
※八字看你未來適合什麼樣的配偶
※行家乾貨!從《邦瀚斯奉文堂專場》看高古瓷未來發展趨勢
※警惕!這種「囚徒思維」正在謀殺你的未來!