Kubernetes CNI具体流程和Flannel原理探究

根据张磊的《深入剖析kubernetes》中可以了解到k8s CNI模式的主要工作流程,加上近期老版本测试环境发现的flannel ip分配冲突问题,个人觉得有必要整理一下CNI工作原理以及Flannel IP 分配流程,供需要的朋友在出现类似问题时进行问题定位和原理理解。

本文不会特别详细的回顾docker网络基础和k8s网络基础,因此适合在实际中使用过docker和k8s的看官阅读~

众所周知,k8s通过cni网络模型来规定集群内网络实现的基本流程和模式,使用各种网络插件来完成具体的容器网络配置,从而完成集群内部的网络打通(本人近似的理解为Java中的模板模式,cni定义了流程和模板)

而Flannel是k8s中最容易上手的一个网络组件,并且其代码就直接位于k8s的仓库中,所以这就很容易理解,为何我们安装完k8s之后,在 /opt/cni/bin下面就可以找到flannel的二进制文件

一、Kubernetes网络模型

本部分根据张磊对CNI模型的讲述做了一定的归纳和总结。

从Docker网络模型出发,实现一个物理机上所有Docker容器的网络互通通常是基于一个虚拟网桥设备docker0,这个docker0设备既充当网桥,同时又是一个具有ip的网关设备,做为Docker容器的网关。

而通过 route -n 命令,也可以看到原生的docker0会有一条默认的路由规则,从而打通容器和外界之间的网络。

而k8s中的网络插件真正要做的事情,也是通过某种方法,将所有的容器通过特殊设备联通(可以是网桥,可以是路由规则等等,根据不同插件实现而异),从而达到容器跨主机通信的目的。

需要注意的是,在使用flannel时,一旦使用了k8s创建Pod,那么会产生一个CNI网桥来接管所有CNI插件负责的Pod,而我们单纯使用docker run命令创建的容器,还是由docker0负责。

张磊提到:k8s之所以不直接使用docker0的原因,因为是k8s项目认为Docker网络模型(CMM)不够优秀,所以其不想采用,所以也就不想去也不能够直接配置Docker网桥;再一个就是CNI模型与k8s如何配置 infra容器的Network Namespace有关

在此明确一下 /opt/cni/bin/flannel 这是个二进制文件,是CNI网络插件的一种,同样的还有calico,weave等,这些flannel二进制文件负责配置Infra容器的网络栈,并将其插入到cni0网桥

flannel本身还应该有一个名为flanneld的进程,这个进程可能会根据我们使用的不同backend,来进行一些操作,比如创建VETP设备等

所以,CNI的设计思想,就是k8s在启动infra容器之后,就可以直接调用CNI网络插件,为这个Infra容器的Network Namespace配置符合预期的网络栈。

二、CNI的工作流程

使用过kubeadm安装k8s集群的朋友应该了解,有一个步骤是安装kubernetes-cni这个包,其完成了在宿主机安装CNI插件所需的可执行文件。

$ ls -al /opt/cni/bin/
total 73088
-rwxr-xr-x 1 root root  3890407 Aug 17  2017 bridge
-rwxr-xr-x 1 root root  9921982 Aug 17  2017 dhcp
-rwxr-xr-x 1 root root  2814104 Aug 17  2017 flannel
-rwxr-xr-x 1 root root  2991965 Aug 17  2017 host-local
-rwxr-xr-x 1 root root  3475802 Aug 17  2017 ipvlan
-rwxr-xr-x 1 root root  3026388 Aug 17  2017 loopback
-rwxr-xr-x 1 root root  3520724 Aug 17  2017 macvlan
-rwxr-xr-x 1 root root  3470464 Aug 17  2017 portmap
-rwxr-xr-x 1 root root  3877986 Aug 17  2017 ptp
-rwxr-xr-x 1 root root  2605279 Aug 17  2017 sample
-rwxr-xr-x 1 root root  2808402 Aug 17  2017 tuning
-rwxr-xr-x 1 root root  3475750 Aug 17  2017 vlan

这些 CNI 的基础可执行文件,按照功能可以分为三类:

  • 第一类,叫作 Main 插件,它是用来创建具体网络设备的二进制文件。比如,bridge(网桥设备)、ipvlan、loopback(lo 设备)、macvlan、ptp(Veth Pair 设备),以及 vlan。我在前面提到过的 Flannel、Weave 等项目,都属于“网桥”类型的 CNI 插件。所以在具体的实现中,它们往往会调用 bridge 这个二进制文件。
  • 第二类,叫作 IPAM(IP Address Management)插件,它是负责分配 IP 地址的二进制文件。比如,dhcp,这个文件会向 DHCP 服务器发起请求;host-local,则会使用预先配置的 IP 地址段来进行分配。
  • 第三类,是由 CNI 社区维护的内置 CNI 插件。比如:flannel,就是专门为 Flannel 项目提供的 CNI 插件;tuning,是一个通过 sysctl 调整网络设备参数的二进制文件;portmap,是一个通过 iptables 配置端口映射的二进制文件;bandwidth,是一个使用 Token Bucket Filter (TBF) 来进行限流的二进制文件。
  • 从这些二进制文件中,我们可以看到,如果要实现一个给 Kubernetes 用的容器网络方案,其实需要做两部分工作,以 Flannel 项目为例:首先,实现这个网络方案本身。这一部分需要编写的,其实就是 flanneld 进程里的主要逻辑。比如,创建和配置 flannel.1 设备、配置宿主机路由、配置 ARP 和 FDB 表里的信息等等。然后,实现该网络方案对应的 CNI 插件。这一部分主要需要做的,就是配置 Infra 容器里面的网络栈,并把它连接在 CNI 网桥上。
  • 由于 Flannel 项目对应的 CNI 插件已经被内置了,所以它无需再单独安装。而对于 Weave、Calico 等其他项目来说,我们就必须在安装插件的时候,把对应的 CNI 插件的可执行文件放在 /opt/cni/bin/ 目录下。实际上,对于 Weave、Calico 这样的网络方案来说,它们的 DaemonSet 只需要挂载宿主机的 /opt/cni/bin/,就可以实现插件可执行文件的安装了。

而我们安装flanneld这个进程后,其启动后会在每个宿主机上生成对应的CNI配置文件(同时也是一个ConfigMap存在ETCD中),从而让k8s可以进行读取。

$ cat /etc/cni/net.d/10-flannel.conflist 
{
  "name": "cbr0",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

下面贴出来我们使用过的一个flannel的清单文件,结合这个配置文件进行理解:

---
kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "ipMasq": false,
      "plugins": [
        {
          "type": "flannel",
          "ipMasq": false,
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }
---
apiVersion: extensions/v1beta1
kind: DaemonSet
spec:
    spec:
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.10.0-amd64
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.10.0-amd64
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq=false
        - --kube-subnet-mgr
        - --iface=ens192
        volumeMounts:
        - mountPath: /etc/localtime
          name: flannel-server-time
        - name: run
          mountPath: /run
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: flannel-server-time
          hostPath:
            path: /etc/localtime
        - name: run
          hostPath:
            path: /run
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg

可以看出,flannel 清单中创建了一个configMap,其内包含两个json文件,其中cni-conf.json,就是上图的/etc/cni/net/10-flannel.conflist**

这个configmap会被flannel的init-container挂载,挂载完毕后会执行cp命令到/etc/cni/net下面。

下面是kubelet如何通过CRI(容器运行时接口)来完成网络处理的一个简单流程:

  1. 通常我们使用docker,其CRI实现叫做dockershim,也就是说,我们如果要通过k8s创建一个docker容器,kubelet会调用dockershim来完成相关工作
  2. dockershim要运行一个容器并配置网络的话,则要加载cni配置文件,也就是/etc/cni/net.d/10-flannel.conflist,这个文件flannel的init-container从configMap中拷贝到pod内部的存储中的。
  3. 所以dockershim加载了这个文件之后,会依次调用flannel和portMap这两个可执行文件,而flannel内部则通过代理,代理给其他插件来完成具体工作(见下文)

而kubelet创建pod,最终完成网络配置的详细流程,张磊在讲述这一部分的时候较为详细,但是不太好懂,我个人总结了一下:

  1. kubelet需要创建pod的时候,肯定优先创建Infra容器,所以这一步,dockershim 调用Docker API创建一个Infra容器,然后执行SetUpPod方法,为CNI插件准备参数,并且调用CNI插件完成配置网络栈,这里以flannel为例,则调用的肯定就是 /opt/cni/bin/flannel

  2. 为CNI插件准备的参数分为两部分,一部分是一组CNI环境变量,用于定义当前的操作(ADD或者DEL,也就是添加一个veth pair或者拆除一个veth pair),根据这两个操作类型,flannel则会对应的实现两个方法,从而实现ADD和DEL流程;另一部分则是从上文中提到的 CNI配置文件中拿到的一些配置信息(被flannel从configMap挂载到了 宿主机的 /etc/cni/net.d/10-flannel.conflist),组装成json格式的Network COnfiguration 通过 stdin 传递给 flannel 插件

  3. 所以有了这两部分参数,flannel插件就可以实现具体的网络栈配置了;但是我们可以发现,在Flannel 的 CNI 配置文件( /etc/cni/net.d/10-flannel.conflist)里有这么一个字段,叫作 delegate

    ...
         "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
    

    Delegate 字段的意思是,这个 CNI 插件并不会自己做事儿,而是会调用 Delegate 指定的某种 CNI 内置插件来完成。对于 Flannel 来说,它调用的 Delegate 插件,就是前面介绍到的 CNI bridge 插件**。所以说,dockershim 对 Flannel CNI 插件的调用,其实就是走了个过场。Flannel CNI 插件唯一需要做的,就是对 dockershim 传来的 Network Configuration 进行补充**。比如,将 Delegate 的 Type 字段设置为 bridge,将 Delegate 的 IPAM 字段设置为 host-local 等。经过 Flannel CNI 插件补充后的、完整的 Delegate 字段如下所示

    {
        "hairpinMode":true,
        "ipMasq":false,
        "ipam":{
            "routes":[
                {
                    "dst":"10.244.0.0/16"
                }
            ],
            "subnet":"10.244.1.0/24",
            "type":"host-local"
        },
        "isDefaultGateway":true,
        "isGateway":true,
        "mtu":1410,
        "name":"cbr0",
        "type":"bridge"
    }
    
  4. 上面提到过,CNI中会预置一些插件,比如bridge,比如host-local等,而flannel完成的事情,则是调用这些插件,来完成将pod插入到网桥,并且给pod分配ip;而每个宿主机都会有不同的ip地址段,这个配置信息则是flanneld在宿主机生成的一个配置文件/run/flannel/subnet.env,而集群内所有主机的地址段都存在etcd中,所以每个宿主机都可以有不同的ip段

  5. 最后,flannel通过代理bridge完成将容器插入的CNI网络,如果没有CNI网桥,则会创建一个,这也是为什么如果集群为空的时候,其实宿主机看不到cni0网桥,创建了第一个pod后才会出现;而上面hairpinMode参数意思是 发夹模式,默认情况下,网桥设备不允许一个数据包从一个端口进来,再从一个端口出去,开启了这个模式则是取消这个限制,因为我们要允许在 NAT模式下,容器自己访问自己:比如将宿主机的8080端口映射到容器的80端口,完全可能在容器内访问宿主机的8080端口来访问自己,这样就成了报文从容器出去,又会原路返回,所以要打开这个限制

至此,其实flannel为容器分配ip的流程算是大体理清楚了

三、flannel 进阶原理

之前踩过一个坑,由于在删除节点时删除了一些flannel 保存的ip历史数据,导致flannel再次分配ip时会发生ip冲突

阅读了一位台湾博主的博客后,对其进行了摘抄和整理,由于时间关系,繁体字这里就不做转换了,不影响观看~

承接张磊讲的cni流程,现扩展一下 flannel的进阶原理:

结论是:

  1. kubernetes 會針對每個 node 去標示一個名為 PodCIDR 的值,代表該 Node 可以使用的網段是什麼,
  2. flanneld 的 Pod 會去讀取該資訊,並且將該資訊寫道 /run/flannel/subnet.env 的這個檔案中
  3. flannel CNI 收到任何創建 Pod 的請求時,會去讀取 /run/flannel/subnet.env 的資訊,並且將其內容轉換最後呼叫 host-local 這隻 IPAM CNI,來取得可以用的 IP 並且設定到 POD 身上

执行命令验证:

$ kubectl describe nodes | grep PodCIDR
PodCIDR:                     10.244.0.0/24
PodCIDR:                     10.244.1.0/24
PodCIDR:                     10.244.2.0/24

$ sudo cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

$ sudo ls /var/lib/cni/networks/cbr0
10.244.0.2  10.244.0.3  10.244.0.8  last_reserved_ip.0  lock

$ sudo cat /var/lib/cni/networks/cbr0/10.244.0.8
2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b

$ sudo docker ps  --no-trunc | grep $(sudo cat /var/lib/cni/networks/cbr0/10.244.0.8)
2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b   k8s.gcr.io/pause:3.1 "/pause" Up 4 hours  k8s_POD_k8s-udpserver-6576555bcb-7h8jh_default_87196597-ccda-4643-ac5d-85343a3b6c90_0
  1. 透過 kubectl describer node 可以觀察到每個節點上都有一個 PodCIDR 的欄位,代表的是該節點可以使用的網段

  2. 由於我的節點是對應到的 PodCIDR10.244.0.0/24,接下來去觀察 /run/flannel/subnet.env,確認裡面的數值一致。

  3. 接下來由於我的系統上有跑過一些 Pod這些Pod 形成的過程中會呼叫 flannel CNI 來處理,而該 CNI 最後會再輾轉呼叫 host-loacl IPAM CNI 來處理,所以就會在這邊看到有 host-local 的產物

  4. 由於前篇介紹 IPAM 的文章有介紹過 host-local 的運作,該檔案的內容則是對應的 **CONTAINER_ID**,因此這邊得到的也是 CONTAINER_ID

  5. 最後則是透過 docker 指令去尋該 CONTAINER_ID,最後就看到對應到的不是真正運行的 Pod,而是先前介紹過的 Infrastructure Contaienr: Pause

    當我們透過 –pod-network-cidr 去初始化 kubeadm** 後,其創造出來的 controller-manager 就會自帶三個參數

    • –cluster-cidr=10.244.0.0/16
    • –allocate-node-cidrs=true
    • –node-cidr-mask-size=24

    這邊就標明的整個 cluster network 會使用的網段,除了 cidr 大網段之外還透過 node-cide–mask 去標示寫網段,所以根據上述的範例,這個節點的數量不能超過255台節點,不然就沒有足够的 可用網段去分配了。

    此外很有趣的一點是,這邊的逻辑在 controller-managfer 代码中叫做 nodeipam,也就是kubernetes 自己跳下來负责做 IPAM 的工作,幫忙分配 IP/Subnet只是單位是以 Node 為基準,不是以 Pod。

    也就是说 k8s 自己先预先给 每个node 分配 pod cidr的一个subnet

    最终Controller-Manager 会通过一定逻辑,产生一个CidrSet的 结构体

    这个结构体描述的信息,就跟subnet文件中所有参数基本一致

    type CidrSet struct {
      sync.Mutex
      clusterCIDR     *net.IPNet
      clusterIP       net.IP
      clusterMaskSize int
      maxCIDRs        int
      nextCandidate   int
      used            big.Int
      subNetMaskSize  int
    }
    

    4、一旦當 host-local 處理結束後,就會再 /var/run/cni/cbr0/networks 看到一系列由 host-local 所維護的正在使用 IP 清單。

    3、最终调用 ipam-hostLocal时,传了一个字典进去,里面定义了所有host-local所用到的参数,flannel最後其實是產生一個使用 bridge 作為主體 CNI 且 IPAM 使用 host-local 的設定檔案

    补充完毕delegate字段后,调用其他的代理插件,也印证了张磊说的flannel只是个过客

    所以,flanneld这个Pod完成的工作就是从 k8s的api中读取podCidr信息,并写入**/run/flannel/subnet.env**

    而flannel的代码分为两部分存放:

    1、CoreOS-Pod (flanneld的代码)

    2、ContainetNetworking-CNI(flannel CNI的代码)

    所以这也解释了为什么k8s一旦被装好,/opt/cni/bin下就有一个flannel的二进制文件了,因为代码本身就在k8s的repo中

你可能感兴趣的:(容器,kubernetes,kubeadm)