在介绍宿主机上的容器网络时,更多是关注如何解决同一主机上容器的相互访问和容器对外暴露服务的问题。但是,并没有涉及怎么解决跨主机的容器之间的互相访问问题。
对于 Kubernetes 来说,除了实现同一主机上 Pod 的互相通信之外,它还要实现跨主机 Pod 之间的互相通信的问题。针对网络问题,Kubernetes 定义了一个合格集群网络的基本要求:
- 所有 Pod 应该可以直接使用 IP 地址与其他 Pod 通信,而无需使用 NAT。
- 所有宿主机都可以直接使用 IP 地址与所有 Pod 通信,而无需使用 NAT。反之亦然。
- Pod 自己“看到”的自己的 IP 地址,和别人(宿主机或者 Pod)看到的地址是完全一样的。
这个要求除了网络互通的基本要求外,还有一个要求就是必须直接基于容器和宿主机的 IP 地址来实现的。为此,Kubernetes 定义了自己的网络模型,并有一套自己的网络标准,叫 CNI(Container Network Interface) 。任何人按照这套接口规范,实现一个 CNI 插件,并部署到 Kubernetes 中即可实现 Kubernetes 中 Pod 的互相访问。总的来说,CNI 插件的最终目的是让 Kubernetes 中的 Pod 实现网络互通,它会根据自己的实现创建相应的 bridge 虚拟网络设备,或者其他虚拟网络设备,然后再配置相应的路由等方式,最终实现 Pod 间的网络互通。
Docker 采用了 CNM(Container Network Model) 网络模型。对于 Docker 来说,任何按照 CNM 网络模型实现了一个驱动的话,就可以应用到 Docker 中实现容器间的通信。CNM 和 CNI 其实本质上并没有区别,它们相当于实现容器网络的两套规范,最终目的都是实现容器的网络互通。所以 CNI 和 CNM 是独立的,不相互依赖,所以使用 CNI 插件的时候,我们会看到 CNI 插件可能并不会用 Docker 创建的那些虚拟设备等。从针对网络模型的实现角度来看的话,CNI 相对于对开发者的约束更少、更开放,不依赖于容器运行时,而 CNM 跟容器运行时绑定严重。
而实现一个 CNI 网络插件只需要一个配置文件和一个可执行文件:
- 配置文件描述 CNI 插件的版本、名称、描述等基本信息。
- 可执行文件会被上层的容器管理平台调用。一个 CNI 可执行文件需要实现将容器加入到网络的 ADD 操作以及将容器从网络中删除的 DEL 操作即可(以及一个可选的 VERSION 查看版本操作)。
CNI 插件创建网络流程
在 Kubernetes 中,CNI 网络插件的基本工作流程如下,
-
Kubelet 组件创建 Pod 的时候,它首先调用 CRI 接口创建并启动 pause 容器(也会创建对应的网络命名空间)。
-
在启动 pause 容器之后,CRI 接口的具体实现(比如 dockershim)中会执行 SetUpPod() 方法。这个方法的主要作用是为 CNI 插件准备参数,并调用 CNI 插件给 pause 容器配置符合预期的网络栈(Pod 中的其他容器都是复用 pause 容器的网络),比如网卡(network interface)、路由表(routing table)和 iptables 规则等,可能还需要涉及宿主机上路由等信息的配置。
-
先为 CNI 插件准备参数。参数分为两部分,
-
dockershim 设置的 CNI 环境变量。其中最重要的环境变量参数叫作:CNI_COMMAND。它的取值只有两种:ADD 和 DEL(ADD 和 DEL 是 CNI 插件唯一需要实现的两个方法)。
-
ADD 操作的含义是:把容器添加到 CNI 网络里。对于网桥类型的 CNI 插件来说,意味着把容器以 Veth Pair 的方式插到 CNI 网桥上。CNI 的 ADD 可能还需要的变量有:
-
容器里网卡的名字 CNI_IFNAME(如 eth0)。
-
Pod 的 Network Namespace 文件的路径(CNI_NETNS,即 /proc/<容器进程的PID>/ns/net)。
-
容器的 ID(CNI_CONTAINERID)等。
-
-
DEL 操作的含义是把容器从 CNI 网络里移除掉。对于网桥类型的 CNI 插件来说,意味着把容易以 Veth Pair 的方式从网桥上“拔掉”。
在 CNI 环境变量里,还有一个叫做 CNI_ARGS 的参数。这个参数用于以 key-value 的格式传递自定义的信息给 CNI 插件,比如 CNI 插件需要额外的变量就可以使用这个参数。
-
-
dockershim 从 CNI 配置文件里加载到的配置信息(这个配置信息在 CNI 中被叫作 Network Configuration,完整定义可查看:https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration)。dockershim 会把 Network Configuration 以 JSON 数据的格式,通过标准输入(stdin)的方式传递给 CNI 插件。
注意:Kubernetes 目前不支持多个 CNI 插件混用,所以如果在
/etc/cni/net.d
(CNI 配置目录)里放置了多个 CNI 配置文件的话,dockershim 只会加载按字母顺序排序的第一个 CNI 配置文件。但是,CNI 允许你在一个 CNI 配置文件里,通过 plugins 字段,定义多个插件进行协作。比如,配置文件中指定 flannel 和 portmap 这两个插件,那么在之后的执行中,flannel 和 portmap 插件会按照定义顺序被调用,从而依次完成配置容器网络和配置端口映射。$ cat /etc/cni/net.d/10-flannel.conflist { "name": "cbr0", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] }
-
-
然后调用 CNI 插件为 pause 容器配置网络(比如调用 /opt/cni/bin/flannel)。
-
从上面可以看到,在 Kubernetes 中,处理容器网络相关的逻辑,比如调用 CNI 插件,其实并不在 kubelet 主干代码里执行,而是会在具体的 CRI(Container Runtime Interface,容器运行时接口)实现里完成。
- 对于 Docker 项目来说,它的 CRI 实现叫作 dockershim,你可以在 kubelet 的代码里找到它。
- 对于 containerd 来说,是在 cri-plugin 里。
主流的 CNI 网络实现方案
开篇的时候,我们曾提到 CNI 通过虚拟设备、iptables 规则、路由等方式,最终实现 Pod 与 Pod 之间互相通信的问题。目前,主流的 CNI 网络实现方案有两种:
-
Overlay:在已有的宿主机网络之上,借助隧道传输技术,比如 VxLAN、ipip 等,构建一层可以将所有容器连通在一起的虚拟网络。比如将容器的数据包封装到原宿主机网络的三层或者四层数据包中,然后使用宿主机网络的 IP 或者 TCP/UDP 传输到目标主机,目标主机拆包后再转发给目标容器。目前使用隧道传输技术的主流 Overlay 容器网络有 Flannel 等。
-
Underlay:不借助隧道传输技术。把容器网络加到宿主机路由表中,把宿主机网络设备当作容器网关,通过路由规则转发到指定的主机,直接实现容器的三层互通。目前通过路由技术实现容器互通的 Underlay 网络方案有 Flannel host-gw、Calico 等。
CNI 插件所需的基础可执行文件
在部署 Kubernetes 的时候,有一个步骤是安装 kubernetes-cni 包,它的目的就是在宿主机上安装 CNI 插件所需的基础可执行文件。这些可执行文件包括(查看 /opt/cni/bin 目录可以看到):
$ 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
按照功能可以分为以下三类:
-
第一类,叫做 Main 插件,它是用来创建具体网络设备的二进制文件。比如,bridge(网桥设备)、ipvlan、loopback(lo设备)、macvlan、ptp(Veth Pari 设备),以及 vlan。
Flannel、Weave 等项目都属于网桥类型的 CNI 插件。所以在具体实现中,它们往往会调用 bridge 这个二进制文件。
-
第二类,叫做 IPAM(IP Address Management)插件,它是负责分配 IP 地址的二进制文件。比如,
- dhcp 会向 DHCP 服务器发起请求;
- host-local 会使用预先配置的 IP 地址段来进行分配。
-
第三类,是由 CNI 社区维护的内置的 CNI 插件,比如
- flannel,这就是专门为 Flannel 项目提供的 CNI 插件;
- tunning,是一个通过 sysctl 调整网络设备参数的二进制文件;
- portmap 是一个通过 iptables 配置端口映射的二进制文件;
- bandwidth 是一个使用 Token Bucket Filter(TBF)来进行限流的二进制文件。
Flannel 项目对应的 CNI 插件已经被内置了,所以不需要再单独安装 CNI 插件(这里的意思是 Flannel 所需要的插件已经在这个安装包中了)。然而,对于 Weave、Calico 等项目来说,这些并没有内置,因此如果需要使用它们的话,则必须要把对应的 CNI 插件的可执行文件放到 /opt/cni/bin 目录中。
Flannel 插件调用流程
-
flanneld 启动后会在每台宿主机上生成对应的 CNI 配置文件,如下所示:
$ cat /etc/cni/net.d/10-flannel.conflist { "name": "cbr0", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] }
-
dockershim 启动 pause 容器之后,在给 pause 容器配置网络的时候,会将上述参数传给 flannel CNI 插件。对于 flannel 说,它会对 dockershim 传来的 Network Configuration 进行补充。比如将 Delegate 的 IPAM 字段设置为如下所示的内容。10.244.1.0/24 等内容读取自 Flannel 在宿主机上生成的 Flannel 配置文件(/run/flannel/subnet.env)。
{ "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" }
-
接下来,Flannel CNI 插件会调用 bridge 插件(Delegate 字段中的 type 字段),也就是执行:/opt/cni/bin/bridge 二进制文件。并且给 bridge 插件传两部分参数,
- 第一个部分,还是 CNI 环境变量没有变化。
- 第二部分 Network Configuration,正好是上面补充的 Delegate 字段。
同时,Flannel CNI 插件还会把 Delegate 字段以 JSON 文件的方式,保存在 /var/lib/cni/flannel 目录下(给后面删除容器调用 DEL 操作时使用)。Delegate 这个字段更多是表明要传给 CNI 插件要调用的另一个 CNI 插件的参数,有这个字段一般表明这个 CNI 插件并不会自己做事儿,而是会调用另外指定的某种内置插件来完成。
-
之后 bridge 插件会在宿主机上检查 CNI 网桥是否存在,如果没有,那就创建。类似下面命令的作用,
# 在宿主机上 $ ip link add cni0 type bridge $ ip link set cni0 up
-
接下来 bridge 插件会通过 pause 容器的 Network Namespace 文件,进入到这个 Network Namespace 中,然后创建一对 Veth Pair 设备。
-
紧接着,它会把这个 Veth Pair 的其中一端,移动到宿主机上。这相当于在容器里执行如下命令,
#在容器里 # 创建一对Veth Pair设备。其中一个叫作eth0,另一个叫作vethb4963f3 $ ip link add eth0 type veth peer name vethb4963f3 # 启动eth0设备 $ ip link set eth0 up # 将Veth Pair设备的另一端(也就是vethb4963f3设备)放到宿主机(也就是Host Namespace)里 $ ip link set vethb4963f3 netns $HOST_NS # 通过Host Namespace,启动宿主机上的vethb4963f3设备 $ ip netns exec $HOST_NS ip link set vethb4963f3 up
经过上述的操作之后,vethb4963f3 就出现在了宿主机上,而这个 Veth Pair 的另一端,就是容器里的 eth0。上述创建 Veth pair 设备的操作,其实在宿主机上也可以执行,然后再把 Veth Pair 的一端放到容器的 Network Space 里,原理是一样的。而之所以这样反着来,是因为 CNI 里对 Namespace 操作函数的设计就是这样反着来的。而这样反着来的原因是因为在编程时,容器的 Namespace 是可以直接通过 Namespace 文件拿到的,而 Host Namespace,则是一个隐含在上下文的参数。所以这样反着来,就是先进入到容器 namespace 里面,然后再反向操作 host namespace,对于编程来说更加方便。
-
接下来,bridge 插件就可以把 vethb4963f3 设备连接在 CNI 网桥上。相当于在宿主机中执行
# 在宿主机上 $ ip link set vethb4963f3 master cni0
-
在将 vethb4963f3 设备连接在 cni0 之后,bridge 插件还会为它设置 Hairpin Mode(发夹模式)(Flannel 插件要在 CNI 配置文件里声明 hairpinMode=true)。这样,将来这个集群里的 Pod 才可以通过它自己的 Service 访问到自己。
默认情况下,网桥设备是不允许一个数据包从一个端口进来后,再从这个端口发出去的,而设置 Hairpin Mode 模式就可以取消这个限制。为什么呢?主要是考虑到容器中通过 NAT (端口映射)的方式,“自己访问自己”的情况。比如执行
docker run -p 8080:80
,就是在宿主机上通过 iptables 设置了一条DNAT(目的地址转换)转发规则。这条规则的作用是,当宿主机上的进程访问“<宿主机的 IP 地址 >:8080”时,iptables 会把该请求直接转发到“<容器的 IP 地址 >:80”上。如果此时在容器里面访问宿主机的 8080 端口,那么这个容器里发出的 IP 包会经过 vethb4963f3 设备(端口)和 docker0 网桥,来到宿主机上。此时,根据上述 DNAT 规则,这个 IP 包又需要回到 docker0 网桥,并且还是通过 vethb4963f3 端口进入到容器里。所以,这种情况下,就需要开启 vethb4963f3 端口的 Hairpin Mode 了。 -
接下来,bridge 插件会调用 ipam 插件,从 ipam.subnet 字段规定的网段里为容器分配一个可用的 IP 地址。然后,bridge 插件就会把这个 IP 地址添加到容器的 eth0 网卡上,同时为容器设置默认路由。相当于在容器中执行:
# 在容器里 $ ip addr add 10.244.0.2/24 dev eth0 $ ip route add default via 10.244.0.1 dev eth0
-
最后 bridge 插件会为 CNI 网桥添加 IP 地址,这相当于在宿主机上执行:
# 在宿主机上 $ ip addr add 10.244.0.1/24 dev cni0
-
在执行完上述操作之后,Flannel 插件会把容器的 IP 地址等信息返回给 dockershim,然后被 kubelet 添加到 Pod 的 Status 字段。
至此,Flannel 插件的 ADD 方法的执行流程结束。总结下,
-
对于网桥类型的 CNI 插件来说,基本是两个步骤,
- 给 pause 容器配置相应的网络栈,比如创建 veth pair,连接到 cni0 bridge 上。
- 网络互通方案的实现,比如创建和配置 flannel.1 设备、配置宿主机路由、配置 ARP 和 FDB 表里的信息等。
-
对于非网桥类型的 CNI 插件来说,上述“将容器添加到 CNI 网络”的操作流程,以及网络方案本身的工作原理会不太一样。
注意:cni0 只是接管由上述 Flannel 插件负责创建的容器。如果此时用 docker run 单独启动一个容器,Docker 项目是把这个容器连接到 docker0 网桥上。
相关链接
-
The Layers of the OSI Model Illustrated:https://www.lifewire.com/layers-of-the-osi-model-illustrated-818017
-
Container Network Interface (CNI) Specification:https://github.com/containernetworking/cni/blob/main/SPEC.md#container-network-interface-cni-specification
-
使用 Go 从零开始实现 CNI:https://morven.life/posts/create-your-own-cni-with-golang/
-
极客时间.张磊.《深入剖析Kubernetes》