淺談K8S cni和網路方案
此文已由作者黃揚授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
在早先的k8s版本中,kubelet程式碼裡提供了networkPlugin,networkPlugin是一組介面,實現了pod的網路配置、解除、獲取,當時kubelet的程式碼中有個一個docker_manager,負責容器的建立和銷燬,亦會負責容器網路的操作。而如今我們可以看到基本上kubelet的啟動引數中,networkPlugin的值都會設定為cni。
cni外掛的使用方式
使用CNI外掛時,需要做三個配置:
kubelet啟動引數中networkPlugin設定為cni
在/etc/cni/net.d中增加cni的配置檔案,配置檔案中可以指定需要使用的cni元件及引數
將需要用到的cni元件(二進位制可執行檔案)放到/opt/cni/bin目錄下
所有的cni元件都支援兩個命令:add和del。即配置網路和解除網路配置。
cni外掛的配置檔案是一個json檔案,不同版本的介面、以及不同的cni元件,有著不同的配置內容結構,目前比較通用的介面版本是0.3.1的版本。
在配置檔案中我們可以填入多個cni元件,當這些cni元件的配置以陣列形式記錄時,kubelet會對所有的元件進行按序鏈式呼叫,所有元件呼叫成功後,視為網路配置完成,過程中任何一步出現error,都會進行回滾的del操作。以保證操作流上的原子性。
幾種基本的cni外掛
cni外掛按照程式碼中的存放目錄可以分為三種:ipam、main、meta。
ipam cni用於管理ip和相關網路資料,配置網絡卡、ip、路由等。
main cni用於進行網路配置,比如建立網橋,vethpair、macvlan等。
meta cni有的是用於和第三方CNI外掛進行適配,如flannel,也有的用於配置核心引數,如tuning
由於官方提供的cni元件就有很多,這裡我們詳細介紹一些使用率較高的元件。
ipam類CNI
ipam型別的cni外掛,在執行add命令時會分配一個IP給呼叫者。執行del命令時會將呼叫者指定的ip放回ip池。社群開源的ipam有host-local、dhcp。
host-local
我們可以通過host-local的配置檔案的資料結構來搞懂這個元件是如何管理ip的。
type IPAMConfig struct { *Range Name string Type string `json:"type"` Routes []*types.Route `json:"routes"`//交付的ip對應的路由 DataDir string `json:"dataDir"`//本地ip池的資料庫目錄 ResolvConf string `json:"resolvConf"`//交付的ip對應的dns Ranges []RangeSet `json:"ranges"`//交付的ip所屬的網段,閘道器資訊 IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args } #配置檔案範例: { "cniVersion": "0.3.1", "name": "mynet", "type": "ipvlan", "master": "foo0", "ipam": { "type": "host-local", "resolvConf": "/home/here.resolv", "dataDir": "/home/cni/network", "ranges": [ [ { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.9", "rangeEnd": "10.1.2.20", "gateway": "10.1.2.30" }, { "subnet": "10.1.4.0/24" } ], [{ "subnet": "11.1.2.0/24", "rangeStart": "11.1.2.9", "rangeEnd": "11.1.2.20", "gateway": "11.1.2.30" }] ] } }
從上面的配置我們可以清楚:
host-local元件通過在配置檔案中指定的subnet進行網路劃分
host-local在本地通過指定目錄(預設為/var/lib/cni/networks)記錄當前的ip pool資料
host-local將IP分配並告知呼叫者時,還可以告知dns、路由等配置資訊。這些資訊通過配置檔案和對應的resolv檔案記錄。
host-local的應用範圍比較廣,kubenet、bridge、ptp、ipvlan等cni network外掛都被用來和host-local配合進行ip管理。
dhcp
社群的cni元件中就包含了dhcp這個ipam,但並沒有提供一個可以參考的案例,翻看了相關的原始碼,大致邏輯是:
向dhcp申請ip時,dhcp會使用rpc訪問本地的socket(/run/cni/dhcp.sock)申請一個ip的租約。然後將IP告知呼叫者。
向dhcp刪除IP時,dhcp同樣通過rpc請求,解除該IP的租約。
main(network)類CNI
main型別的cni元件做的都是一些核心功能,比如配置網橋、配置各種虛擬化的網路介面(veth、macvlan、ipvlan等)。這裡我們著重講使用率較高的bridge和ptp。
bridge
brige模式,即網橋模式。在node上建立一個linux bridge,並通過vethpair的方式在容器中設定網絡卡和IP。只要為容器配置一個二層可達的閘道器:比如給網橋配置IP,並設定為容器ip的閘道器。容器的網路就能建立起來。
如下是bridge的配置項資料結構:
type NetConf struct { types.NetConf BrName string `json:"bridge"` //網橋名 IsGW bool `json:"isGateway"` //是否將網橋配置為閘道器 IsDefaultGW bool `json:"isDefaultGateway"` // ForceAddress bool `json:"forceAddress"`//如果網橋已存在且已配置了其他IP,通過此引數決定是否將其他ip除去 IPMasq bool `json:"ipMasq"`//如果true,配置私有網段到外部網段的masquerade規則 MTU int `json:"mtu"` HairpinMode bool `json:"hairpinMode"` PromiscMode bool `json:"promiscMode"` }
我們關注其中的一部分欄位,結合程式碼可以大致整理出bridge元件的工作內容。首先是ADD命令:
執行ADD命令時,brdige元件建立一個指定名字的網橋,如果網橋已經存在,就使用已有的網橋;
建立vethpair,將node端的veth裝置連線到網橋上;
從ipam獲取一個給容器使用的ip資料,並根據返回的資料計算出容器對應的閘道器;
進入容器網路名字空間,修改容器中網絡卡名和網絡卡ip,以及配置路由,並進行arp廣播(注意我們只為vethpair的容器端配置ip,node端是沒有ip的);
如果IsGW=true,將網橋配置為閘道器,具體方法是:將第三步計算得到的閘道器IP配置到網橋上,同時根據需要將網橋上其他ip刪除。最後開啟網橋的ip_forward核心引數;
如果IPMasq=true,使用iptables增加容器私有網網段到外部網段的masquerade規則,這樣容器內部訪問外部網路時會進行snat,在很多情況下配置了這條路由後容器內部才能訪問外網。(這裡程式碼中會做exist檢查,防止生成重複的iptables規則);
配置結束,整理當前網橋的資訊,並返回給呼叫者。
其次是DEL命令:
根據命令執行的引數,確認要刪除的容器ip,呼叫ipam的del命令,將IP還回IP pool;
進入容器的網路名字空間,根據容器IP將對應的網絡卡刪除;
如果IPMasq=true,在node上刪除建立網路時配置的幾條iptables規則。
ptp
ptp其實是bridge的簡化版。但是它做的網路配置其實看上去倒是更復雜了點。並且有一些配置在自測過程中發現並沒有太大用處。它只建立vethpair,但是會同時給容器端和node端都配置一個ip。容器端配置的是容器IP,node端配置的是容器IP的閘道器(/32),同時,容器裡做了一些特殊配置的路由,以滿足讓容器發出的arp請求能被vethpair的node端響應。實現內外的二層連通。
ptp的網路配置步驟如下:
從ipam獲取IP,根據ip型別(ipv4或ipv6)配置響應的核心ip_forward引數;
建立一對vethpair;一端放到容器中;
進入容器的網路namespace,配置容器端的網絡卡,修改網絡卡名,配置IP,並配置一些路由。假如容器ip是10.18.192.37/20,所屬網段是10.18.192.0/20,閘道器是10.18.192.1,我們這裡將進行這樣的配置:
配置IP後,核心會自動生成一條路由,形如:
10.18.192.0/20 dev eth0 scope link
,我們將它刪掉:ip r d ****
配置一條私有網到閘道器的真實路由:
ip r a 10.18.192.0/20 via 10.18.192.1 dev eth0
配置一條到閘道器的路由:
10.18.192.1/32 dev eth0 scope link
退出到容器外,將vethpair的node端配置一個IP(ip為容器ip的閘道器,mask=32);
配置外部的路由:訪問容器ip的請求都路由到vethpair的node端裝置去。
如果IPMasq=true,配置iptables
獲取完整的網絡卡資訊(vethpair的兩端),返回給呼叫者。
與bridge不同主要的不同是:ptp不使用網橋,而是直接使用vethpair+路由配置,這個地方其實有很多其他的路由配置可以選擇,一樣可以實現網路的連通性,ptp配置的方式只是其中之一。萬變不離其宗的是:
只要容器內網絡卡發出的arp請求,能被node回覆或被node轉發並由更上層的裝置回覆,形成一個二層網路,容器裡的資料報文就能被髮往node上;然後通過node上的路由,進行三層轉發,將資料報文發到正確的地方,就可以實現網路的互聯。
bridge和ptp其實是用了不同方式實現了這個原則中的“二層網路”:
bridge元件給網橋配置了閘道器的IP,並給容器配置了到閘道器的路由。實現二層網路
ptp元件給vethpair的對端配置了閘道器的IP,並給容器配置了單獨到閘道器IP的路由,實現二層網路
ptp模式的路由還存在一個問題:沒有配置default路由,因此容器不能訪問外部網路,要實現也很簡單,以上面的例子,在容器裡增加一條路由:default via 10.18.192.1 dev eth0
host-device
相比前面兩種cni main元件,host-device顯得十分簡單因為他就只會做兩件事情:
收到ADD命令時,host-device根據命令引數,將網絡卡移入到指定的網路namespace(即容器中)。
收到DEL命令時,host-device根據命令引數,將網絡卡從指定的網路namespace移出到root namespace。
細心的你肯定會注意到,在bridge和ptp元件中,就已經有“將vethpair的一端移入到容器的網路namespace”的操作。那這個host-device不是多此一舉嗎?
並不是。host-device元件有其特定的使用場景。假設叢集中的每個node上有多個網絡卡,其中一個網絡卡配置了node的IP。而其他網絡卡都是屬於一個網路的,可以用來做容器的網路,我們只需要使用host-device,將其他網絡卡中的某一個丟到容器裡面就行。
host-device模式的使用場景並不多。它的好處是:bridge、ptp等方案中,node上所有容器的網路報文都是通過node上的一塊網絡卡出入的,host-device方案中每個容器獨佔一個網絡卡,網路流量不會經過node的網路協議棧,隔離性更強。缺點是:在node上配置數十個網絡卡,可能並不好管理;另外由於不經過node上的協議棧,所以kube-proxy直接廢掉。k8s叢集內的負載均衡只能另尋他法了。
macvlan
有關macvlan的實踐可以參考這篇文章。這裡做一個簡單的介紹:macvlan是linux kernal的特性,用於給一個物理網路介面(parent)配置虛擬化介面,虛擬化介面與parent網路介面擁有不同的mac地址,但parent介面上收到發給其對應的虛擬化介面的mac的包時,會分發給對應的虛擬化介面,有點像是將虛擬化介面和parent介面進行了'橋接'。給虛擬化網路介面配置了IP和路由後就能互相訪問。
macvlan省去了linux bridge,但是配置macvlan後,容器不能訪問parent介面的IP。
ipvlan
ipvlan與macvlan有點類似,但對於核心要求更高(3.19),ipvlan也會從一個網路介面創建出多個虛擬網路介面,但他們的mac地址是一樣的, 只是IP不一樣。通過路由可以實現不同虛擬網路介面之間的互聯。
使用ipvlan也不需要linux bridge,但容器一樣不能訪問parent介面的IP。 關於ipvlan的內容可以參考這篇文章
關於macvlan和ipvlan,還可以參考這篇文章
meta 類CNI
meta元件通常進行一些額外的網路配置(tuning),或者二次呼叫(flannel)。
tuning
用於進行核心網路引數的配置。並將呼叫者的資料和配置後的核心引數返回給呼叫者。
有時候我們需要配置一些虛擬網路介面的核心引數,比如:網易雲在早期經典網路方案中曾修改vethpair的proxy_arp引數(後面會介紹)。可以通過這個元件進行配置。 另外一些可能會改動的網路引數比如:
accept_redirects
send_redirects
proxy_delay
accept_local
arp_filter
可以在這裡檢視可配置的網路引數和釋義。
portmap
用於在node上配置iptables規則,進行SNAT,DNAT和埠轉發。
portmap元件通常在main元件執行完畢後執行,因為它的執行引數仰賴之前的元件提供
flannel
cni plugins中的flannel是開源網路方案flannel的“呼叫器”。這也是flannel網路方案適配CNI架構的一個產物。為了便於區分,以下我們稱cni plugins中的flannel 為flanenl cni
。
我們知道flannel是一個容器的網路方案,通常使用flannel時,node上會執行一個daemon程序:flanneld,這個程序會返回該node上的flannel網路、subnet,MTU等資訊。並儲存到本地檔案中。
如果對flannel網路方案有一定的瞭解,會知道他在做網路介面配置時,其實幹的事情和bridge元件差不多。只不過flannel網路下的bridge會跟flannel0網絡卡互聯,而flannel0網絡卡上的資料會被封包(udp、vxlan下)或直接轉發(host-gw)。
而flannel cni
做的事情就是:
執行ADD命令時,
flannel cni
會從本地檔案中讀取到flanneld的配置。然後根據命令的引數和檔案的配置,生成一個新的cni配置檔案(儲存在本地,檔名包含容器id以作區分)。新的cni配置檔案中會使用其他cni元件,並注入相關的配置資訊。之後,flannel cni
根據這個新的cni配置檔案執行ADD命令。執行DEL命令時,
flannel cni
從本地根據容器id找到之前建立的cni配置檔案,根據該配置檔案執行DEL命令。
也就是說flannel cni
此處是一個flannel網路模型的委託者,falnnel網路模型委託它去呼叫其他cni元件,進行網路配置。通常呼叫的是bridge和host-local。
幾種常見的網路方案
上述所有的cni元件,能完成的事情就是建立容器到虛擬機器上的網路。而要實現跨虛擬機器的容器之間的網路,有幾種可能的辦法:
容器的IP就是二層網路裡分配的IP,這樣容器相當於二層網路裡的節點,那麼就可以天然互訪;
容器的IP與node的IP不屬於同一個網段,node上配置個到各個網段的路由(指向對應容器網段所部屬的node IP),通過路由實現互訪[flannel host-gw, calico bgp均是通過此方案實現];
容器的IP與node的IP不屬於同一個網段,node上有服務對容器發出的包進行封裝,對發給容器的包進行解封。封裝後的包通過node所在的網路進行傳輸。解封后的包通過網橋或路由直接發給容器,即overlay網路。[flannel udp/vxlan,calico ipip,openshift-sdn均通過此方案實現]
kubenet
瞭解常用的網路方案前,我們先了解一下kubenet,kubenet其實是k8s程式碼中內建的一個cni元件。如果我們要使用kubenet,就得在kubelet的啟動引數中指定networkPlugin
值為kubenet
而不是cni
。
如果你閱讀了kubernetes的原始碼,你就可以在一個名為kubenet_linux.go的檔案中看到kubenet做了什麼事情:
身為一種networkPlugin,kubenet自然要實現networkPlugin的一些介面。比如SetUpPod,TearDownPod,GetPodNetworkStatus等等,kubelet通過這些介面進行容器網路的建立、解除、查詢。
身為一個程式碼中內建的cni,kubenet要主動生成一個cni配置檔案(位元組流資料),自己按照cni的規矩去讀取配置檔案,做類似ADD/DEL指令的工作。實現網路的建立、解除。
設計上其實挺蠢萌的。實際上是為了省事。我們可以看下自生成的配置檔案:
{ "cniVersion": "0.1.0", "name": "kubenet", "type": "bridge", "bridge": "%s", //通常這裡預設是“cbr0” "mtu": %d, //kubelet的啟動引數中可以配置,預設使用機器上的最小mtu "addIf": "%s", //配置到容器中的網絡卡名字 "isGateway": true, "ipMasq": false, "hairpinMode": %t, "ipam": { "type": "host-local", "subnet": "%s", //node上容器ip所屬子網,通常是kubelet的pod-cidr引數指定 "gateway": "%s", //通過subnet可以確定gateway "routes": [ { "dst": "0.0.0.0/0" } ] } }
配置檔案中明確了要使用的其他cni元件:bridge、host-local(這裡程式碼中還會呼叫lo元件,通常lo元件會被k8s程式碼直接呼叫,所以不需要寫到cni配置檔案中)。之後的事情就是執行二進位制而已。
為什麼我們要學習kubenet?因為kubenet可以讓使用者以最簡單的成本(配置networkPlugin和pod-cidr兩個啟動kubelet啟動引數),配置出一個簡單的、虛擬機器本地的容器網路。結合上面提到的幾種“跨虛擬機器的容器之間的網路方案”,就是一個完整的k8s叢集網路方案了。
通常kubenet不適合用於overlay網路方案,因為overlay網路方案定製化要求會比較高。
許多企業使用vpc網路時,使用自定義路由實現不同pod-cidr之間的路由,他們的網路方案裡就會用到kubenet,比如azure AKS(基礎網路)。
flannel
關於flannel,上面的文章也提到了一下。網上flannel的文章也是一搜一大把。這裡簡單介紹下flannel對k8s的支援,以及通用的幾個flannel backend(後端網路配置方案)。
flannel for kubernetes
flannel在對kubernets進行支援時,flanneld啟動引數中會增加--kube-subnet-mgr
引數,flanneld會初始化一個kubernetes client,獲取本地node的pod-cidr,這個pod-cidr將會作為flannel為node本地容器規劃的ip網段。記錄到/run/flannel/subnet.env。(flannel_cni元件會讀取這個檔案並寫入到net-conf.json中,供cni使用)。
udp/vxlan
flannel的overlay方案。每個node節點上都有一個flanneld程序,和flannel0網橋,容器網路會與flannel0網橋互聯,並經由flannel0發出,所以flanneld可以捕獲到容器發出的報文,進行封裝。udp方案下會給報文包裝一個udp的頭部,vxlan下會給報文包裝一個vxlan協議的頭部(配置了相同VNI的node,就能進行互聯)。 目前flannel社群還提供了更多實驗性的封裝協議選擇,比如ipip,但仍舊將vxlan作為預設的backend。
host-gw
flannel的三層路由方案。每個node節點上都會記錄其他節點容器ip段的路由,通過路由,node A上的容器發給node B上的容器的資料,就能在node A上進行轉發。
alloc
類似kubenet,只分配子網,不做其他任何事情。
支援雲廠商的vpc
flannel支援了aliVPC、gce、aws等雲廠商的vpc網路。原理都是一樣的,就是當flanneld在某雲廠商的機器上執行時,根據機器自身的vpc網路IP,和flanneld分配在該機器上的subnet,呼叫雲廠商的api建立對應的自定義路由。
calico
calico是基於BGP路由實現的容器叢集網路方案,對於使用者來說,基礎的calico使用體驗可能和flannel host-gw是基本一樣的:node節點上做好對容器arp的響應。然後通過node上的路由將容器發出的包轉發到對端容器所在node的IP。對端節點上再將包轉發給對端容器。
ipip模式則如同flannel ipip模式。對報文封裝一個ipip頭部,頭部中使用node ip。傳送到對端容器所在node的IP,對端的網路元件再解包,並轉發給容器。
不同之處在於flannel方案下路由都是通過程式碼邏輯進行配置。而calico則在每個節點建立bgp peer,bgp peer彼此之間會進行路由的共享和學習,所以自動生成並維護了路由。
更多網易技術、產品、運營經驗分享請點選。
相關文章:
【推薦】 JVM鎖實現探究2:synchronized深探
【推薦】 Flask寫web時cookie的處理
【推薦】 MemcachedHash演算法