CNI 简介

CNI (Container Network Interface) 定义了一组用于实现容器网络接口的配置以及 IP 地址的分配的规范。CNI 只关注容器的网络连接以及当容器删除时移除被分配的网络资源,因此 CNI 得到了广泛的支持,并且规范也易于实现。
CNI - 图1
随着kuberntes飞速发展,如今涌现出很多优秀的开源 CNI,如calico,flannel,cilium。每个 CNI 都有各自的特点以及应用场景,但是也有各自的不足之处。因此,针对不同的场景使用合适的 CNI 就变得尤为重要。有时为了满足更复杂的网络需求,甚至于需要多种 CNI 联合使用,这样会使网络模型变得更为复杂,大大增加维护难度。

CNI插件的实现模式

通常来说,CNI 插件可以分为三种:Overlay、路由及 Underlay。
image.png

  • Overlay 模式的典型特征是容器独⽴于主机的 IP 段,这个 IP 段进⾏跨主机⽹络通信时是通过在主机之间创建隧道的⽅式,将整个容器⽹段的包全都封装成底层的物理⽹络中主机之间的包。该⽅式的好处在于它不依赖于底层⽹络。
  • 路由模式中主机和容器也分属不同的⽹段,它与 Overlay 模式的主要区别在于它的跨主机通信是通过路由打通,⽆需在不同主机之间做⼀个隧道封包。但路由打通就需要部分依赖于底层⽹络,⽐如说要求底层⽹络有⼆层可达的⼀个能⼒。
  • Underlay 模式中容器和宿主机位于同⼀层⽹络,两者拥有相同的地位。容器之间⽹络的打通主要依靠于底层⽹络。因此该模式是强依赖于底层能⼒的。

    k8s与CNI

    CNI的全称是 Container Network Interface,即容器⽹络的 API 接⼝。它是 K8s 中标准的⼀个调⽤⽹络实现的接⼝。
    Kubelet 通过这个标准的 API 来调⽤不同的⽹络插件以实现不同的⽹络配置⽅式。实现了这个接⼝的就是CNI 插件,它实现了⼀系列的 CNI API 接⼝。常见的 CNI 插件包括 Calico、flannel、Terway、Weave Net 以及
    Contiv。

    Kubernetes 中如何使⽤ CNI

    基本的使⽤⽅法为:
  1. ⾸先在每个结点上配置 CNI 配置⽂件(/etc/cni/net.d/xxnet.conf),其中 xxnet.conf 是某⼀个⽹络配置⽂件的名称;
  2. 安装 CNI 配置⽂件中所对应的⼆进制插件;
  3. 在这个节点上创建 Pod 之后,Kubelet 就会根据 CNI 配置⽂件执⾏前两步所安装的 CNI 插件;
  4. 上步执⾏完之后,Pod 的⽹络就配置完成了。

具体的流程如下图所⽰:
image.png
在集群⾥⾯创建⼀个 Pod 的时候,⾸先会通过 apiserver 将 Pod 的配置写⼊。apiserver 的⼀些管控组件(⽐如 Scheduler)会调度到某个具体的节点上去。Kubelet 监听到这个 Pod 的创建之后,会在本地进⾏⼀些创建的操作。当执⾏到创建⽹络这⼀步骤时,⾸先它会读取刚才我们所说的配置⽬录中的配置⽂件,配置⽂件⾥⾯会声明所使⽤的是哪⼀个插件,然后去执⾏具体的 CNI 插件的⼆进制⽂件,再由CNI 插件进⼊ Pod 的⽹络空间去配置 Pod 的⽹络。配置完成之后,Kuberlet 也就完成了整个 Pod 的创建过程,这个 Pod 就在线了。
⼤家可能会觉得上述流程有很多步(⽐如要对 CNI 配置⽂件进⾏配置、安装⼆进制插件等等),看起来⽐较复杂。但如果我们只是作为⼀个⽤户去使⽤ CNI 插件的话就⽐较简单,因为很多 CNI 插件都已提供了⼀键安装的能⼒。以我们常⽤的 Flannel 为例,如下图所⽰:只需要我们使⽤ kubectl apply Flannel 的⼀个 Deploying 模板,它就能⾃动地将配置、⼆进制⽂件安装到每⼀个节点上去。安装完之后,整个集群的 CNI 插件就安装完成了。
因此,如果我们只是去使⽤ CNI 插件的话,那么其实很多 CNI 插件已经提供了⼀键安装的脚本,⽆需⼤家关⼼ Kubernetes 内部是如何配置的以及如何调⽤ API 的。

哪个 CNI 插件适合

社区有很多的 CNI 插件,⽐如 Calico、flannel、Terway 等等。那么在⼀个真正具体的⽣产环境中,我们要选择哪⼀个 CNI 插件呢?
我们需要根据不同的场景选择不同的实现模式,再去选择对应的具体某⼀个插件。了解了CNI的三种常⽤的实现模式之后,再根据⾃⼰的环境、需求判断可由哪⼀种模式进⾏实现,再在对应的模式中去找 CNI 插件。不过社区中有那么多插件,它们⼜都属于哪种模式?如何进⾏选择呢?怎么挑选适合⾃⼰的呢?我们可以从以下 3 个⽅⾯来考虑。
image.png

环境限制

不同环境中所⽀持的底层能⼒是不同的。

  • 虚拟化环境(例如 OpenStack)中的⽹络限制较多,⽐如不允许机器之间直接通过⼆层协议访问,必须要带有 IP 地址这种三层的才能去做转发,限制某⼀个机器只能使⽤某些 IP 等。在这种被做了强限制的底层⽹络中,只能去选择 Overlay 的插件,常见的有 Flannel-vxlan、Calico-ipip、Weave 等等。
  • 物理机环境中底层⽹络的限制较少,⽐如说我们在同⼀个交换机下⾯直接做⼀个⼆层的通信。对于这种集群环境,我们可以选择Underlay 或者路由模式的插件。Underlay 意味着我们可以直接在⼀个物理机上插多个⽹卡或者是在⼀些⽹卡上做硬件虚拟化;路由模式就是依赖于 Linux 的路由协议做⼀个打通。这样就避免了像 vxlan 的封包⽅式导致的性能降低。这种环境下我们可选的插件包括clico-bgp、flannel-hostgw、sriov 等等。
  • 公有云环境也是虚拟化,因此底层限制也会较多。但每个公有云都会考虑适配容器,提升容器的性能,因此每家公有云可能都提供了⼀些 API 去配置⼀些额外的⽹卡或者路由这种能⼒。在公有云上,我们要尽量选择公有云⼚商提供的 CNI 插件以达到兼容性和性能上的最优。⽐如 Aliyun 就提供了⼀个⾼性能的 Terway 插件。

环境限制考虑完之后,我们⼼中应该都有⼀些选择了,知道哪些能⽤、哪些不能⽤。在这个基础上,我们再去考虑功能上的需求。

功能需求

  • ⾸先是安全需求:
    • K8s ⽀持 NetworkPolicy,就是说我们可以通过 NetworkPolicy 的⼀些规则去⽀持“Pod 之间是否可以访问”这类策略。但不是每个CNI 插件都⽀持 NetworkPolicy 的声明,如果⼤家有这个需求,可以选择⽀持 NetworkPolicy 的⼀些插件,⽐如 Calico、Weave 等等。
  • 第⼆个是是否需要集群外的资源与集群内的资源互联互通:
    • ⼤家的应⽤最初都是在虚拟机或者物理机上,容器化之后,应⽤⽆法⼀下就完成迁移,因此就需要传统的虚拟机或者物理机能跟容器的 IP地址互通。为了实现这种互通,就需要两者之间有⼀些打通的⽅式或者直接位于同⼀层。此时可以选择 Underlay 的⽹络,⽐如 sriov 这种就是 Pod 和以前的虚拟机或者物理机在同⼀层。我们也可以使⽤ calico-bgp,此时它们虽然不在同⼀⽹段,但可以通过它去跟原有的路由器做⼀些 BGP 路由的⼀个发布,这样也可以打通虚拟机与容器。
  • 最后考虑的就是 K8s 的服务发现与负载均衡的能⼒。
    • K8s 的服务发现与负载均衡就是我们前⾯所介绍的 K8s 的 Service,但并不是所有的 CNI 插件都能实现这两种能⼒。⽐如很多 Underlay模式的插件,在 Pod 中的⽹卡是直接⽤的 Underlay 的硬件,或者通过硬件虚拟化插到容器中的,这个时候它的流量⽆法⾛到宿主机所在的命名空间,因此也⽆法应⽤ kube-proxy 在宿主机配置的规则。种情况下,插件就⽆法访问到 K8s 的服务发现。因此⼤家如果需要服务发现与负载均衡,在选择 Underlay 的插件时就需要注意它们是否⽀持这两种能⼒。

经过功能需求的过滤之后,能选的插件就很少了。经过环境限制和功能需求的过滤之后,如果还剩下 3、4 种插件,可以再来考虑性能需求。

性能需求

我们可以从 Pod 的创建速度和 Pod 的⽹络性能来衡量不同插件的性能。

  • Pod 的创建速度
    • 当我们创建⼀组 Pod 时,⽐如业务⾼峰来了,需要紧急扩容,这时⽐如说我们扩容了 1000 个 Pod,就需要 CNI 插件创建并配置 1000 个⽹络资源。Overlay 和路由模式在这种情况下的创建速度是很快的,因为它是在机器⾥⾯⼜做了虚拟化,所以只需要调⽤内核接⼝就可以完成这些操作。但对于 Underlay 模式,由于需要创建⼀些底层的⽹络资源,所以整个 Pod 的创建速度相对会慢⼀些。因此对于经常需要紧急扩容或者创建⼤批量的 Pod 这些场景,我们应该尽量选择 Overlay 或者路由模式的⽹络插件。
  • Pod 的⽹络性能
    • 主要表现在两个 Pod 之间的⽹络转发、⽹络带宽、PPS 延迟等这些性能指标上。Overlay 模式的性能较差,因为它在节点上⼜做了⼀层虚拟化,还需要去封包,封包⼜会带来⼀些包头的损失、CPU 的消耗等,如果⼤家对⽹络性能的要求⽐较⾼,⽐如说机器学习、⼤数据这些场景就不适合使⽤ Overlay 模式。这种情形下我们通常选择 Underlay 或者路由模式的 CNI 插件。

相信⼤家通过这三步的挑选之后都能找到适合⾃⼰的⽹络插件。

主流CNI对比

CNI - 图5
综上, 在开源 CNI 的领域中calico, flannel, cilium 都有各自的优势,但是针对企业功能还略显不足。比如 calico, flannel 基本功能都有,社区也比较活跃,同时二者可以组合使用达到取长补短,但是组合使用维护成本也会增加。cilium 则是通过 eBPF 实现的独立的数据面,在网络安全,服务转发方面研究较深,但是运维起来比较困难。而Fabric则在功能方面比较全面,性能出色且稳定性强,运维也相对简单。

如何开发⾃⼰的 CNI 插件

有时社区的插件⽆法满⾜⾃⼰的需求,⽐如在阿⾥云上只能使⽤ vxlan 这种 Overlay 的插件,⽽ Overlay 插件的性能相对较差,⽆法满⾜阿⾥云上的⼀些业务需求,所以阿⾥云上开发了⼀个 Terway 的插件。
如果我们⾃⼰的环境⽐较特殊,在社区⾥⾯⼜找不到合适的⽹络插件,此时可以开发⼀个⾃⼰的 CNI 插件。
CNI 插件的实现通常包含两个部分:

  1. ⼀个⼆进制的 CNI 插件去配置 Pod ⽹卡和 IP 地址。这⼀步配置完成之后相当于给 Pod 上插上了⼀条⽹线,就是说它已经有⾃⼰的IP、有⾃⼰的⽹卡了。
  2. ⼀个 Daemon 进程去管理 Pod 之间的⽹络打通。这⼀步相当于说将 Pod 真正连上⽹络,让 Pod 之间能够互相通信。给 Pod 插上⽹线

    给 Pod 插上⽹线

    image.png

    1. 给 Pod 准备⼀个⽹卡

    通常我们会⽤⼀个 “veth” 这种虚拟⽹卡,⼀端放到 Pod 的⽹络空间,⼀端放到主机的⽹络空间,这样就实现了 Pod 与主机这两个命名空间的打通。

    2. 给 Pod 分配 IP 地址

    这个 IP 地址有⼀个要求,我们在之前介绍⽹络的时候也有提到,就是说这个 IP 地址在集群⾥需要是唯⼀的。如何保障集群⾥⾯给 Pod 分配的是个唯⼀的 IP 地址呢?
    ⼀般来说我们在创建整个集群的时候会指定 Pod 的⼀个⼤⽹段,按照每个节点去分配⼀个 Node ⽹段。⽐如说上图右侧创建的是⼀个 172.16 的⽹段,我们再按照每个节点去分配⼀个 /24 的段,这样就能保障每个节点上的地址是互不冲突的。然后每个 Pod 再从⼀个具体的节点上的⽹段中再去顺序分配具体的 IP 地址,⽐如 Pod1 分配到了 172.16.0.1,Pod2 分配到了 172.16.0.2,这样就实现了在节点⾥⾯ IP 地址分配的不冲突,并且不同的 Node ⼜分属不同的⽹段,因此不会冲突。
    这样就给 Pod 分配了集群⾥⾯⼀个唯⼀的 IP 地址。

    3. 配置 Pod 的 IP 和路由

  • 第⼀步,将分配到的 IP 地址配置给 Pod 的虚拟⽹卡;
  • 第⼆步,在 Pod 的⽹卡上配置集群⽹段的路由,令访问的流量都⾛到对应的 Pod ⽹卡上去,并且也会配置默认路由的⽹段到这个⽹卡上,也就是说⾛公⽹的流量也会⾛到这个⽹卡上进⾏路由;
  • 最后在宿主机上配置到 Pod 的 IP 地址的路由,指向到宿主机对端 veth1 这个虚拟⽹卡上。这样实现的是从 Pod 能够到宿主机上进⾏路由出去的,同时也实现了在宿主机上访问到 Pod 的 IP 地址也能路由到对应的 Pod 的⽹卡所对应的对端上去。

    给 Pod 连上⽹络

    刚才我们是给 Pod 插上⽹线,也就是给它配了 IP 地址以及路由表。那怎么打通 Pod 之间的通信呢?也就是让每⼀个 Pod 的 IP 地址在集群⾥⾯都能被访问到。
    ⼀般我们是在 CNI Daemon 进程中去做这些⽹络打通的事情。通常来说是这样⼀个步骤:

  • ⾸先 CNI 在每个节点上运⾏的 Daemon 进程会学习到集群所有 Pod 的 IP 地址及其所在节点信息。学习的⽅式通常是通过监听 K8sAPIServer,拿到现有 Pod 的 IP 地址以及节点,并且新的节点和新的 Pod 的创建的时候也能通知到每个 Daemon。

  • 拿到 Pod 以及 Node 的相关信息之后,再去配置⽹络进⾏打通。
    • ⾸先 Daemon 会创建到整个集群所有节点的通道。这⾥的通道是个抽象概念,具体实现⼀般是通过 Overlay 隧道、阿⾥云上的 VPC路由表、或者是⾃⼰机房⾥的 BGP 路由完成的;
    • 第⼆步是将所有 Pod 的 IP 地址跟上⼀步创建的通道关联起来。关联也是个抽象概念,具体的实现通常是通过 Linux 路由、fdb 转发表或者OVS 流表等完成的。Linux 路由可以设定某⼀个 IP 地址路由到哪个节点上去。fdb 转发表是 forwarding database 的缩写,就是把某个 Pod 的 IP 转发到某⼀个节点的隧道端点上去(Overlay ⽹络)。OVS 流表是由 Open vSwitch 实现的,它可以把 Pod的 IP 转发到对应的节点上。