【乾貨分享】Kubernetes容器網絡之CNI漫談

image.png

前言


容器技術的出現,對傳統的應用程序架構、應用開發發佈流程等提供了新的思路,容器技術能將應用程序及其依賴進行打包,能提供跨環境的一致性,擁有良好的可移植性。而Kubernetes的出現,解決了企業中大規模運行容器的管理問題,它能提供容器的生命週期管理、容器編排的能力。但這兩種技術本身不具備完整的容器網絡功能,需要依靠第三方提供容器網絡功能,CNI(Container Network Interface)則爲第三方容器網絡技術與Kubernetes的集成提供了標準。


本文主要通過以下幾個方面介紹下CNI的功能和原理:首先通過介紹CNI接口規範和CNI插件類型使大家簡單瞭解CNI的概念;然後介紹Kubernetes對CNI的調用流程以及CNI插件的開發方式;最後會結合一個開發的案例,來分享一些CNI開發中需要注意到的事項。


CNI簡介

Kubernetes很多容器網絡功能的實現都依賴於單獨的網絡插件,現階段的網絡插件主要有兩類:Kubenet與CNI。其中,Kubenet是一個基礎的、極其簡單的網絡插件,本身並不提供跨主機的容器網絡轉發或網絡策略功能;一般Kubernetes的應用場景中,使用較普遍的是CNI插件。


CNI是一個通用接口的標準,定義了一系列用於連接容器編排系統與網絡插件的規範,CNI插件通過實現CNI規範,來提供對容器網絡的配置功能,CNI插件可以創建管理容器網卡、配置容器DNS、配置容器路由、爲容器分配IP等。CNI最初並不是爲Kubernetes開發的,而是來自於rkt的runtime中,而除了CNI外,由Docker主導的CNM(Container network model)也爲容器網絡提供方的接入提供了標準,但由於包括設計靈活性在內的種種因素,Kubernetes最終選擇了CNI作爲容器網絡的接口規範。

圖片

圖 1 CNI架構


Kubernetes中的Kubelet組件在進行pod生命週期的管理時,會調用CNI插件的接口,爲Pod配置或釋放容器網絡。CNI的調用並不像一般組件,通過HTTP、RPC等方式調用,而是通過執行二進制文件的方式進行調用。


CNI接口規範

爲了豐富、完善CNI插件的功能,CNI的接口規範是不斷的在更新迭代的,最新的版本是0.4.0版本,包括下面4個操作:


1)ADD,用於將容器添加到CNI網絡中。2)DEL,用於將容器從CNI網絡中清除。3)CHECK,用於判斷容器的網絡是否如預期設置的。4)VERSION,用於返回插件自身支持的CNI規範版本。


與上一個0.3.1版本的規範最大的區別在於,新添加了CHECK接口。這是由於在以往的CNI規範中,只有ADD、DEL的接口,缺少GET、LIST之類的狀態檢索接口,這樣一來,Kubernetes在調用ADD與DEL接口後,僅依靠這兩個接口返回的信息,很難準確的獲取到容器網絡現在的狀態。


詳細的操作參數和規範可以參考https://github.com/containernetworking/cni/blob/master/SPEC.md


CNI插件類型

CNI插件根據其實現的功能的不同,分爲4類,社區爲每一類CNI插件都提供了一些標準CNI實現,實現了一些基礎的網絡功能:


1)Main:主要的CNI網絡插件,一般負責網絡設備的創建刪除等,可以單獨使用。例如bridge插件,可以爲容器創建veth pair,並連接到linux bridge上。


2)IPAM:用於管理容器IP資源的CNI插件,一般配合其他插件共同使用。例如host-local插件,可以根據預先設置的IP池範圍、分配要求等,爲容器分配釋放IP資源。


3)Meta:這類插件功能較雜,比如提供端口映射的portmap插件,可以利用iptables將宿主機端口與容器端口進行映射;提供帶寬控制的bandwidth插件,可以利用TC(Traffic Control)對容器的網絡接口進行帶寬的限制。但這類插件需要與Main插件配合使用,無法單獨使用。另外,普遍使用的用於提供完整的容器網絡功能的Flannel網絡插件也屬於這一類,一般會配合bridge插件與host-local插件共同使用。


4)Windows:專門用於Windows平臺的CNI插件。

CNI插件可以通過插件鏈的方式被調用,通過設置CNI的配置文件,可以自由組合各種CNI插件的功能,滿足容器網絡的需求。以提供完整容器網絡解決方案Canal爲例,Canal是容器網絡插件Flannel與Calico通過特定方式組合部署的,Canal具有Calico的網絡策略功能以及Flannel的容器網絡路由功能,官方提供的CNI配置文件如下:

{
       "name": "canal",
       "cniVersion": "0.3.1",
       "plugins": [
           {
               "type": "flannel",
               "delegate": {
                   "type": "calico",
                   "include_default_routes": true,
                   "etcd_endpoints": "__ETCD_ENDPOINTS__",
                   "etcd_key_file": "__ETCD_KEY_FILE__",
                   "etcd_cert_file": "__ETCD_CERT_FILE__",
                   "etcd_ca_cert_file": "__ETCD_CA_CERT_FILE__",
                   "log_level": "info",
                   "policy": {
                       "type": "k8s",
                       "k8s_api_root": "https://__KUBERNETES_SERVICE_HOST__:__KUBERNETES_SERVICE_PORT__",
                       "k8s_auth_token": "__SERVICEACCOUNT_TOKEN__"
                   },
                   "kubernetes": {
                       "kubeconfig": "/etc/cni/net.d/__KUBECONFIG_FILENAME__"
                   }
               }
           },
           {
               "type": "portmap",
               "capabilities": {"portMappings": true},
               "snat": true
           }
       ]
   }





























在plugins字段下包含了使用的插件,其中type字段表示使用的插件類型,可以看到配置文件裏包括了兩個CNI插件:flannel與portmap,兩個插件會通過插件鏈的方式被調用。首先是flannel插件,flannel中的delegate字段表示flannel會將一些容器網絡的配置工作交給calico插件完成,這裏主要是容器的網絡設備的創建與配置,而原始的flannel配置文件中,這部分爲bridge插件的配置;接着是portmap插件,portmap中的capabilities字段用來表示此插件具有的一些特殊功能,Kubernetes如果需要對Pod設置hostport功能,則會在調用CNI插件時,帶上portMappings所需的參數。


Kubernetes對CNI的調用

由於Kubernetes最新的release版本v1.15.1中使用的仍然是CNI 0.3.1規範,因此下面以CNI release 0.6.0版本(對應CNI 0.3.1規範)進行介紹。


在Kubernetes中,要使用CNI插件作爲network plugin時,需要設置Kubelet的--network-plugin、--cni-conf-dir、--cni-bin-dir參數,分別對應:network-plugin的名稱(現階段只有kubenet、cni兩個值可以設置);CNI配置的文件夾;CNI二進制的文件夾。


Kubernete對CNI的調用是通過Kubelet完成的,而kubelet通過CRI(Container Runtime Interface,容器運行時的接口規範)來操作容器,因此CNI的調用最終是由CRI完成的,以內置的一種CRI實現——dockershim爲例,調用流程如下圖。其中需要說明的是,Kubernetes中的Pod是一組容器的集合,而Kubernetes將這一組容器分爲sandbox與container,創建sandbox時,會創建NetworkNamespace,而其他的container,會與sandbox共享這個NetworkNamespace,因此,只有在CRI操作sandbox類型的容器時,纔會調用CNI。


圖片

圖 2 Kubelet對CNI的調用流程


另外,Kubelet不支持多CNI,這裏說的多CNI是指多套CNI網絡方案,而不是多個CNI插件,多個CNI插件可以通過插件鏈的方式進行調用。Kubelet會在--cni-conf-dir指定的目錄下查找後綴名爲.conf、.conflist、.json的文件,按字符順序,選擇第一個有效的CNI配置文件,來進行NetworkPlugin的初始化,因此Kubelet只會將容器加入一個CNI的容器網絡中。


回到上面的圖中,可以看到,最終Kubernetes調用了CNI的AddNetworkList()接口與DelNetWorkList()接口來分別進行容器網絡的創建與刪除,這兩個接口實際上是由CNI庫中的CNIConfig結構實現。理解了這兩個方法,就能理解CNI的調用流程。

func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {}

func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) error {}

首先來看下接口的參數,參數有兩個:一是NetworkConfigList,包含CNI配置文件的內容。爲什麼叫List呢,其實是對應的conflist後綴的CNI配置文件,conflist後綴的配置文件表示的是一組CNI插件的配置,與conf後綴的CNI配置文件相對應,上面介紹的Canal的CNI配置文件就是conflist,包含了2個plugin:flannel與portmap。二是RuntimeConf,是由Kubernetes生成的,提供了容器網絡配置的必要參數以及規則。RuntimeConf結構如下所示:

type RuntimeConf struct {
   ContainerID string
   NetNS       string
   IfName      string
   Args        [][2]string
   // A dictionary of capability-specific data passed by the runtime
   // to plugins as top-level keys in the 'runtimeConfig' dictionary
   // of the plugin's stdin data.  libcni will ensure that only keys
   // in this map which match the capabilities of the plugin are passed
   // to the plugin
   CapabilityArgs map[string]interface{}
}










其中,ContainerID、NetNS分別爲需要配置的容器ID以及容器對應的NetworkNamespace路徑,IfName爲需要創建的容器網絡接口名稱,Args包含一些必要的參數。


而Kubernetes生成的RuntimeConf值如下,需要提到的是,Kubernetes傳遞的IfName始終爲“eth0”,這是由於現階段Kubernetes不會通過AddNetworkList接口返回的Results獲取Pod的IP值,而是通過執行nsenter命令去獲取容器裏eth0網卡的IP,但這種方式限制了Pod多網卡、多CNI插件的場景(根據相關的註釋可以看出,後續Kubernetes會使用AddNetworkList接口返回的IP,只有當返回的Results中IP丟失時,纔會採用nsenter命令去獲取)。在Args方面,kubernetes會將Pod的Name與Pod所在的Namespace作爲參數傳遞,CNI插件可以使用Namespace/Name的組合作爲容器的唯一標識。

 rt := &libcni.RuntimeConf{
      ContainerID: podSandboxID.ID,
      NetNS:       podNetnsPath,
      IfName:      network.DefaultInterfaceName,
      Args: [][2]string{
          {"IgnoreUnknown", "1"},
          {"K8S_POD_NAMESPACE", podNs},
          {"K8S_POD_NAME", podName},
          {"K8S_POD_INFRA_CONTAINER_ID", podSandboxID.ID},
      },
   }









AddNetworkList()方法會順序執行CNI配置文件裏的CNI插件的二進制文件,執行ADD操作,每次執行都會將NetworkConfigList、RuntimeConf以及上一個插件返回的Results,編碼成Json格式,以命令行參數的方式傳遞到CNI插件中。DelNetworkList()與AddNetworkList()類似,不同在於:是逆序執行DEL操作,同時不會傳遞上一個插件返回的Results。


CNI插件開發

CNI插件的開發比較簡單,需要使用到skel包(github.com/containernetworking/cni/pkg/skel),實現如下的兩個接口並註冊即可。從接口的名稱中就可以看出,兩個接口分別對應了CNI規範裏的ADD操作和DEL操作。

func cmdAdd(args *skel.CmdArgs) error {}
func cmdDel(args *skel.CmdArgs) error {}

skel包實現了CNI插件的命令行參數的設置、解析,根據命令行的參數調用註冊的cmdAdd方法與cmdDel方法,其中skel.CmdArgs包含了完整的Json格式的命令行參數。通過skel包,可以很方便的按照CNI規範開發自己的CNI插件。

func main() {
   skel.PluginMain(cmdAdd, cmdDel, version.All)
}
func cmdAdd(args *skel.CmdArgs) error {
//add network
}
func cmdDel(args *skel.CmdArgs) error {
//del network
}








案例:hostport隨機分配

在Kubernetes中,Pod的生命週期都是短暫的,可以隨時刪除後重啓,而每次重啓,Pod的ip地址又會被分配。因此Kubernetes中訪問Pod主要是依賴服務發現機制,Kubernetes提供了Cluster IP、Nodeport、Ingress、DNS等機制,用於將流量轉發到後端的一組Pod中。


除了這類一個地址對應後端多個Pod的訪問方式外,Kubernetes還爲Pod提供了一種一對一的訪問方式,用戶可以爲Pod設置hostport,將Pod的端口映射到宿主機端口。但hostport有如下的缺點:


1)需要手動設定,而且還不能和Nodeport衝突,而Nodeport是支持隨機分配的,這樣就導致手動設定hostport較複雜。


2)一個Deployment的所有Pod都只能設置爲同一個hostport,那麼Pod數量就會受到Kubernetes集羣的節點數量的限制,當Pod數量超過節點數量,如果希望所有Pod都能正常運行,則必定有兩個Pod會調度到同一個節點,出現端口的衝突。


3)不像Nodeport,hostport只能映射到Pod所在宿主機的端口,如果Pod發生遷移,訪問地址需要重新獲取。


因此,我們希望能實現一種hostport方式,能自動分配端口進行映射,同時能夠將完整的訪問地址更新在Pod的annotation中。最終我們選擇使用CNI完成這項工作,而不是將這個邏輯添加在Kubernetes中,主要是考慮到版本升級的影響,選擇了對Kubernetes侵入性最小的方案。由於portmap插件已經實現了端口映射的功能,我們需要做的只有管理、分配映射端。這個功能本身實現起來並不難,但有些設計上的細節可以和大家分享下。


參數如何傳遞


portmap插件需要具體的端口參數進行iptables配置,這些參數其實來自於RuntimeConf,而前面介紹過,參數的傳遞是如下圖所示的,RuntimeConf由kubernetes設置好發送到各個CNI,各個CNI之間只會通過PreResults(即前一個CNI插件的結果)傳遞,因此採用插件鏈的方式是不可行的。


圖片

圖 3 CNI的參數傳遞


我們選擇了在Kubelet與原始的CNI之間添加一層CNI,通過這層CNI插件,可以靈活的控制傳遞的參數,hostport隨機分配的功能就可以在這裏實現。


另外,這層CNI也能解決Kubelet僅使用“第一個有效的CNI配置文件”的問題,因爲後續怎麼調用CNI完全由我們來控制,這也是目前很多的多CNI插件的實現方式。當然,多CNI會更加複雜,裏面還涉及多CNI之間的路由配置衝突等問題(這主要還是由於CNI接口給了各個CNI插件足夠的權限,去完全配置容器的網絡),而我們這裏只需要進行傳遞參數的修改。


圖片

圖 4 hostport隨機分配組件採用的參數傳遞方式


更新Pod的annotation


一般來說,Pod對象的修改會引起Kube-scheduler對pod的重新調度,然後Pod會在新的節點進行Pod的創建、CNI的調用等,但Pod的annotation的更改不會導致重新調度。因此,除非你的CNI插件有特殊的使用場景,否則CNI插件最多隻修改Pod的annotation。比如在hostport隨機分配的CNI中,我們將Pod當前所在的宿主機IP與分配的Hostport,作爲Pod的訪問方式寫入Pod的annotation。


Del接口的健壯性

Kubelet調用CNI的Del接口的場景有多種,比如用戶刪除Pod,Kubernetes GC進行資源釋放,Pod狀態和預期設定的不一致等,爲了使Del接口在這些場景中都能正常運行,需要儘可能的滿足一些要求。


1)需要考慮到短時間內使用相同的參數多次調用Del接口的情況,Del接口要能夠正常運行。一般當Del接口一次調用,需要刪除或更新多種資源時,需要特別注意。比如我們在釋放hostport的時候,需要進行刪除本地的分配記錄、更新用於記錄port資源的位圖等操作,即使在更新位圖的時候發現端口已經被釋放,也會嘗試繼續進行後面分配記錄的刪除等流程。


2)能允許Del空的資源,當需要釋放的資源未找到的時候,可以認爲資源已經進行過釋放了。這個和上面一條說的有些類似,在Kubelet中,如果CNI返回的錯誤中有“no such file or directory”(代碼邏輯如下),會忽略錯誤,但CNI插件最好能自己完成這個邏輯。因此,即使在釋放hostport的過程中,找不到port被分配的情況,接口也會返回釋放成功,只需要最終的狀態符合預期。

  err = cniNet.DelNetworkList(netConf, rt)
   // The pod may not get deleted successfully at the first time.
   // Ignore "no such file or directory" error in case the network has already been deleted in previous attempts.
   if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
      klog.Errorf("Error deleting %s from network %s/%s: %v", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, err)
      return err
   }





3)Del接口不要通過查詢Pod對象來獲取相關參數,需要考慮到執行Del操作時,kube-apiserver中已刪除相應的Pod對象的情況。一般來說,Kubelet會把要釋放的資源傳遞給CNI,比如Pod的IP、hostport端口等,但在我們做的hostport隨機分配插件中,Kubelet是不感知我們分配的端口的,雖然我們在Pod的annotation中有存儲端口,但我們還是需要本地存儲一份Pod與端口的分配記錄,以供Del接口使用。


總結

CNI規範爲CNI插件提供了很大的靈活性,使得Kubernetes與容器網絡的實現解耦,文章介紹了一些基礎的CNI開發,而較複雜的容器網絡方案,除了CNI插件外,一般還需要配合Controller進行資源的同步(比如Kubernetes Networkpolicy的同步),甚至需要開發組件接管Kubernetes的Service網絡,代替Kube-proxy的功能,以實現一個完整的容器網絡實現方案。


End


往期精選

1

【乾貨分享】硬件加速介紹及Cyborg項目代碼分析

2

【乾貨分享】BC-MQ大雲消息隊列高可用設計之談

3

【大雲製造】爲雲而生 - 大雲BEK內核

圖片



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