前言


容器技术的出现,对传统的应用程序架构、应用开发发布流程等提供了新的思路,容器技术能将应用程序及其依赖进行打包,能提供跨环境的一致性,拥有良好的可移植性。而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内核


©著作权归作者所有:来自51CTO博客作者mb5fed6f06e367a的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 【我的Linux,我做主!】Docker容器技术基础入门
  2. java实现多行html文本转换成单行文本,并用QQ小程序towxml插件转换
  3. MySQL 5.6密码强度审计插件使用说明
  4. docker基本操作
  5. 10 个用纯 Javascript 实现的好用插件
  6. CSS进阶知识flex弹性布局容器与项目
  7. LayoutInflater(布局服务)
  8. Unity Android(安卓)打开相册和摄像头
  9. 携程Android(安卓)App插件化和动态加载实践

随机推荐

  1. 灵活调用xsl来解析xml文档(js异步)
  2. FireFox对XML的处理兼容IE的节点处理方法
  3. 读写xml所有节点个人小结 和 读取xml节点
  4. 基于关系型数据库引擎的"XML"索引技术
  5. XML 增、删、改和查示例
  6. 效率最高的xml解析方式
  7. Python 解析 xml[xml.dom]
  8. Linux下将Tinyxml编译为静态库
  9. AJAX之xmlHttp
  10. XML与Web服务和SOA有何关联?