Docker是基于一个虚拟网桥设备docker0
来实现宿主机上容器通信的,而k8s本身不提供网络方案,只提供cni接口,维护了一个单独的网桥(cni0)来代替 docker0,由遵从cni的第三方网络插件提供。
在使用cni插件时,一旦使用了k8s创建Pod,那么会产生一个CNI网桥来接管所有CNI插件负责的Pod,而我们单纯使用docker run命令创建的容器,还是由docker0负责。
Kubernetes 之所以要设置这样一个与 docker0 网桥功能几乎一样的 CNI 网桥,主要原因包括两个方面:
- Kubernetes 项目并没有使用 Docker 的网络模型(CNM),所以它并不希望、也不具备配置 docker0 网桥的能力;
- 这还与 Kubernetes 如何配置 Pod,也就是 Infra 容器的 Network Namespace密切相关。
CNI的设计思想是k8s在启动容器之后,就可以直接调用CNI网络插件,为这个容器的Network Namespace配置符合预期的网络栈。
CNI工作流程
在部署 Kubernetes 的时候,有一个步骤是安装 kubernetes-cni 包,它的目的就是在宿主机上安装CNI 插件所需的基础可执行文件。
在安装完成后,可以在宿主机的 /opt/cni/bin 目录下看到它们:
1 | [root@test1 ~]# ls /opt/cni/bin/ |
可以看到flannel就在里面,k8s仓库中保留着flannel的实现。
这些 CNI 的基础可执行文件,按照功能可以分为三类:
- 第一类,叫作 Main 插件,它是用来创建具体网络设备的二进制文件。比如,bridge(网桥设备)、ipvlan、loopback(lo 设备)、macvlan、ptp(Veth Pair 设备),以及 vlan。
- 第二类,叫作 IPAM(IP Address Management)插件,它是负责分配 IP 地址的二进制文件。比如,dhcp,这个文件会向 DHCP 服务器发起请求;host-local,则会使用预先配置的 IP地址段来进行分配。
- 第三类,是由 CNI 社区维护的内置 CNI 插件。比如:flannel,就是专门为 Flannel 项目提供的CNI 插件;tuning,是一个通过 sysctl 调整网络设备参数的二进制文件;portmap,是一个通过 iptables 配置端口映射的二进制文件;bandwidth,是一个使用 Token Bucket Filter (TBF)来进行限流的二进制文件。
下面以flannel为例说明:
首先,实现这个网络方案本身。这一部分需要编写的,其实就是 flanneld 进程里的主要逻辑。
比如,创建和配置 flannel.1 设备、配置宿主机路由、配置 ARP 和 FDB 表里的信息等等。然后,实现该网络方案对应的 CNI 插件。这一部分主要需要做的,就是配置 Infra 容器里面的网络栈,并把它连接在 CNI 网桥上。
由于 Flannel 项目对应的 CNI 插件已经被内置了,所以它无需再单独安装。而对于 Weave、Calico 等其他项目来说,我们就必须在安装插件的时候,把对应的 CNI 插件的可执行文件放在 /opt/cni/bin/ 目录下。实际上,对于 Weave、Calico 这样的网络方案来说,它们的 DaemonSet 只需要挂载宿主机的 /opt/cni/bin/,就可以实现插件可执行文件的安装了。
接下来,就需要在宿主机上安装 flanneld(网络方案本身)。而在这个过程中,flanneld 启动后会在每台宿主机上生成它对应的CNI 配置文件(它其实是一个 ConfigMap),从而告诉Kubernetes,这个集群要使用 Flannel 作为容器网络方案。
这个 CNI 配置文件的内容如下所示:
1 | [root@test1 ~]# cat /etc/cni/net.d/10-flannel.conflist |
需要注意,Kubernetes 目前不支持多个 CNI 插件混用。如果在 CNI 配置目录(/etc/cni/net.d)里放置了多个 CNI 配置文件的话,dockershim 只会加载按字母顺序排序的第一个插件。
但另一方面,CNI 允许你在一个 CNI 配置文件里,通过 plugins 字段,定义多个插件进行协作。
比如,在我们上面这个例子里,Flannel 项目就指定了 flannel 和 portmap 这两个插件。
下面是目前使用的flannel的服务文件,结合这个配置文件可以更好理解:
1 | apiVersion: apps/v1 |
可以看出,flannel 清单中创建了一个configMap,其内包含两个json文件,其中cni-conf.json,这个configmap会被flannel的init-container挂载,挂载完毕后会执行cp命令到/etc/cni/net下面。
在 Kubernetes 中,处理容器网络相关的逻辑并不会在 kubelet 主干代码里执行,而是会在具体的 CRI(Container Runtime Interface,容器运行时接口)实现里完成。对于 Docker 项目来说,它的 CRI 实现叫作 dockershim.
CRI网络处理流程:
当 kubelet 组件需要创建 Pod 的时候,它第一个创建的一定是 Infra 容器。所以在这一步,dockershim 就会先调用 Docker API 创建并启动 Infra 容器,紧接着执行一个叫作 SetUpPod的方法。这个方法的作用就是:为 CNI 插件准备参数,然后调用 CNI 插件为 Infra 容器配置网络。
这里要调用的 CNI 插件,就是
/opt/cni/bin/flannel
;而调用它所需要的参数,分为两部分。
一部分是一组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 插件所以有了这两部分参数,flannel插件就可以实现具体的网络栈配置了;在Flannel 的 CNI 配置文件(
/etc/cni/net.d/10-flannel.conflist
)里有这么一个字段,叫作 delegate
1 | { |
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 字段如下所示:
1 | { |
其中,ipam 字段里的信息,比如 10.244.1.0/24,读取自 Flannel 在宿主机上生成的 Flannel配置文件,即:宿主机上的 /run/flannel/subnet.env
文件。
- 接下来,Flannel CNI 插件就会调用 CNI bridge 插件,也就是执行:/opt/cni/bin/bridge 二进制文件。
这一次,调用 CNI bridge 插件需要的两部分参数的第一部分、也就是 CNI 环境变量,并没有变化。所以,它里面的 CNI_COMMAND 参数的值还是“ADD”。而第二部分 Network Configration,正是上面补充好的 Delegate 字段。Flannel CNI 插件会把 Delegate 字段的内容以标准输入(stdin)的方式传递给 CNI bridge 插件。 - 最后,flannel通过代理bridge完成将容器插入的CNI网络,如果没有CNI网桥,则会创建一个,这也是为什么如果集群为空的时候,其实宿主机看不到cni0网桥,创建了第一个pod后才会出现;而上面hairpinMode参数意思是 发夹模式,默认情况下,网桥设备不允许一个数据包从一个端口进来,再从一个端口出去,开启了这个模式则是取消这个限制,因为我们要允许在 NAT模式下,容器自己访问自己:比如将宿主机的8080端口映射到容器的80端口,完全可能在容器内访问宿主机的8080端口来访问自己,这样就成了报文从容器出去,又会原路返回,所以要打开这个限制。
基本的流程图如下:
Flannel vxlan在k8s中的实现
原理图:
Cni0:网桥设备,每创建一个 Pod 都会创建一对 Veth Pair。其中一端是 Pod 中的 eth0,另一端是 cni0 网桥中的端口(网卡)。Pod 中从网卡 eth0 发出的流量都会发送到 cni0 网桥设备的端口(网卡)上。
注:cni0 设备获得的 IP 地址是该节点分配到的网段的第一个地址。
Flannel.1:Overlay 网络的设备,用来进行 Vxlan 报文的处理(封包和解包)。不同 Node 之间的 Pod 数据流量都从 Overlay 设备以隧道的形式发送到对端。
Flanneld:Flannel 在每个主机中运行 Flanneld 作为 Agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段 Subnet,本主机内所有容器的 IP 地址都将从中分配。
同时 Flanneld 监听 K8S 集群数据库(etcd),为 Flannel.1 设备提供封装数据时必要的 Mac,IP 等网络数据信息。
跨主 Pod 的通信流程:
- Pod 中产生数据,根据 Pod 的路由信息,将数据发送到 cni0。
- cni0 根据节点的路由表,将数据发送到隧道设备 Flannel.1。
- Flannel.1 查看数据包的目的 IP,从 Flanneld 获得对端隧道设备的必要信息,封装数据包。
内层封装:Flannel.1 为 Vxlan 隧道端点,当数据包来到 Flannel.1 时,需要将数据包封装起来。此时:
源 IP src ip 为 源pod_ip。目的 IP dst ip 为目标pod_ip。
数据包继续封装需要知道目的pod_IP 地址对应的 Mac 地址。此时,Flannel.1 会将请求发送到用户空间的 Flanned 程序。Flanned 程序收到内核的请求事件之后,从 etcd 查找能够匹配该地址的子网的 Flannel.1 设备的 Mac 地址,即发往的 Pod 所在 Host 中 Flannel.1 设备的 Mac 地址。通过上面的介绍我们可知Flannel 在为 Node 节点分配 POD_IP 网段时记录了所有的网段和 Mac 等信息。
Flanned 将查询到的信息放入 Master 节点的 ARP Cache 表中:可以通过arp -n来查看。外层封装:此时内层封装已经准备好,需要找到 Vxlan 的外层封装。Kernel 需要查看 Node 上的 FDB(forwarding database)以获得内层封包中目的 Vtep 设备所在的 Node 地址。
因为已经从 ARP Table 中查到目的设备(flannel.1) Mac 地址,同时在 FDB 中存在该 Mac 地址对应的 Node 节点的 IP 地址。
如果 FDB 中没有这个信息,那么 Kernel 会向用户空间的 Flanned 程序发起“L2 MISS”事件。Flanneld 收到该事件后,会查询 etcd,获取该 Vtep 设备对应的 Node 的“Public IP”,并将信息注册到 FDB 中。
当内核获得了发往机器的 IP 地址后,ARP 得到 Mac 地址,之后就能完成 Vxlan 的外层封装。
- Flannel.1 将数据包发送到对端设备。节点的网卡接收到数据包,发现数据包为 Overlay 数据包,解开外层封装,并发送内层封装到 Flannel.1 设备。
- Flannel.1 设备查看数据包,根据路由表匹配,将数据发送给 cni0 设备。
- cni0 匹配路由表,发送数据给网桥上对应的端口。
一些实现细节
kubernetes 会对每个 node 去标识一個名 PodCIDR 的值,代表该 Node 可以使用的网段。flanneld 的 Pod 会去读取该信息,并且记录在/run/flannel/subnet.env
文件中。
flannel CNI 收到任何创建pod请求时,会去读取/run/flannel/subnet.env
文件,并且将其內容转换,发给 host-local 这个 IPAM CNI,来取得可以用的ip设定在pod身上。一旦当 host-local 处理结束后,就会在/var/run/cni/cbr0/networks
看到一系列由 host-local 所维护的正在使用 IP 清单,这些IP清单内容记录着对应pod infra容器的container id。
在创建容器时,Flannel CNI 插件还会把 Delegate 字段以 JSON 文件的方式,保存在/var/lib/cni/flannel 目录下。这是为了给后面删除容器调用 DEL 操作时使用的。
1 | [root@test1 CompassProject]# kubectl describe nodes|grep -w PodCIDR |
参考文章
《Kubernetes网络模型与CNI网络插件》 张磊