容器的网络解决方案有很多种,每支持一种网络实现就进行一次适配显然是不现实的,而 CNI 就是为了兼容多种网络方案而发明的。CNI 是 Container Network Interface 的缩写,是一个标准的通用的接口,用于连接容器管理系统和网络插件。
简单来说,容器 runtime 为容器提供 network namespace,网络插件负责将 network interface 插入该 network namespace 中并且在宿主机做一些必要的配置,最后对 namespace 中的 interface 进行 IP 和路由的配置。
所以网络插件的主要工作就在于为容器提供网络环境,包括为 pod 设置 ip 地址、配置路由保证集群内网络的通畅。目前比较流行的网络插件是 Flannel 和 Calico。
Flannel
Flannel 主要提供的是集群内的 Overlay 网络,支持三种后端实现,分别是:UDP 模式、VXLAN 模式、host-gw 模式。
UDP 模式
UDP 模式,是 Flannel 项目最早支持的一种方式,却也是性能最差的一种方式。这种模式提供的是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。工作原理如下图所示。
node1 上的 pod1 请求 node2 上的 pod2 时,流量的走向如下:
- pod1 里的进程发起请求,发出 IP 包;
- IP 包根据 pod1 里的 veth 设备对,进入到 cni0 网桥;
- 由于 IP 包的目的 ip 不在 node1 上,根据 flannel 在节点上创建出来的路由规则,进入到 flannel0 中;
- 此时 flanneld 进程会收到这个包,flanneld 判断该包应该在哪台 node 上,然后将其封装在一个 UDP 包中;
- 最后通过 node1 上的网关,发送给 node2;
flannel0 是一个 TUN 设备(Tunnel 设备)。在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能:在操作系统内核和用户应用程序之间传递 IP 包。
可以看到,这种模式性能差的原因在于,整个包的 UDP 封装过程是 flanneld 程序做的,也就是用户态,而这就带来了一次内核态向用户态的转换,以及一次用户态向内核态的转换。在上下文切换和用户态操作的代价其实是比较高的,而 UDP 模式因为封包拆包带来了额外的性能消耗。
VXLAN 模式
VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。通过利用 Linux 内核的这种特性,也可以实现在内核态的封装和解封装的能力,从而构建出覆盖网络。其工作原理如下图所示:
VXLAN 模式的 flannel 会在节点上创建一个叫 flannel.1 的 VTEP (VXLAN Tunnel End Point,虚拟隧道端点) 设备,跟 UDP 模式一样,该设备将二层数据帧封装在 UDP 包里,再转发出去,而与 UDP 模式不一样的是,整个封装的过程是在内核态完成的。
node1 上的 pod1 请求 node2 上的 pod2 时,流量的走向如下:
- pod1 里的进程发起请求,发出 IP 包;
- IP 包根据 pod1 里的 veth 设备对,进入到 cni0 网桥;
- 由于 IP 包的目的 ip 不在 node1 上,根据 flannel 在节点上创建出来的路由规则,进入到 flannel.1 中;
- flannel.1 将原始 IP 包加上一个目的 MAC 地址,封装成一个二层数据帧;然后内核将数据帧封装进一个 UDP 包里;
- 最后通过 node1 上的网关,发送给 node2;
抓包验证
在 node1 上部署一个 nginx pod1,node2 上部署一个 nginx pod2。然后在 pod1 的容器中 curl pod2 容器的 80 端口。
集群网络环境如下:
node1 网卡 ens33:192.168.50.10
pod1 veth:10.244.0.7
node1 cni0:10.244.0.1/24
node1 flannel.1:10.244.0.0/32
node2 网卡 ens33:192.168.50.11
pod2 veth:10.244.1.9
node2 cni0:10.244.1.1/24
node2 flannel.1:10.244.1.0/32
node1 上的路由信息如下:
➜ ~ ip route
default via 192.168.50.1 dev ens33
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.50.0/24 dev ens33 proto kernel scope link src 192.168.50.10 metric 100
node1 的网卡 ens33 的抓包情况:
只能看到源 ip 为 node1 ip、目的 ip 为 node2 ip 的 UDP 包。由于 flannel.1 进行了一层 UDP 封包,这里我们在 Wireshark 中设置一下将 UDP 包解析为 VxLAN 格式(端口为 8472),设置过程为 Analyze->Decode As:
然后再来看一下 node1 网卡上收到的包:
可以看到源 ip 为 pod1 ip、目的 ip 为 pod2 ip,并且该 IP 包被封装在 UDP 包中。
host-gw 模式
最后一种 host-gw 模式是一种纯三层网络方案。其工作原理为将每个 Flannel 子网的“下一跳”设置成了该子网对应的宿主机的 IP 地址,这台主机会充当这条容器通信路径里的“网关”。这样 IP 包就能通过二层网络达到目的主机,而正是因为这一点,host-gw 模式要求集群宿主机之间的网络是二层连通的,如下图所示。
宿主机上的路由信息是 flanneld 设置的,因为 flannel 子网和主机的信息保存在 etcd 中,所以 flanneld 只需要 watch 这些数据的变化,实时更新路由表即可。在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。
node1 上的 pod1 请求 node2 上的 pod2 时,流量的走向如下:
- pod1 里的进程发起请求,发出 IP 包,从网络层进入链路层封装成帧;
- 根据主机上的路由规则,数据帧从 Node 1 通过宿主机的二层网络到达 Node 2 上;
Calico
Calico 没有使用 CNI 的网桥模式,而是将节点当成边界路由器,组成了一个全连通的网络,通过 BGP 协议交换路由。所以,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。
Calico 的组件:
- CNI 插件:Calico 与 Kubernetes 对接的部分
- Felix:负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
- BIRD (BGP Route Reflector):是 BGP 的客户端,专门负责在集群里分发路由规则信息。
三个组件都是通过一个 DaemonSet 安装的。CNI 插件是通过 initContainer 安装的;而 Felix 和 BIRD 是同一个 pod 的两个 container。
工作原理
Calico 采用的 BGP,就是在大规模网络中实现节点路由信息共享的一种协议。全称是 Border Gateway Protocol,即:边界网关协议。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的 “自治系统” 之间路由信息的、无中心的路由协议。
由于没有使用 CNI 的网桥,Calico 的 CNI 插件需要为每个容器设置一个 Veth Pair 设备,然后把其中的一端放置在宿主机上,还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。如下图所示:
可以使用 calicoctl 查看 node1 的节点连接情况:
~ calicoctl get no
NAME
node1
node2
node3
~ calicoctl node status
Calico process is running.
IPv4 BGP status
+---------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+---------------+-------------------+-------+------------+-------------+
| 192.168.50.11 | node-to-node mesh | up | 2020-09-28 | Established |
| 192.168.50.12 | node-to-node mesh | up | 2020-09-28 | Established |
+---------------+-------------------+-------+------------+-------------+
IPv6 BGP status
No IPv6 peers found.
可以看到整个 calico 集群上有 3 个节点,node1 和另外两个节点处于连接状态,模式为 “Node-to-Node Mesh”。再看下 node1 上的路由信息如下:
~ ip route
default via 192.168.50.1 dev ens33 proto static metric 100
10.244.104.0/26 via 192.168.50.11 dev ens33 proto bird
10.244.135.0/26 via 192.168.50.12 dev ens33 proto bird
10.244.166.128 dev cali717821d73f3 scope link
blackhole 10.244.166.128/26 proto bird
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.50.0/24 dev ens33 proto kernel scope link src 192.168.50.10 metric 100
其中,第 2 条的路由规则表明 10.244.104.0/26 网段的数据包通过 bird 协议由 ens33 设备发往网关 192.168.50.11。这也就定义了目的 ip 为 node2 上 pod 请求的走向。第 3 条路由规则与之类似。
抓包验证
与上面一样,从 node1 上的 pod1 发送一个 http 请求到 node2 上的 pod2。
集群网络环境如下:
node1 网卡 ens33:192.168.50.10
pod1 ip:10.244.166.128
node2 网卡 ens33:192.168.50.11
pod2 ip:10.244.104.1
node1 的网卡 ens33 的抓包情况:
IPIP 模式
IPIP 模式为了解决两个 node 不在一个子网的问题。只要将名为 calico-node 的 daemonset 的环境变量 CALICO_IPV4POOL_IPIP 设置为 "Always" 即可。如下:
- name: CALICO_IPV4POOL_IPIP
value: "Off"
IPIP 模式的 calico 使用了 tunl0 设备,这是一个 IP 隧道设备。IP 包进入 tunl0 后,内核会将原始 IP 包直接封装在宿主机的 IP 包中;封装后的 IP 包的目的地址为下一跳地址,即 node2 的 IP 地址。由于宿主机之间已经使用路由器配置了三层转发,所以这个 IP 包在离开 node 1 之后,就可以经过路由器,最终发送到 node 2 上。如下图所示。
由于 IPIP 模式的 Calico 额外多出了封包和拆包的过程,集群的网络性能受到了影响,所以在集群的二层网络通的情况下,建议不要使用 IPIP 模式。
看下 node1 上的路由信息:
~ ip route
default via 192.168.50.1 dev ens33 proto static metric 100
10.244.104.0/26 via 192.168.50.11 dev tunl0 proto bird onlink
10.244.135.0/26 via 192.168.50.12 dev tunl0 proto bird onlink
blackhole 10.244.166.128/26 proto bird
10.244.166.129 dev calif3c799362a5 scope link
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.50.0/24 dev ens33 proto kernel scope link src 192.168.50.10 metric 100
可以看到,与之前不一样的是,目的 IP 为 node2 上的 Pod 的数据包是经由 tunl0 发送到网关 192.168.50.11。
抓包验证
从 node1 上的 pod1 发送一个 http 请求到 node2 上的 pod2。
集群网络环境如下:
node1 网卡 ens33:192.168.50.10
node1 tunl0:10.244.166.128/32
pod1 ip:10.244.166.129
node2 网卡 ens33:192.168.50.11
pod2 ip:10.244.104.2
node2 tunl0:10.244.104.0/32
tunl0 设备的抓包情况:
node1 网卡 ens33 的抓包情况:
可以看到,IP 包在 tunl0 设备中被封装进了另一个 IP 包,其目的 IP 为 node2 的 IP,源 IP 为 node1 的 IP。
总结
Kubernetes 的集群网络插件实现方案有很多种,本文主要分析了社区比较常见的两种 Flannel 和 Calico 的工作原理,针对集群内不同节点的 pod 间通信的场景,抓包分析了网络包的走向。
Flannel 主要提供了 Overlay 的网络方案,UDP 模式由于其封包拆包的过程涉及了多次上下文的切换,导致性能很差,逐渐被社区抛弃;VXLAN 模式的封包拆包过程均在内核态,性能要比 UDP 好很多,也是最经常使用的模式;host-gw 模式不涉及封包拆包,所以性能相对较高,但要求节点间二层互通。
Calico 主要采用了 BGP 协议交换路由,没有采用 cni0 网桥,当二层网络不通的时候,可以采用 IPIP 模式,但由于涉及到封包拆包的过程,性能相对较弱,与 Flannel 的 VXLAN 模式相当。