淺談k8s cni 插件

目前不論是個人還是企業,在使用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彼此之間會進行路由的共享和學習,所以自動生成並維護了路由。

一些大廠的容器服務網絡方案

阿里雲

通過上文flannel aliVPC模式可見一斑。阿里雲中kubernetes服務裏,k8s集羣通常使用自定義路由的方案+flannel_cni組件,這個方案易於部署和管理,同時將容器IP和nodeIP區分,用戶可以自定義集羣網絡範圍。

(比較奇怪的是這裏flanenl的backend配置成alloc而非aliVPC,在集羣中另外部署了一個controller進行自定義路由的配置)

自定義路由是vpc網絡中的一個常用功能,在vpc範圍內可以自定義某個網絡接口作爲一個任意網段的網關。在flannel host-gw模式中,我們將這塊的路由配置在node上,由內核執行,而自定義路由則是將類似的路由記錄到vpc網絡的數據庫中,由vpc-router去執行。

azure

azure最近開放了kubernetes服務AKS,AKS支持兩種網絡方案:基礎和高級。

基礎網絡方案與阿里雲的自定義路由方案如出一轍。基礎網絡中k8s集羣使用的網絡組件是kubenet,簡單的做了網絡劃分和本地的網絡接口配置,自定義路由由其vpc實現。

高級網絡方案中,node上的網絡接口會創建並綁定多個(默認三十個)fixedIP,主FixedIP作爲node IP,其餘fixedIP則用於容器IP。
通過azure SDN的支持,不同node之間的容器網絡變成一個大二層,他們可以直接互聯。高級網絡方案中,k8s集羣使用azure開源的cni組件:azure-container-networking。這個cni組件包括了ipam和main兩部分

azure cni的ipam負責將本地網絡接口上綁定着的空閒的fixedIP配置給容器使用。一旦空閒的fixedIP耗盡,除非手動給網卡創建新的fixedIP,否則容器無法創建成功。

azure cni的main組件在node上創建了一個bridge,將node的網卡連接到網橋上,並將node網卡IP設置到網橋上,容器網卡均由vethpair實現,vethpair的node端也是連在網橋上。由此構成node的網絡:網橋上的IP作爲容器網絡的網關,容器網絡通過網橋與其他節點形成一個大二層的網絡。

本文轉自SegmentFault-淺談k8s cni 插件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章