K8S CNI

作者: 程序员札记 | 来源:发表于2022-01-17 14:57 被阅读0次

    从网络模型说起

    容器的网络技术日新月异,经过多年发展,业界逐渐聚焦到 Docker 的 CNM(Container Network Model, 容器网络模型) 和 CoreOS 的 CNI(Container Network Interface, 容器网络接口)。

    CNM 模型

    CNM 是一个被 Docker 提出的规范。现在已经被 Cisco Contiv, Kuryr, Open Virtual Networking (OVN), Project Calico, VMware 和 Weave 这些公司和项目所采纳。

    Libnetwork 是 CNM 的原生实现。它为 Docker daemon 和网络驱动程序之间提供了接口。网络控制器负责将驱动和一个网络进行对接。每个驱动程序负责管理它所拥有的网络以及为该网络提供的各种服务,例如 IPAM 等等。由多个驱动支撑的多个网络可以同时并存。原生驱动包括 none, bridge, overlay 以及 MACvlan。

    但是,container runtime 会在不同情况下使用到不同的插件,这带来了复杂性。另外,CNM 需要使用分布式存储系统来保存网络配置信息,例如 etcd。


    image.png

    CNM-model

    • Network Sandbox:容器内部的网络栈,包括网络接口、路由表、DNS 等配置的管理。Sandbox 可用 Linux 网络命名空间、FreeBSD Jail 等机制进行实现。一个 Sandbox 可以包含多个 Endpoint。

    • Endpoint:用于将容器内的 Sandbox 与外部网络相连的网络接口。可以使用 veth pair、Open vSwitch 的内部 port 等技术进行实现。一个 Endpoint 仅能够加入一个 Network。

    • Network:可以直接互连的 Endpoint 的集合。可以通过 Linux bridge、VLAN 等技术进行实现。一个 Network 包含多个 Endpoint。

    什么是CNI?

    CNI是Container Network Interface的缩写,是一个标准的通用的接口。为了让用户在容器创建或销毁时都能够更容易地配置容器网络,现在容器平台:docker,kubernetes,mesos。CNI 是由 CoreOS 提出的一个容器网络规范。已采纳改规范的包括 Apache Mesos, Cloud Foundry, Kubernetes, Kurma 和 rkt。另外 Contiv Networking, Project Calico 和 Weave 这些项目也为 CNI 提供插件。

    CNI 对外暴露了从一个网络里面添加和剔除容器的接口。CNI 使用一个 json 配置文件保存网络配置信息。和 CNM 不一样,CNI 不需要一个额外的分布式存储引擎。
    一个容器可以被加入到被不同插件所驱动的多个网络之中。一个网络有自己对应的插件和唯一的名称。CNI 插件需要提供两个命令:ADD 用来将网络接口加入到指定网络,DEL 用来将其移除。这两个接口分别在容器被创建和销毁的时候被调用。


    image.png

    CNI如何实现?

    CNI用于连接容器管理系统和网络插件。提供一个容器所在的network namespace,将network interface插入该network namespace中(比如veth的一端),并且在宿主机做一些必要的配置(例如将veth的另一端加入bridge中),最后对namespace中的interface进行IP和路由的配置。CNI插件是可执行文件,会被kubelet调用。启动kubelet --network-plugin=cni,–cni-conf-dir 指定networkconfig配置,默认路径是:/etc/cni/net.d,并且,–cni-bin-dir 指定plugin可执行文件路径,默认路径是:/opt/cni/bin
    前不论是个人还是企业,在使用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组件就有很多,这里我们详细介绍一些使用率较高的组件。


    image.png

    ipam类CNI

    ipam类型的cni插件,在执行add命令时会分配一个IP给调用者。执行del命令时会将调用者指定的ip放回ip池。社区开源的ipam有host-local、dhcp。IP Address Management,它是负责分配 IP 地址的二进制文件。比如,dhcp,这个文件会向 DHCP 服务器发起请求;host-local,则会使用预先配置的 IP 地址段来进行分配

    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、ipvlan、loopback、macvlan、ptp(point-to-point, Veth Pair 设备),以及 vlan。如开源的 Flannel、Weave 等项目,都属于 bridge 类型的 CNI 插件,在具体的实现中,它们往往会调用 bridge 这个二进制文件

    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)。由 CNI 社区维护的内置 CNI 插件,不能作为独立的插件使用,需要调用其他插件。tuning,是一个通过 sysctl 调整网络设备参数的二进制文件;portmap,是一个通过 iptables 配置端口映射的二进制文件;bandwidth,是一个使用 Token Bucket Filter (TBF) 来进行限流的二进制文件。

    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,flannel、calico、weave、Canal、Terway、以及 Contiv、azure。

    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:

    在默认的Docker配置中,每个节点上的Docker服务会分别负责所在节点容器的IP分配。这样导致的一个问题是,不同节点上容器可能获得相同的内外IP地址。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使用)。

    Flannel的功能简单的来讲就是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。 Flannel的设计目的就是为集群中的所有节点重新规划IP地址的使用规则,从而使得不同节点上的容器能够获得同属一个内网且不重复的IP地址,并让属于不同节点上的容器能够直接通过内网IP通信。Flannel实质上是一种覆盖网络(overlay network),也就是将TCP数据包装在另一种网络包里面进行路由转发和通信,目前已经支持udp、vxlan、host-gw、aws-vpc、gce和alloc路由等数据转发方式,常用的的三种转发模式。

    • hostgw 这种方式就是直接路由 (性能最高 要求集群节点在同一个网段)flannel的三层路由方案。每个node节点上都会记录其他节点容器ip段的路由,通过路由,node A上的容器发给node B上的容器的数据,就能在node A上进行转发。
    • vxlan 是flannel推荐的方式。需要通信的网络设备能够支持vxlan协议 (性能较好)

    • udp 该方式与vxlan很类似,它对ip层网络进行包装。通常用于调试环境或者不支持vxlan协议网络环境中。(性能最差)flannel的overlay方案。

    vxlan/udp 每个node节点上都有一个flanneld进程,和flannel0网桥,容器网络会与flannel0网桥互联,并经由flannel0发出,所以flanneld可以捕获到容器发出的报文,进行封装。udp方案下会给报文包装一个udp的头部,vxlan下会给报文包装一个vxlan协议的头部(配置了相同VNI的node,就能进行互联)。 目前flannel社区还提供了更多实验性的封装协议选择,比如ipip,但仍旧将vxlan作为默认的backend。

    优点:

         1.Flannel相对容易安装和配置。
    
         2.支持多个Kubernetes发行版。
    
         3.使集群中的不同Node主机创建的Docker容器都具有全集群唯一的虚拟IP地址。
    
         4.Flannel可以使用Kubernetes集群的现有etcd集群来使用API存储其状态信息,因此不需要专用的数据存储。
    
         5.默认路由使用的方法是使用VXLAN,因为VXLAN性能更良好并且需要的手动干预更少。
    

    缺点:

         1.不支持pod之间的网络隔离。Flannel设计思想是将所有的pod都放在一个大的二层网络中,所以pod之间没有隔离策略。
         2.由于使用二层技术,vlan 隔离和 tunnel 隧道则消耗更多的资源并对物理环境有要求,随着网络规模的增大,整体会变得越加复杂在较大的k8s集群规模下不适用。
    

    Calico:

    calico是基于BGP路由实现的容器集群网络方案,Calico是一个纯三层的协议,使用虚拟路由代替虚拟交换。与 Flannel 不同的是 Calico 不使用隧道或 NAT 来实现转发,而是巧妙的把所有二三层流量转换成三层流量,并通过 host 上路由配置完成跨 Host 转发。基础的calico使用体验可能和flannel host-gw是基本一样的。Calico在每个计算节点都利用Linux Kernel实现了一个高效的虚拟路由器vRouter来负责数据转发。每个vRouter都通过BGP1协议把在本节点上运行的容器的路由信息向整个Calico网络广播,并自动设置到达其他节点的路由转发规则。Calico保证所有容器之间的数据流量都是通过IP路由的方式完成互联互通的。Calico节点组网时可以直接利用网络结构,不需要额外的NAT、隧道或者Overlay Network,没有额外的封包解包,能够节约CPU运算,提高网络效率。支持两种网络IPIP、BGP

    • ipip模式则如同flannel ipip模式。对报文封装一个ipip头部,头部中使用node ip。发送到对端容器所在node的IP,对端的网络组件再解包,并转发给容器。
    • 不同之处在于flannel方案下路由都是通过代码逻辑进行配置。而calico则在每个节点建立bgp peer,bgp peer彼此之间会进行路由的共享和学习,所以自动生成并维护了路由。

    calico网络模型中的路由原理

    优点:

     1.二层网络通讯需要依赖广播消息机制,广播消息的开销与 host 的数量呈指数级增长,Calico 使用的三层路由方法,完全抑制了二层广播,减少了资源开销。
     2.不同之处在于flannel方案下路由都是通过代码逻辑进行配置。calico会在每个节点建立bgp peer,bgp peer彼此之间会进行路由的共享和学习,所以自动生成并维护了路由。在k8s集群大规模的情况下集群间的网络保持通畅。
     3.Calico 较少的依赖性使它能适配所有 VM、Container、混合环境场景。
     4.支持network-policy,自定义ingress(进栈)egress(出栈)规则。
    

    缺点:

     1.通过路由规则可以看出,路由规模和 pod 分布有关,如果 pod离散分布在 host 集群中,会产生较多的路由项。
     2.1台 Host 上可能虚拟化十几或几十个容器实例,过多的 iptables 规则造成复杂性和不可调试性,同时也存在性能损耗。
     3.网关路由问题,当对端网络不为二层可达时,需要通过三层路由机时,需要网关支持自定义路由配置,即 pod 的目的地址为本网段的网关地址,再由网关进行跨三层转发。
     4.简单使用和flannel无异,深层的使用需要有较高的学习成本。
    

    Weave:

    和flannel一样它能够创建一个虚拟网络,用于连接部署在多台主机上的Docker容器,这样容器就像被接入了同一个网络交换机,那些使用网络的应用程序不必去配置端口映射和链接等信息。外部设备能够访问Weave网络上的应用程序容器所提供的服务,同时已有的内部系统也能够暴露到应用程序容器上。Weave能够穿透防火墙并运行在部分连接的网络上。与Calico一样,Weave也为Kubernetes集群提供网络策略功能。

    优点:

        1.Weave通过创建虚拟网络使Docker容器能够跨主机通信并能够自动相互发现。
        2.通过weave网络,由多个容器构成的基于微服务架构的应用可以运行在任何地方主机,多主机,云上或者数据中心。应用程序使用网络就好像容器是插在同一个网络交换机上一样,不需要配置端口映射,连接等。
        3.Weave网络自动在两个节点之间选择最快的路径,提供接近本地网络的吞吐量和延迟。同时支持主机间通信加密。
        4.Weave网络能够在节点间转发流量,它甚至能够在网状网络部分连接的情况下工作。这意味着你可以在混合了传统系统和容器化的应用的环境中使用Weave网络来保持通信。
    

    缺点:

        1.只能通过weave launch或者weave connect加入weave网络。
    

    Canal:

    Canal 是一个项目的名称,它试图将Flannel提供的网络层与Calico的网络策略功能集成在一起。Canal名称的由来是Flannel和Calico的结合,最终只实现了将两种技术部署在一起的预期能力。出于这个原因,即使这个项目不复存在,业界还是会习惯性地将Flannel和Calico的组成称为“Canal”。由于Canal是Flannel和Calico的组合,因此它的优点也在于这两种技术的交叉。网络层用的是Flannel提供的简单overlay,可以在许多不同的部署环境中运行且无需额外的配置。在网络策略方面,Calico强大的网络规则评估,为基础网络提供了更多补充,从而提供了更多的安全性和控制。

    优点:集成Flannel和Calico和功能。
    缺点:部署相对比较复杂,维护和配置有较高的学习成本。

    Terway:

    专有网络VPC.1

    Terway是阿里云容器服务Kubernetes版自研的网络插件,使用原生的弹性网卡分配给Pod实现Pod网络。将阿里云的弹性网卡和辅助IP分配给容器,支持Network Policy来定义容器间的访问策略,支持对单个容器做带宽的限流,兼容Calico的Network Policy。在Terway网络插件中,每个Pod拥有自己网络栈和IP地址。同一台ECS内的Pod之间通信,直接通过机器内部的转发,跨ECS的Pod通信,报文通过VPC的弹性网卡直接转发。由于不需要使用VxLAN等的隧道技术封装报文,因此具有较高的通信性能。
    优点:相比Flannel支持了Network Policy访问策略,支持对单个容器做带宽对限流。

    缺点:依赖与阿里云服务器平台。

    Contiv:

    Contiv是一个用于跨虚拟机、裸机、公有云或私有云的异构容器部署的开源容器网络架构。Contiv具有2层、3层、overlay和ACI模式,能够与思科基础设施进行本地集成,并使用丰富的网络和安全策略将应用意图与基础设施功能进行映射。能够和非容器环境协作,不依赖物理网络。支持物理网卡sriov和offload。支持Policy/ACI/Qos租户。

    优点:

              1.集成Flannel和Calico所有的功能。
              2.能够与思科基础设施完美的集成。
              3.支持物理网卡sriov和offload。支持Policy/ACI/Qos租户
    

    缺点:

                1.集成配置比较复杂,学习资料少。
                2.学习成本较高,需要定制化开发。
    

    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作为容器网络的网关,容器网络通过网桥与其他节点形成一个大二层的网络。

    kubelet的网络插件

    kubelet的网络插件有以下3种类型:
    (1)CNI;
    (2)kubenet;
    (3)Noop,代表不配置网络插件。

    这里主要对kubelet中CNI相关的源码进行分析。

    kubelet创建/删除pod时,会调用CRI,然后CRI会调用CNI来进行pod网络的构建/删除。

    image

    kubelet构建pod网络的大致过程

    (1)kubelet先通过CRI创建pause容器(pod sandbox),生成network namespace;
    (2)kubelet根据启动参数配置调用具体的网络插件如CNI网络插件;
    (3)网络插件给pause容器(pod sandbox)配置网络;
    (4)pod 中其他的容器都与pause容器(pod sandbox)共享网络。

    kubelet中cni相关的源码分析

    kubelet的cni源码分析包括如下几部分:
    (1)cni相关启动参数分析;
    (2)关键struct/interface分析;
    (3)cni初始化分析;
    (4)cni构建pod网络分析;
    (5)cni销毁pod网络分析。
    1.kubelet组件cni相关启动参数分析
    kubelet组件cni相关启动参数相关代码如下:

    // pkg/kubelet/config/flags.go
    func (s *ContainerRuntimeOptions) AddFlags(fs *pflag.FlagSet) {
        ...
        // Network plugin settings for Docker.
        fs.StringVar(&s.NetworkPluginName, "network-plugin", s.NetworkPluginName, fmt.Sprintf("<Warning: Alpha feature> The name of the network plugin to be invoked for various events in kubelet/pod lifecycle. %s", dockerOnlyWarning))
        fs.StringVar(&s.CNIConfDir, "cni-conf-dir", s.CNIConfDir, fmt.Sprintf("<Warning: Alpha feature> The full path of the directory in which to search for CNI config files. %s", dockerOnlyWarning))
        fs.StringVar(&s.CNIBinDir, "cni-bin-dir", s.CNIBinDir, fmt.Sprintf("<Warning: Alpha feature> A comma-separated list of full paths of directories in which to search for CNI plugin binaries. %s", dockerOnlyWarning))
        fs.StringVar(&s.CNICacheDir, "cni-cache-dir", s.CNICacheDir, fmt.Sprintf("<Warning: Alpha feature> The full path of the directory in which CNI should store cache files. %s", dockerOnlyWarning))
        fs.Int32Var(&s.NetworkPluginMTU, "network-plugin-mtu", s.NetworkPluginMTU, fmt.Sprintf("<Warning: Alpha feature> The MTU to be passed to the network plugin, to override the default. Set to 0 to use the default 1460 MTU. %s", dockerOnlyWarning))
        ...
    }
    

    cni相关启动参数的默认值在NewContainerRuntimeOptions函数中设置。

    // cmd/kubelet/app/options/container_runtime.go
    // NewContainerRuntimeOptions will create a new ContainerRuntimeOptions with
    // default values.
    func NewContainerRuntimeOptions() *config.ContainerRuntimeOptions {
        dockerEndpoint := ""
        if runtime.GOOS != "windows" {
            dockerEndpoint = "unix:///var/run/docker.sock"
        }
    
        return &config.ContainerRuntimeOptions{
            ContainerRuntime:           kubetypes.DockerContainerRuntime,
            RedirectContainerStreaming: false,
            DockerEndpoint:             dockerEndpoint,
            DockershimRootDirectory:    "/var/lib/dockershim",
            PodSandboxImage:            defaultPodSandboxImage,
            ImagePullProgressDeadline:  metav1.Duration{Duration: 1 * time.Minute},
            ExperimentalDockershim:     false,
    
            //Alpha feature
            CNIBinDir:   "/opt/cni/bin",
            CNIConfDir:  "/etc/cni/net.d",
            CNICacheDir: "/var/lib/cni/cache",
        }
    }
    

    下面来简单分析几个比较重要的cni相关启动参数:
    (1)--network-plugin:指定要使用的网络插件类型,可选值cni、kubenet、"",默认为空串,代表Noop,即不配置网络插件(不构建pod网络)。此处配置值为cni时,即指定kubelet使用的网络插件类型为cni。

    (2)--cni-conf-dir:CNI 配置文件所在路径。默认值:/etc/cni/net.d。

    (3)--cni-bin-dir:CNI 插件的可执行文件所在路径,kubelet 将在此路径中查找 CNI 插件的可执行文件来执行pod的网络操作。默认值:/opt/cni/bin。

    关键struct/interface分析

    interface NetworkPlugin

    interface:NetworkPlugin。

    NetworkPlugin interface声明了kubelet网络插件的一些操作方法,不同类型的网络插件只需要实现这些方法即可,其中最关键的就是SetUpPod与TearDownPod方法,作用分别是构建pod网络与销毁pod网络,cniNetworkPlugin实现了该interface。

    // pkg/kubelet/dockershim/network/plugins.go
    // NetworkPlugin is an interface to network plugins for the kubelet
    type NetworkPlugin interface {
        // Init initializes the plugin.  This will be called exactly once
        // before any other methods are called.
        Init(host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error
    
        // Called on various events like:
        // NET_PLUGIN_EVENT_POD_CIDR_CHANGE
        Event(name string, details map[string]interface{})
    
        // Name returns the plugin's name. This will be used when searching
        // for a plugin by name, e.g.
        Name() string
    
        // Returns a set of NET_PLUGIN_CAPABILITY_*
        Capabilities() utilsets.Int
    
        // SetUpPod is the method called after the infra container of
        // the pod has been created but before the other containers of the
        // pod are launched.
        SetUpPod(namespace string, name string, podSandboxID kubecontainer.ContainerID, annotations, options map[string]string) error
    
        // TearDownPod is the method called before a pod's infra container will be deleted
        TearDownPod(namespace string, name string, podSandboxID kubecontainer.ContainerID) error
    
        // GetPodNetworkStatus is the method called to obtain the ipv4 or ipv6 addresses of the container
        GetPodNetworkStatus(namespace string, name string, podSandboxID kubecontainer.ContainerID) (*PodNetworkStatus, error)
    
        // Status returns error if the network plugin is in error state
        Status() error
    }
    

    struct cniNetworkPlugin

    cniNetworkPlugin struct实现了NetworkPlugin interface,实现了SetUpPod与TearDownPod等方法。

    // pkg/kubelet/dockershim/network/cni/cni.go
    type cniNetworkPlugin struct {
        network.NoopNetworkPlugin
    
        loNetwork *cniNetwork
    
        sync.RWMutex
        defaultNetwork *cniNetwork
    
        host        network.Host
        execer      utilexec.Interface
        nsenterPath string
        confDir     string
        binDirs     []string
        cacheDir    string
        podCidr     string
    }
    

    struct PluginManager

    struct PluginManager中的plugin属性是interface NetworkPlugin类型,可以传入具体的网络插件实现,如cniNetworkPlugin struct。

    // pkg/kubelet/dockershim/network/plugins.go
    // The PluginManager wraps a kubelet network plugin and provides synchronization
    // for a given pod's network operations.  Each pod's setup/teardown/status operations
    // are synchronized against each other, but network operations of other pods can
    // proceed in parallel.
    type PluginManager struct {
        // Network plugin being wrapped
        plugin NetworkPlugin
    
        // Pod list and lock
        podsLock sync.Mutex
        pods     map[string]*podLock
    }
    

    struct dockerService

    struct dockerService其实在CRI分析的博文部分有做过详细分析,可以去回顾一下,下面再简单做一下介绍。

    struct dockerService实现了CRI shim服务端的容器运行时接口以及容器镜像接口,所以其代表了dockershim(kubelet内置的CRI shim)的服务端。

    struct dockerService中的network属性是struct PluginManager类型,在该结构体初始化时会将具体的网络插件结构体如struct cniNetworkPlugin存储进该属性。

    创建pod、删除pod时会根据dockerService结构体的network属性里面存储的具体的网络插件结构体,去调用某个具体网络插件(如cniNetworkPlugin)的SetUpPod、TearDownPod方法来构建pod的网络、销毁pod的网络。

    // pkg/kubelet/dockershim/docker_service.go
    type dockerService struct {
        client           libdocker.Interface
        os               kubecontainer.OSInterface
        podSandboxImage  string
        streamingRuntime *streamingRuntime
        streamingServer  streaming.Server
    
        network *network.PluginManager
        // Map of podSandboxID :: network-is-ready
        networkReady     map[string]bool
        networkReadyLock sync.Mutex
    
        containerManager cm.ContainerManager
        // cgroup driver used by Docker runtime.
        cgroupDriver      string
        checkpointManager checkpointmanager.CheckpointManager
        // caches the version of the runtime.
        // To be compatible with multiple docker versions, we need to perform
        // version checking for some operations. Use this cache to avoid querying
        // the docker daemon every time we need to do such checks.
        versionCache *cache.ObjectCache
        // startLocalStreamingServer indicates whether dockershim should start a
        // streaming server on localhost.
        startLocalStreamingServer bool
    
        // containerCleanupInfos maps container IDs to the `containerCleanupInfo` structs
        // needed to clean up after containers have been removed.
        // (see `applyPlatformSpecificDockerConfig` and `performPlatformSpecificContainerCleanup`
        // methods for more info).
        containerCleanupInfos map[string]*containerCleanupInfo
    }
    

    cni初始化分析

    Kubelet 启动过程中针对网络主要做以下步骤,分别是探针获取当前环境的网络插件以及初始化网络插件(只有当容器运行时选择为内置dockershim时,才会做CNI的初始化操作,将CNI初始化完成后交给dockershim使用)。

    cni初始化的调用链:
    main (cmd/kubelet/kubelet.go)
    -> NewKubeletCommand (cmd/kubelet/app/server.go)
    -> Run (cmd/kubelet/app/server.go)
    -> run (cmd/kubelet/app/server.go)
    -> RunKubelet (cmd/kubelet/app/server.go)
    -> CreateAndInitKubelet(cmd/kubelet/app/server.go)
    -> kubelet.NewMainKubelet(pkg/kubelet/kubelet.go)
    -> cni.ProbeNetworkPlugins & network.InitNetworkPlugin(pkg/kubelet/network/plugins.go)

    调用链很长,这里直接进入关键的函数NewMainKubelet进行分析。

    NewMainKubelet

    NewMainKubelet函数中主要看到dockershim.NewDockerService调用。

    // pkg/kubelet/kubelet.go
    // NewMainKubelet instantiates a new Kubelet object along with all the required internal modules.
    // No initialization of Kubelet and its modules should happen here.
    func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,...) {
        ...
        switch containerRuntime {
        case kubetypes.DockerContainerRuntime:
            // Create and start the CRI shim running as a grpc server.
            streamingConfig := getStreamingConfig(kubeCfg, kubeDeps, crOptions)
            ds, err := dockershim.NewDockerService(kubeDeps.DockerClientConfig, crOptions.PodSandboxImage, streamingConfig,
                &pluginSettings, runtimeCgroups, kubeCfg.CgroupDriver, crOptions.DockershimRootDirectory, !crOptions.RedirectContainerStreaming)
        ...
    }
    

    这里对变量containerRuntime值等于docker时做分析,即kubelet启动参数--container-runtime值为docker,这时kubelet会使用内置的CRI shim即dockershim作为容器运行时,初始化并启动dockershim。

    其中,调用dockershim.NewDockerService的作用是:新建并初始化dockershim服务端,包括初始化docker client、初始化cni网络配置等操作。

    而其中CNI部分的主要逻辑为:
    (1)调用cni.ProbeNetworkPlugins:根据kubelet启动参数cni相关配置,获取cni配置文件、cni网络插件可执行文件等信息,根据这些cni的相关信息来初始化cniNetworkPlugin结构体并返回;
    (2)调用network.InitNetworkPlugin:根据networkPluginName的值(对应kubelet启动参数--network-plugin),选择相应的网络插件,调用其Init()方法,做网络插件的初始化操作(初始化操作主要是起了一个goroutine,定时探测cni的配置文件以及可执行文件,让其可以热更新);
    (3)将上面步骤中获取到的cniNetworkPlugin结构体,赋值给dockerService struct的network属性,待后续创建pod、删除pod时可以调用cniNetworkPlugin的SetUpPod、TearDownPod方法来构建pod的网络、销毁pod的网络。

    kubelet对CNI的实现的主要代码:pkg/kubelet/network/cni/cni.go-SetUpPod/TearDownPod(构建Pod网络和销毁Pod网络)

    其中函数入参pluginSettings *NetworkPluginSettings的参数值,其实是从kubelet启动参数配置而来,kubelet cni相关启动参数在前面已经做了分析了,忘记的可以回头看一下。

    // pkg/kubelet/dockershim/docker_service.go
    // NewDockerService creates a new `DockerService` struct.
    // NOTE: Anything passed to DockerService should be eventually handled in another way when we switch to running the shim as a different process.
    func NewDockerService(config *ClientConfig, podSandboxImage string, streamingConfig *streaming.Config, pluginSettings *NetworkPluginSettings,
        cgroupsName string, kubeCgroupDriver string, dockershimRootDir string, startLocalStreamingServer bool, noJsonLogPath string) (DockerService, error) {
        ...
        ds := &dockerService{
            client:          c,
            os:              kubecontainer.RealOS{},
            podSandboxImage: podSandboxImage,
            streamingRuntime: &streamingRuntime{
                client:      client,
                execHandler: &NativeExecHandler{},
            },
            containerManager:          cm.NewContainerManager(cgroupsName, client),
            checkpointManager:         checkpointManager,
            startLocalStreamingServer: startLocalStreamingServer,
            networkReady:              make(map[string]bool),
            containerCleanupInfos:     make(map[string]*containerCleanupInfo),
            noJsonLogPath:             noJsonLogPath,
        }
        ...
        // dockershim currently only supports CNI plugins.
        pluginSettings.PluginBinDirs = cni.SplitDirs(pluginSettings.PluginBinDirString)
        // (1)根据kubelet启动参数cni相关配置,获取cni配置文件、cni网络插件可执行文件等信息,根据这些cni的相关信息来初始化```cniNetworkPlugin```结构体并返回
        cniPlugins := cni.ProbeNetworkPlugins(pluginSettings.PluginConfDir, pluginSettings.PluginCacheDir, pluginSettings.PluginBinDirs)
        cniPlugins = append(cniPlugins, kubenet.NewPlugin(pluginSettings.PluginBinDirs, pluginSettings.PluginCacheDir))
        netHost := &dockerNetworkHost{
            &namespaceGetter{ds},
            &portMappingGetter{ds},
        }
        // (2)根据networkPluginName的值(对应kubelet启动参数```--network-plugin```),选择相应的网络插件,调用其```Init()```方法,做网络插件的初始化操作(初始化操作主要是起了一个goroutine,定时探测cni的配置文件以及可执行文件,让其可以热更新)
        plug, err := network.InitNetworkPlugin(cniPlugins, pluginSettings.PluginName, netHost, pluginSettings.HairpinMode, pluginSettings.NonMasqueradeCIDR, pluginSettings.MTU)
        if err != nil {
            return nil, fmt.Errorf("didn't find compatible CNI plugin with given settings %+v: %v", pluginSettings, err)
        }
        // (3)将上面步骤中获取到的```cniNetworkPlugin```结构体,赋值给```dockerService struct```的```network```属性,待后续创建pod、删除pod时可以调用```cniNetworkPlugin```的```SetUpPod```、```TearDownPod```方法来构建pod的网络、销毁pod的网络。  
        ds.network = network.NewPluginManager(plug)
        klog.Infof("Docker cri networking managed by %v", plug.Name())
        ...
    }
    

    先来看下pluginSettings长什么样,其实是struct NetworkPluginSettings,包含了网络插件名称、网络插件可执行文件所在目录、网络插件配置文件所在目录等属性,代码如下:

    // pkg/kubelet/dockershim/docker_service.go
    type NetworkPluginSettings struct {
        // HairpinMode is best described by comments surrounding the kubelet arg
        HairpinMode kubeletconfig.HairpinMode
        // NonMasqueradeCIDR is the range of ips which should *not* be included
        // in any MASQUERADE rules applied by the plugin
        NonMasqueradeCIDR string
        // PluginName is the name of the plugin, runtime shim probes for
        PluginName string
        // PluginBinDirString is a list of directiores delimited by commas, in
        // which the binaries for the plugin with PluginName may be found.
        PluginBinDirString string
        // PluginBinDirs is an array of directories in which the binaries for
        // the plugin with PluginName may be found. The admin is responsible for
        // provisioning these binaries before-hand.
        PluginBinDirs []string
        // PluginConfDir is the directory in which the admin places a CNI conf.
        // Depending on the plugin, this may be an optional field, eg: kubenet
        // generates its own plugin conf.
        PluginConfDir string
        // PluginCacheDir is the directory in which CNI should store cache files.
        PluginCacheDir string
        // MTU is the desired MTU for network devices created by the plugin.
        MTU int
    }
    

    cni.ProbeNetworkPlugins

    cni.ProbeNetworkPlugins中主要作用为:根据kubelet启动参数cni相关配置,获取cni配置文件、cni网络插件可执行文件等信息,根据这些cni的相关信息来初始化cniNetworkPlugin结构体并返回。

    其中看到plugin.syncNetworkConfig()调用,主要作用是给cniNetworkPlugin结构体的defaultNetwork属性赋值。

    // pkg/kubelet/dockershim/network/cni/cni.go
    // ProbeNetworkPlugins : get the network plugin based on cni conf file and bin file
    func ProbeNetworkPlugins(confDir, cacheDir string, binDirs []string) []network.NetworkPlugin {
        old := binDirs
        binDirs = make([]string, 0, len(binDirs))
        for _, dir := range old {
            if dir != "" {
                binDirs = append(binDirs, dir)
            }
        }
    
        plugin := &cniNetworkPlugin{
            defaultNetwork: nil,
            loNetwork:      getLoNetwork(binDirs),
            execer:         utilexec.New(),
            confDir:        confDir,
            binDirs:        binDirs,
            cacheDir:       cacheDir,
        }
    
        // sync NetworkConfig in best effort during probing.
        plugin.syncNetworkConfig()
        return []network.NetworkPlugin{plugin}
    }
    

    plugin.syncNetworkConfig()

    主要逻辑:
    (1)getDefaultCNINetwork():根据kubelet启动参数配置,去对应的cni conf文件夹下寻找cni配置文件,返回包含cni信息的cniNetwork结构体;
    (2)plugin.setDefaultNetwork():根据上一步获取到的cniNetwork结构体,赋值给cniNetworkPlugin结构体的defaultNetwork属性。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) syncNetworkConfig() {
        network, err := getDefaultCNINetwork(plugin.confDir, plugin.binDirs)
        if err != nil {
            klog.Warningf("Unable to update cni config: %s", err)
            return
        }
        plugin.setDefaultNetwork(network)
    }
    
    getDefaultCNINetwork()

    主要逻辑:
    (1)在cni配置文件所在目录下,可以识别3种cni配置文件,分别是.conf, .conflist, .json。

    (2)调用sort.Strings()将cni配置文件所在目录下的所有cni配置文件按照字典顺序升序排序。

    (3)只取第一个读取到的cni配置文件,然后直接return。所以就算在cni配置文件目录下配置了多个cni配置文件,也只会有其中一个最终生效。

    (4)调用cniConfig.ValidateNetworkList(),校验cni可执行文件目录下是否存在对应的可执行文件。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func getDefaultCNINetwork(confDir string, binDirs []string) (*cniNetwork, error) {
        files, err := libcni.ConfFiles(confDir, []string{".conf", ".conflist", ".json"})
        switch {
        case err != nil:
            return nil, err
        case len(files) == 0:
            return nil, fmt.Errorf("no networks found in %s", confDir)
        }
    
        cniConfig := &libcni.CNIConfig{Path: binDirs}
    
        sort.Strings(files)
        for _, confFile := range files {
            var confList *libcni.NetworkConfigList
            if strings.HasSuffix(confFile, ".conflist") {
                confList, err = libcni.ConfListFromFile(confFile)
                if err != nil {
                    klog.Warningf("Error loading CNI config list file %s: %v", confFile, err)
                    continue
                }
            } else {
                conf, err := libcni.ConfFromFile(confFile)
                if err != nil {
                    klog.Warningf("Error loading CNI config file %s: %v", confFile, err)
                    continue
                }
                // Ensure the config has a "type" so we know what plugin to run.
                // Also catches the case where somebody put a conflist into a conf file.
                if conf.Network.Type == "" {
                    klog.Warningf("Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?", confFile)
                    continue
                }
    
                confList, err = libcni.ConfListFromConf(conf)
                if err != nil {
                    klog.Warningf("Error converting CNI config file %s to list: %v", confFile, err)
                    continue
                }
            }
            if len(confList.Plugins) == 0 {
                klog.Warningf("CNI config list %s has no networks, skipping", string(confList.Bytes[:maxStringLengthInLog(len(confList.Bytes))]))
                continue
            }
    
            // Before using this CNI config, we have to validate it to make sure that
            // all plugins of this config exist on disk
            caps, err := cniConfig.ValidateNetworkList(context.TODO(), confList)
            if err != nil {
                klog.Warningf("Error validating CNI config list %s: %v", string(confList.Bytes[:maxStringLengthInLog(len(confList.Bytes))]), err)
                continue
            }
    
            klog.V(4).Infof("Using CNI configuration file %s", confFile)
    
            return &cniNetwork{
                name:          confList.Name,
                NetworkConfig: confList,
                CNIConfig:     cniConfig,
                Capabilities:  caps,
            }, nil
        }
        return nil, fmt.Errorf("no valid networks found in %s", confDir)
    }
    
    plugin.setDefaultNetwork

    将上面获取到的cniNetwork结构体赋值给cniNetworkPlugin结构体的defaultNetwork属性。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) setDefaultNetwork(n *cniNetwork) {
        plugin.Lock()
        defer plugin.Unlock()
        plugin.defaultNetwork = n
    }
    

    network.InitNetworkPlugin

    network.InitNetworkPlugin()主要作用:根据networkPluginName的值(对应kubelet启动参数--network-plugin),选择相应的网络插件,调用其Init()方法,做网络插件的初始化操作。

    // pkg/kubelet/dockershim/network/plugins.go
    // InitNetworkPlugin inits the plugin that matches networkPluginName. Plugins must have unique names.
    func InitNetworkPlugin(plugins []NetworkPlugin, networkPluginName string, host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) (NetworkPlugin, error) {
        if networkPluginName == "" {
            // default to the no_op plugin
            plug := &NoopNetworkPlugin{}
            plug.Sysctl = utilsysctl.New()
            if err := plug.Init(host, hairpinMode, nonMasqueradeCIDR, mtu); err != nil {
                return nil, err
            }
            return plug, nil
        }
    
        pluginMap := map[string]NetworkPlugin{}
    
        allErrs := []error{}
        for _, plugin := range plugins {
            name := plugin.Name()
            if errs := validation.IsQualifiedName(name); len(errs) != 0 {
                allErrs = append(allErrs, fmt.Errorf("network plugin has invalid name: %q: %s", name, strings.Join(errs, ";")))
                continue
            }
    
            if _, found := pluginMap[name]; found {
                allErrs = append(allErrs, fmt.Errorf("network plugin %q was registered more than once", name))
                continue
            }
            pluginMap[name] = plugin
        }
    
        chosenPlugin := pluginMap[networkPluginName]
        if chosenPlugin != nil {
            err := chosenPlugin.Init(host, hairpinMode, nonMasqueradeCIDR, mtu)
            if err != nil {
                allErrs = append(allErrs, fmt.Errorf("network plugin %q failed init: %v", networkPluginName, err))
            } else {
                klog.V(1).Infof("Loaded network plugin %q", networkPluginName)
            }
        } else {
            allErrs = append(allErrs, fmt.Errorf("network plugin %q not found", networkPluginName))
        }
    
        return chosenPlugin, utilerrors.NewAggregate(allErrs)
    }
    
    chosenPlugin.Init()

    当kubelet启动参数--network-plugin的值配置为cni时,会调用到cniNetworkPlugin的Init()方法,代码如下。

    启动一个goroutine,每隔5秒,调用一次plugin.syncNetworkConfig。再来回忆一下plugin.syncNetworkConfig()的作用:根据kubelet启动参数配置,去对应的cni conf文件夹下寻找cni配置文件,返回包含cni信息的cniNetwork结构体,赋值给cniNetworkPlugin结构体的defaultNetwork属性,从而达到cni conf以及bin更新后,kubelet也能感知并更新cniNetworkPlugin结构体的效果。

    此处也可以看出该goroutine存在的意义,让cni的配置文件以及可执行文件等可以热更新,而无需重启kubelet。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) Init(host network.Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error {
        err := plugin.platformInit()
        if err != nil {
            return err
        }
    
        plugin.host = host
    
        plugin.syncNetworkConfig()
    
        // start a goroutine to sync network config from confDir periodically to detect network config updates in every 5 seconds
        go wait.Forever(plugin.syncNetworkConfig, defaultSyncConfigPeriod)
    
        return nil
    }
    plugin.platformInit()只是检查了下是否有nsenter,没有做其他操作。
    
    // pkg/kubelet/dockershim/network/cni/cni_others.go
    func (plugin *cniNetworkPlugin) platformInit() error {
        var err error
        plugin.nsenterPath, err = plugin.execer.LookPath("nsenter")
        if err != nil {
            return err
        }
        return nil
    }
    

    CNI构建pod网络分析

    kubelet创建pod时,通过CRI创建并启动pod sandbox,然后CRI会调用CNI网络插件构建pod网络。

    kubelet中CNI构建pod网络的方法是:pkg/kubelet/network/cni/cni.go-SetUpPod。

    其中SetUpPod方法的调用链如下(只列出了关键部分):
    main (cmd/kubelet/kubelet.go)
    ...
    -> klet.syncPod(pkg/kubelet/kubelet.go)
    -> kl.containerRuntime.SyncPod(pkg/kubelet/kubelet.go)
    -> m.createPodSandbox(pkg/kubelet/kuberuntime/kuberuntime_manager.go)
    -> m.runtimeService.RunPodSandbox (pkg/kubelet/kuberuntime/kuberuntime_sandbox.go)
    -> ds.network.SetUpPod(pkg/kubelet/dockershim/docker_sandbox.go)
    -> pm.plugin.SetUpPod(pkg/kubelet/dockershim/network/plugins.go)
    -> SetUpPod(pkg/kubelet/dockershim/network/cni/cni.go)

    下面的代码只是列出来看一下关键方法cniNetworkPlugin.SetUpPod()的调用链,不做具体分析。

    // pkg/kubelet/kuberuntime/kuberuntime_manager.go
    func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
        ...
        podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt)
        ...
    }
    // pkg/kubelet/kuberuntime/kuberuntime_sandbox.go
    // createPodSandbox creates a pod sandbox and returns (podSandBoxID, message, error).
    func (m *kubeGenericRuntimeManager) createPodSandbox(pod *v1.Pod, attempt uint32) (string, string, error) {
        ...
        podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig, runtimeHandler)
        ...
    }
    在RunPodSandbox方法中可以看到,是先创建pod sandbox,然后启动pod sandbox,然后才是给该pod sandbox构建网络。
    
    // pkg/kubelet/dockershim/docker_sandbox.go
    func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
        ...
        createResp, err := ds.client.CreateContainer(*createConfig)
        ...
        err = ds.client.StartContainer(createResp.ID)
        ...
        err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)
        ...
    }
    

    在PluginManager.SetUpPod方法中可以看到,调用了pm.plugin.SetUpPod,前面介绍cni初始化的时候讲过相关赋值初始化操作,这里会调用到cniNetworkPlugin的SetUpPod方法。

    // pkg/kubelet/dockershim/network/plugins.go
    func (pm *PluginManager) SetUpPod(podNamespace, podName string, id kubecontainer.ContainerID, annotations, options map[string]string) error {
        defer recordOperation("set_up_pod", time.Now())
        fullPodName := kubecontainer.BuildPodFullName(podName, podNamespace)
        pm.podLock(fullPodName).Lock()
        defer pm.podUnlock(fullPodName)
    
        klog.V(3).Infof("Calling network plugin %s to set up pod %q", pm.plugin.Name(), fullPodName)
        if err := pm.plugin.SetUpPod(podNamespace, podName, id, annotations, options); err != nil {
            return fmt.Errorf("networkPlugin %s failed to set up pod %q network: %v", pm.plugin.Name(), fullPodName, err)
        }
    
        return nil
    }
    

    cniNetworkPlugin.SetUpPod

    cniNetworkPlugin.SetUpPod方法作用cni网络插件构建pod网络的调用入口。其主要逻辑为:
    (1)调用plugin.checkInitialized():检查网络插件是否已经初始化完成;
    (2)调用plugin.host.GetNetNS():获取容器网络命名空间路径,格式/proc/${容器PID}/ns/net;
    (3)调用context.WithTimeout():设置调用cni网络插件的超时时间;
    (3)调用plugin.addToNetwork():如果是linux环境,则调用cni网络插件,给pod构建回环网络;
    (4)调用plugin.addToNetwork():调用cni网络插件,给pod构建默认网络。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations, options map[string]string) error {
        if err := plugin.checkInitialized(); err != nil {
            return err
        }
        netnsPath, err := plugin.host.GetNetNS(id.ID)
        if err != nil {
            return fmt.Errorf("CNI failed to retrieve network namespace path: %v", err)
        }
    
        // Todo get the timeout from parent ctx
        cniTimeoutCtx, cancelFunc := context.WithTimeout(context.Background(), network.CNITimeoutSec*time.Second)
        defer cancelFunc()
        // Windows doesn't have loNetwork. It comes only with Linux
        if plugin.loNetwork != nil {
            if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, netnsPath, annotations, options); err != nil {
                return err
            }
        }
    
        _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, annotations, options)
        return err
    }
    

    plugin.addToNetwork

    plugin.addToNetwork方法的作用就是调用cni网络插件,给pod构建指定类型的网络,其主要逻辑为:
    (1)调用plugin.buildCNIRuntimeConf():构建调用cni网络插件的配置;
    (2)调用cniNet.AddNetworkList():调用cni网络插件,进行网络构建。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) addToNetwork(ctx context.Context, network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (cnitypes.Result, error) {
        rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, annotations, options)
        if err != nil {
            klog.Errorf("Error adding network when building cni runtime conf: %v", err)
            return nil, err
        }
    
        pdesc := podDesc(podNamespace, podName, podSandboxID)
        netConf, cniNet := network.NetworkConfig, network.CNIConfig
        klog.V(4).Infof("Adding %s to network %s/%s netns %q", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, podNetnsPath)
        res, err := cniNet.AddNetworkList(ctx, netConf, rt)
        if err != nil {
            klog.Errorf("Error adding %s to network %s/%s: %v", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, err)
            return nil, err
        }
        klog.V(4).Infof("Added %s to network %s: %v", pdesc, netConf.Name, res)
        return res, nil
    }
    

    cniNet.AddNetworkList

    AddNetworkList方法中主要是调用了addNetwork方法,所以来看下addNetwork方法的逻辑:
    (1)调用c.exec.FindInPath():拼接出cni网络插件可执行文件的绝对路径;
    (2)调用buildOneConfig():构建配置;
    (3)调用c.args():构建调用cni网络插件的参数;
    (4)调用invoke.ExecPluginWithResult():调用cni网络插件进行pod网络的构建操作。

    // vendor/github.com/containernetworking/cni/libcni/api.go 
    func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
        var err error
        var result types.Result
        for _, net := range list.Plugins {
            result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
            if err != nil {
                return nil, err
            }
        }
    
        if err = setCachedResult(result, list.Name, rt); err != nil {
            return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
        }
    
        return result, nil
    }
    
    func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
        c.ensureExec()
        pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
        if err != nil {
            return nil, err
        }
    
        newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
        if err != nil {
            return nil, err
        }
    
        return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
    }
    

    c.args

    c.args方法作用是构建调用cni网络插件可执行文件时的参数。

    从代码中可以看出,参数有Command(命令,Add代表构建网络,Del代表销毁网络)、ContainerID(容器ID)、NetNS(容器网络命名空间路径)、IfName(Interface Name即网络接口名称)、PluginArgs(其他参数如pod名称、pod命名空间等)等。

    // vendor/github.com/containernetworking/cni/libcni/api.go
    func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
        return &invoke.Args{
            Command:     action,
            ContainerID: rt.ContainerID,
            NetNS:       rt.NetNS,
            PluginArgs:  rt.Args,
            IfName:      rt.IfName,
            Path:        strings.Join(c.Path, string(os.PathListSeparator)),
        }
    }
    

    invoke.ExecPluginWithResult

    invoke.ExecPluginWithResult主要是将调用参数变成env,然后调用cni网络插件可执行文件,并获取返回结果。

    func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
        if exec == nil {
            exec = defaultExec
        }
    
        stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
        if err != nil {
            return nil, err
        }
    
        // Plugin must return result in same version as specified in netconf
        versionDecoder := &version.ConfigDecoder{}
        confVersion, err := versionDecoder.Decode(netconf)
        if err != nil {
            return nil, err
        }
    
        return version.NewResult(confVersion, stdoutBytes)
    }
    

    5.CNI销毁pod网络分析
    kubelet删除pod时,CRI会调用CNI网络插件销毁pod网络。

    kubelet中CNI销毁pod网络的方法是:pkg/kubelet/network/cni/cni.go-TearDownPod。

    其中TearDownPod方法的调用链如下(只列出了关键部分):
    main (cmd/kubelet/kubelet.go)
    ...
    -> m.runtimeService.StopPodSandbox (pkg/kubelet/kuberuntime/kuberuntime_sandbox.go)
    -> ds.network.TearDownPod(pkg/kubelet/dockershim/docker_sandbox.go)
    -> pm.plugin.TearDownPod(pkg/kubelet/dockershim/network/plugins.go)
    -> TearDownPod(pkg/kubelet/dockershim/network/cni/cni.go)

    下面的代码只是列出来看一下关键方法cniNetworkPlugin.TearDownPod()的调用链,不做具体分析。

    在StopPodSandbox方法中可以看到,会先销毁pod网络,然后停止pod sandbox的运行,但是这两个操作中的任何一个发生错误,kubelet都会继续进行重试,直到成功为止,所以对这两个操作成功的顺序并没有严格的要求(删除pod sandbox的操作由kubelet gc去完成)。

    // pkg/kubelet/dockershim/docker_sandbox.go
    func (ds *dockerService) StopPodSandbox(ctx context.Context, r *runtimeapi.StopPodSandboxRequest) (*runtimeapi.StopPodSandboxResponse, error) {
        ...
        // WARNING: The following operations made the following assumption:
        // 1. kubelet will retry on any error returned by StopPodSandbox.
        // 2. tearing down network and stopping sandbox container can succeed in any sequence.
        // This depends on the implementation detail of network plugin and proper error handling.
        // For kubenet, if tearing down network failed and sandbox container is stopped, kubelet
        // will retry. On retry, kubenet will not be able to retrieve network namespace of the sandbox
        // since it is stopped. With empty network namespcae, CNI bridge plugin will conduct best
        // effort clean up and will not return error.
        errList := []error{}
        ready, ok := ds.getNetworkReady(podSandboxID)
        if !hostNetwork && (ready || !ok) {
            // Only tear down the pod network if we haven't done so already
            cID := kubecontainer.BuildContainerID(runtimeName, podSandboxID)
            err := ds.network.TearDownPod(namespace, name, cID)
            if err == nil {
                ds.setNetworkReady(podSandboxID, false)
            } else {
                errList = append(errList, err)
            }
        }
        if err := ds.client.StopContainer(podSandboxID, defaultSandboxGracePeriod); err != nil {
            // Do not return error if the container does not exist
            if !libdocker.IsContainerNotFoundError(err) {
                klog.Errorf("Failed to stop sandbox %q: %v", podSandboxID, err)
                errList = append(errList, err)
            } else {
                // remove the checkpoint for any sandbox that is not found in the runtime
                ds.checkpointManager.RemoveCheckpoint(podSandboxID)
            }
        }
        ...
    }
    

    在PluginManager.TearDownPod方法中可以看到,调用了pm.plugin.TearDownPod,前面介绍cni初始化的时候讲过相关赋值初始化操作,这里会调用到cniNetworkPlugin的TearDownPod方法。

    // pkg/kubelet/dockershim/network/plugins.go
    func (pm *PluginManager) TearDownPod(podNamespace, podName string, id kubecontainer.ContainerID) error {
        defer recordOperation("tear_down_pod", time.Now())
        fullPodName := kubecontainer.BuildPodFullName(podName, podNamespace)
        pm.podLock(fullPodName).Lock()
        defer pm.podUnlock(fullPodName)
    
        klog.V(3).Infof("Calling network plugin %s to tear down pod %q", pm.plugin.Name(), fullPodName)
        if err := pm.plugin.TearDownPod(podNamespace, podName, id); err != nil {
            return fmt.Errorf("networkPlugin %s failed to teardown pod %q network: %v", pm.plugin.Name(), fullPodName, err)
        }
    
        return nil
    }
    

    cniNetworkPlugin.TearDownPod

    cniNetworkPlugin.TearDownPod方法作用cni网络插件销毁pod网络的调用入口。其主要逻辑为:
    (1)调用plugin.checkInitialized():检查网络插件是否已经初始化完成;
    (2)调用plugin.host.GetNetNS():获取容器网络命名空间路径,格式/proc/${容器PID}/ns/net;
    (3)调用context.WithTimeout():设置调用cni网络插件的超时时间;
    (3)调用plugin.deleteFromNetwork():如果是linux环境,则调用cni网络插件,销毁pod的回环网络;
    (4)调用plugin.deleteFromNetwork():调用cni网络插件,销毁pod的默认网络。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) TearDownPod(namespace string, name string, id kubecontainer.ContainerID) error {
        if err := plugin.checkInitialized(); err != nil {
            return err
        }
    
        // Lack of namespace should not be fatal on teardown
        netnsPath, err := plugin.host.GetNetNS(id.ID)
        if err != nil {
            klog.Warningf("CNI failed to retrieve network namespace path: %v", err)
        }
    
        // Todo get the timeout from parent ctx
        cniTimeoutCtx, cancelFunc := context.WithTimeout(context.Background(), network.CNITimeoutSec*time.Second)
        defer cancelFunc()
        // Windows doesn't have loNetwork. It comes only with Linux
        if plugin.loNetwork != nil {
            // Loopback network deletion failure should not be fatal on teardown
            if err := plugin.deleteFromNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, netnsPath, nil); err != nil {
                klog.Warningf("CNI failed to delete loopback network: %v", err)
            }
        }
    
        return plugin.deleteFromNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, nil)
    }
    

    plugin.deleteFromNetwork
    plugin.deleteFromNetwork方法的作用就是调用cni网络插件,销毁pod指定类型的网络,其主要逻辑为:
    (1)调用plugin.buildCNIRuntimeConf():构建调用cni网络插件的配置;
    (2)调用cniNet.DelNetworkList():调用cni网络插件,进行pod网络销毁。

    // pkg/kubelet/dockershim/network/cni/cni.go
    func (plugin *cniNetworkPlugin) deleteFromNetwork(ctx context.Context, network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations map[string]string) error {
        rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, annotations, nil)
        if err != nil {
            klog.Errorf("Error deleting network when building cni runtime conf: %v", err)
            return err
        }
    
        pdesc := podDesc(podNamespace, podName, podSandboxID)
        netConf, cniNet := network.NetworkConfig, network.CNIConfig
        klog.V(4).Infof("Deleting %s from network %s/%s netns %q", pdesc, netConf.Plugins[0].Network.Type, netConf.Name, podNetnsPath)
        err = cniNet.DelNetworkList(ctx, 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
        }
        klog.V(4).Infof("Deleted %s from network %s/%s", pdesc, netConf.Plugins[0].Network.Type, netConf.Name)
        return nil
    }
    

    cniNet.DelNetworkList

    DelNetworkList方法中主要是调用了addNetwork方法,所以来看下addNetwork方法的逻辑:
    (1)调用c.exec.FindInPath():拼接出cni网络插件可执行文件的绝对路径;
    (2)调用buildOneConfig():构建配置;
    (3)调用c.args():构建调用cni网络插件的参数;
    (4)调用invoke.ExecPluginWithResult():调用cni网络插件进行pod网络的销毁操作。

    // vendor/github.com/containernetworking/cni/libcni/api.go 
    // DelNetworkList executes a sequence of plugins with the DEL command
    func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) error {
        var cachedResult types.Result
    
        // Cached result on DEL was added in CNI spec version 0.4.0 and higher
        if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
            return err
        } else if gtet {
            cachedResult, err = getCachedResult(list.Name, list.CNIVersion, rt)
            if err != nil {
                return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err)
            }
        }
    
        for i := len(list.Plugins) - 1; i >= 0; i-- {
            net := list.Plugins[i]
            if err := c.delNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
                return err
            }
        }
        _ = delCachedResult(list.Name, rt)
    
        return nil
    }
    
    func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
        c.ensureExec()
        pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
        if err != nil {
            return err
        }
    
        newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
        if err != nil {
            return err
        }
    
        return invoke.ExecPluginWithoutResult(ctx, pluginPath, newConf.Bytes, c.args("DEL", rt), c.exec)
    }
    

    c.args

    c.args方法作用是构建调用cni网络插件可执行文件时的参数。

    从代码中可以看出,参数有Command(命令,Add代表构建网络,Del代表销毁网络)、ContainerID(容器ID)、NetNS(容器网络命名空间路径)、IfName(Interface Name即网络接口名称)、PluginArgs(其他参数如pod名称、pod命名空间等)等。

    // vendor/github.com/containernetworking/cni/libcni/api.go
    func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
        return &invoke.Args{
            Command:     action,
            ContainerID: rt.ContainerID,
            NetNS:       rt.NetNS,
            PluginArgs:  rt.Args,
            IfName:      rt.IfName,
            Path:        strings.Join(c.Path, string(os.PathListSeparator)),
        }
    }
    

    invoke.ExecPluginWithResult
    invoke.ExecPluginWithResult主要是将调用参数变成env,然后调用cni网络插件可执行文件,并获取返回结果。

    func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
        if exec == nil {
            exec = defaultExec
        }
    
        stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
        if err != nil {
            return nil, err
        }
    
        // Plugin must return result in same version as specified in netconf
        versionDecoder := &version.ConfigDecoder{}
        confVersion, err := versionDecoder.Decode(netconf)
        if err != nil {
            return nil, err
        }
    
        return version.NewResult(confVersion, stdoutBytes)
    }
    

    总结

    CNI,全称是 Container Network Interface,即容器网络接口。

    CNI是K8s 中标准的调用网络实现的接口。Kubelet 通过这个标准的接口来调用不同的网络插件以实现不同的网络配置方式。

    CNI网络插件是一个可执行文件,是遵守容器网络接口(CNI)规范的网络插件。常见的 CNI网络插件包括 Calico、flannel、Terway、Weave Net等。

    当kubelet选择使用CNI类型的网络插件时(通过kubelet启动参数指定),kubelet在创建pod、删除pod的时候,通过CRI调用CNI网络插件来做pod的构建网络和销毁网络等操作。

    相关文章

      网友评论

        本文标题:K8S CNI

        本文链接:https://www.haomeiwen.com/subject/ohxchrtx.html