Flannel 是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,这三种后端实现代表了容器跨主机网络的主流实现方法。
- VXLAN。
- host-gw。
- UDP。是 Flannel 项目最早支持的一种方式,因为这种模式是最直接、也是最容易理解的容器跨主机网络实现,却也是性能最差的一种方式,这个模式在「2018-11」已经被弃用。
UDP(Overlay,已废弃)
Flannel UDP 是一种 Overlay 类型的网络。Flannel UDP 模式的跨主通信的基本原理如下:
-
假设现在有两台宿主机
-
宿主机 Node 1 上有一个容器 container-1,它的 IP 地址是 100.96.1.2,对应的 cni0 网桥的地址是100.96.1.1/24。
-
宿主机 Node 2 上有一个容器 container-2,它的 IP 地址是 100.96.2.3,对应的 cni0 网桥的地址是100.96.2.1/24。
-
-
container1 容器里发出的 IP 包,其源地址是 100.96.1.2,目的地址是 100.96.2.3。然而,这个目的地址并不在 Node1 的 cni0 网桥的网段内,所以这个数据包会使用容器的默认规则出现在宿主机上,也就是出现在宿主机网络的 IP 层。
-
Flannel 会在宿主机上创建一系列的路由规则,以 node1 为例,如下所示。由于 IP 包的目的地址是 100.96.2.3,它匹配的是第二条、也就是 100.96.0.0/16 对应的这条路由规则,从而进入到一个叫作 flannel0 的设备中。
# 在Node 1上 $ ip route default via 10.168.0.1 dev eth0 100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0 100.96.1.0/24 dev cni0 proto kernel scope link src 100.96.1.1 10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.2
-
Flannel 项目会将一台宿主机上的所有容器都会被划分到该宿主机被分配的一个子网中。在我们的例子中,Node1 被分配的子网是 100.96.1.0/24,container1 的 IP 地址是 100.96.1.2。Node2 被分配的子网是 100.96.2.0/24,container2 的 IP 地址是100.96.2.3。而这些子网和宿主机的对应关系,都保存在 Etcd 当中。
当 IP 进入到 flannel0 设备后,宿主机上的 flanneld 进程,就会收到这个 IP 包。flanneld 进程首先根据 IP 包的目的 IP 地址(100.96.2.3)匹配到相应的子网(100.96.2.0/24),之后从 Etcd 中找到这个子网对应的宿主机的 IP 地址是 10.168.0.3。
flannel0 设备的类型是一个 TUN 设备(Tunnel 设备),在 Linux 中 TUN 设备是一种工作在三层的(network layer)的虚拟网络设备。TUN 设备的功能非常简单,就是在操作系统内核和用户应用程序之间传递 IP 包。
-
当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个 IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。
-
如果 Flannel 进程向 flannel0 设备发送了一个 IP 包,那么这个 IP 包就会出现在宿主机网络栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向。
-
-
之后 flanneld 会把这个 IP 包封装在一个 UDP 包里,最终这个 UDP 包的源地址是 flanneld 所在的 node1 的地址,而目的地址则是 container2 所在的宿主机 Node2 的地址,而目的端口则是 8285(每台宿主机上的 flanneld,都监听着一个 8285 端口)。
-
数据包最终来到了 Node2 节点,由于该数据包的目的端口是 8285,因此该数据包最终会交给 flanneld。当 node2 上的 flanneld 收到 UDP 数据包之后,会从这个数据包中解析出封装在里面的、container1 发送的原 IP 包。之后 flanneld 会直接把这个 IP 包发送给它所管理的 TUN 设备,即 flannel0 设备。
-
把这个 IP 包发送给 flannel0 设备之后,IP 包就会出现在宿主机网络栈中。之后通过本机的路由表来寻找这个 IP 包的下一步流向,Node2 上的路由表跟 Node1 非常相似。由于这个 IP 数据包的目的地址是 100.96.2.3,它跟第三条,也就是 cni0 那条对应的路由规则匹配更加准确,所以 Linux 内核就会按照这条路由规则,把这个 IP 包转发给 cni0 网桥。
# 在Node 2上 $ ip route default via 10.168.0.1 dev eth0 100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.2.0 100.96.2.0/24 dev cni0 proto kernel scope link src 100.96.2.1 10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.3
-
之后 cni0 网桥就会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过 Veth Pair 设备进入到 container2 的 Network Namespace。
-
而 container2 返回给 container1 的数据包,则会经过与上述过程相反的路径回到 container1 中。需要注意的是,cni0 网桥的地址范围必须是 Flannel 为宿主机分配的子网。
总结
Flannel UDP 主要利用了 TUN 设备(可以和用户态应用程序直接交互),从实现上来看主要分为两部分:
- 一部分是 flanneld 进程维护的路由。这些路由是 Linux 内核协议栈转发 IP 数据包需要的。
- 另一部分是 flanneld 进程需要对 IP 数据包进行封装、解封装的逻辑。TUN 设备给 flanneld 进程的是 IP 数据包。
那么,最终 Flannel UDP 模式是基于已有的宿主机三层网络构建了一个三层 Overlay 网络,因为它封装和解封装的都是 IP 包。如此一来,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。
但是,UDP 模式存在严重的性能问题。相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程,由于使用到了 flannel0 这个 TUN 设备,仅在发出 IP 包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,
- 第一次,用户态的容器进程发出的数据包进入内核态,变成 IP 包;
- 第二次,IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
- 第三次,flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。
此外,Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。上述的用户态和内核态的上下文切换,以及用户态操作的代价都是非常高,最终造成 Flannel UDP 模式性能不好。
VXLAN(Overlay)
Flannel VXLAN 基于 Linux VXLAN 实现,是一种 Overlay 类型的网络。
VXLAN,全称 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。VXLAN 覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP(VXLAN Tunnel End Point,虚拟隧道端点)。VTEP 设备的作用是封装和解封装二层数据帧(Ethernet frame),这些工作均在内核里完成的。
基于 VTEP 设备进行“隧道”通信的流程,如下所示:
-
每台宿主机上都有一个名叫 flannel.1 的设备,这个就是 VXLAN 需要的 VTEP 设备,它既有 IP 地址,也有 Mac 地址。
-
当 container-1 发出给 Node2 上 container-2 的数据包后,由于 container-2 的 IP 地址是 10.1.16.3 不跟 container-1 处于同一网段,因此这个数据包会发送给默认网关,也就是 cni0,数据包也因此来到了宿主机网络。
-
在 Node2 启动之后,Node1(以及所有其他节点)上的 flanneld 进程会添加如下的一条路由:凡是发往 10.1.16.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,并且,它最后被发往的网关地址是:10.1.16.0。同时 Node1(以及所有其他节点)上的 flanneld 进程还会添加如下 ARP 记录:IP 地址 10.1.16.0,对应的 MAC 地址是 5e:f8:4f:00:e3:37(Flannel 并不依赖 ARP 学习,它会在每台节点启动时把该节点的 VTEP 设备对应的 ARP 记录,发给其他宿主机)。
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface ... 10.1.16.0 10.1.16.0 255.255.255.0 UG 0 0 0 flannel.1
# 在Node 1上 $ ip neigh show dev flannel.1 10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
因此 container-1 的数据包来到宿主机网络之后,通过宿主机上 IP 层的路由规则先将该数据包(IP 包)转发给了 flannel.1 设备。
-
flannel.1 设备根据路由和 ARP 记录,由于该数据包需要发送给网关 10.1.16.0,因此它会添加该网关的 MAC 地址,也就是 Node2 节点上 VTEP 设备的 MAC 地址,最终封装出来的二层帧内容如下所示(注意 flannel.1 设备相当于只负责添加目的 VTEP 设备的 MAC 地址),
-
上述封装的二层帧的内容会进一步被封装,最终被封装成为宿主机网络里面的一个普通数据帧,通过宿主机 eth0 网卡进行传输。而上述二层帧的内容,在这一步就相当于应用层的数据包而已。
-
首先 VXLAN 模块加上一个特殊的 VXLAN 头,表示该数据帧是 VXLAN 要使用的数据帧。VXLAN 头里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。
-
接着 VXLAN 模块将上述内容封装到一个 UDP 包中。
-
接着 Linux 内核给它添加上一个 IP 头,组成一个 IP 包。这个 IP 头里填上目的主机的 IP 地址,也就是 Node2 的 IP 地址 10.168.0.3(Linux 内核获取到这个数据包是 VXLAN 数据包之后,会使用 Linux 内核中 VXLAN 模块的代码获取到 IP 地址)。
那这个 IP 地址是怎么知道的呢?flanneld 进程会维护自己的 FDB 转发数据库,里面记录了 flannel.1 设备 MAC 地址所在的 Node 节点 IP 地址,如下所示。该条记录表明,发往“目的 VTEP 设备”(MAC 地址是 5e:f8:4f:00:e3:37)的二层数据帧,最终要往 IP 地址为 10.168.0.3 的主机发送。
# 在Node 1上,使用“目的VTEP设备”的MAC地址进行查询 $ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37 5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent
-
之后 Linux 内核会在这个 IP 包前面再加上二层数据帧,也就是把 Node2 的 MAC 地址填进去。而这个 MAC 地址是 Node1 自己通过 ARP 学习到的。最终的数据帧格式如下所示:
-
-
上述的数据帧会通过网络来到 Node2 的 eth0。
- Node2 的内核协议栈会对该数据帧逐层拆包,最终来到 UDP 传输层。
- UDP 传输层发现目的端口号跟 VXLAN 模块配置的端口号匹配,就会将该数据包发给 VXLAN 模块。
- VXLAN 模块解析 VXLAN 头部,提取 VNI,同时将原始的以太网帧从 VXLAN 数据包中提取出来。根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备。
- flannel.1 设备会校验 MAC 地址,然后进一步拆包,取出“原始 IP 包”,通过宿主机网络路由将这个包发送给 cni0 网桥,从而最终交给 container-2 容器。
总结
Flannel VXLAN 方式主要利用了 Linux 内核的 VXLAN 能力,从实现上来看主要分为两部分:
- 一部分是需要 flanneld 进程维护的路由、ARP 记录表、 FDB 数据表等。这些是 Linux VXLAN 能力需要的内容。
- 另一部分是创建 VTEP 设备。之后 VTEP 设备就会根据上述维护的信息完成相应的 VXLAN 能力。
那么,与 Flannle UDP 模式相比,主要有以下两点的区别:
-
封装解封装的对象。Flannel VXLAN 模式中,封装的内容是一个二层数据帧,最终解封装后出来的内容也是一个二层数据帧,也就说在 VXLAN 中传递的是一个二层数据帧。Flannel UDP 模式中,封装的内容是一个 IP 数据包,解封出来的也是一个 IP 数据包。
-
封装解封装的处理位置。Flannel VXALN 模式中,由于 VXLAN 是 Linux 内核模块的一个能力,因此它封装和解封装是在内核中。Flannel UDP 模式中,封装和解封装是在用户态进程。
因此,Flannel VXLAN 由于减少了用户态和内核态之间的切换次数,性能会更好,使其成为了一个主流的容器网络方案。
系统级编程的时候,一个非常重要的优化原则就是减少用户态到内核态的切换次数。
其他
Flannel 分配的子网范围可以在部署的时候指定,也可以在部署完成后,通过修改 kube-controller-manager 的配置文件来指定。如下所以,
$ kubeadm init --pod-network-cidr=10.244.0.0/16
host-gw(Underlay)
Flannel host-gw 通过在宿主机网络中添加路由规则实现(需要宿主机之间二层连通),是一种 Underlay 类型的网络。
Flannel host-gw 的跨主通信的基本原理如下:
-
假设 Node 1 上的 Infra-container-1,要访问 Node 2 上的 Infra-container-2。
-
infra-container-1 发送的数据包会经由 cni0 来到宿主机网络。
-
Flannel 使用 host-gw 模式之后,flanneld 会在宿主机上创建这样一条路由规则。以 Node 1 为例,这条路由规则的含义是:目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去,并且它下一跳地址是 10.168.0.3,也就是 Node2 eth0 的地址。下一跳的意思是,如果一个 IP 包要从主机 A 发到主机 B,需要经过路由设备 X 的中转。那么对于这个数据包来说下一跳地址就是 X 的 IP 地址。此时这个 IP 包封装的数据帧的目的 MAC 地址是设备 X 的 MAC 地址,间接要求了主机 A 和设备 X 需要是二层互通的。
$ ip route ... 10.244.1.0/24 via 10.168.0.3 dev eth0
因此,Node1 会根据上述配置的规则,将数据帧的目的 MAC 地址配置为下一跳地址对应的 MAC 地址,也就是 Node2 eth0 的 MAC 地址。
-
Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是 10.244.1.3,即 Infra-container-2 的 IP 地址。这时候,会根据 Node 2 上的路由表,将该数据包发送给 cni0 网桥,进而继续发送给 Infra-container-2。
总结
Flannel host-gw 主要通过添加路由实现,从实现上来说,
- 只需要维护各主机上的路由即可。具体实现上,Flannel host-gw 模式中子网和主机的信息,都会保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,实时更新路由表即可。注意:在 Kubernetes v1.7 之后,类似 Flannel、Calico 的 CNI 网络插件都是可以直接连接 Kubernetes 的 APIServer 来访问 Etcd 的,无需额外部署 Etcd 给它们使用。
需要注意的是,
-
Flannel host-gw 模式的隐性要求是 Kubernetes 集群中各宿主机之间是二层连通的,因为这种模式能够正常工作的核心,就在于 IP 包在封装成帧发送出去的时候,会使用路由表里的“下一跳”来设置目的 MAC 地址,而这下一跳就得是宿主机。
在 Kubernetes 集群中节点之间二层不连通的情况也是存在的。比如宿主机分布在了不同的子网(VLAN)里。Kubernetes 集群中宿主机之间只要求可以通过 IP 地址进行通信,也就是说至少是三层可达的,并没有要求宿主机之间二层网络需要互通。
相比其他所有基于 VXLAN“隧道”机制的网络方案来说,
- host-gw 模式免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右。