在上一篇文章中,讲解了在单机环境下,Linux 容器网络的实现原理(网桥模式)。并且提到了,在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。
正是为了解决这个容器“跨主通信”的问题,社区里才出现了那么多的容器网络方案。而要理解容器“跨主通信”的原理,就一定要先从 Flannel 这个项目说起。
Flannel
Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:
- VXLAN
- host-gw
- UDP
UDP模式
UDP 模式是 Flannel 项目最早支持的一种方式,却也是性能最差的一种方式,常用于调试环境或者不支持VXLAN协议网络环境中。所以,这个模式目前已经被弃用。不过,Flannel 之所以最先选择 UDP 模式,就是因为这种模式是最直接、也是最容易理解的容器跨主网络实现。
如上图所示,两台宿主机:
- 宿主机 Node 1 上有一个容器 container-1,它的 IP 地址是 100.96.1.2,对应的 docker0 网桥的地址是:100.96.1.1/24。
- 宿主机 Node 2 上有一个容器 container-2,它的 IP 地址是 100.96.2.3,对应的 docker0 网桥的地址是:100.96.2.1/24。
container-1容器里的进程发起IP包 ==> 通过容器的网关进入 docker0 网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上 ==> 通过 Flannel 在Node1上预先创建的路由规则进入 flannel0 设备 ==> flannel0 设备将IP包交给创建这个设备的应用程序,即flanneld进程(从内核态流向用户态) ==> flanneld通过查找保存在Etcd中的子网与宿主机对应关系,找到这个子网对应的宿主机IP地址 ==> flanneld 将IP包封装到一个UDP包中,发送给Node2。
其中:
- flannel0 是一个 TUN 设备(Tunnel 设备)。在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个 IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。反之,如果 Flannel 进程向 flannel0 设备发送了一个 IP 包,那么这个 IP 包就会出现在宿主机网络栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向。
- 在由 Flannel 管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”(subnets)。在上述例子中,Node 1 的子网是 100.96.1.0/24,container-1 的 IP 地址是 100.96.1.2。Node 2 的子网是 100.96.2.0/24,container-2 的 IP 地址是 100.96.2.3。而这些子网与宿主机的对应关系,正是保存在 Etcd 当中。
- 每台宿主机上的 flanneld,都监听着一个 8285 端口,所以 Node1 的 flanneld 只要把 UDP 包发往 Node 2 的 8285 端口即可,Node 2 上监听 8285 端口的进程也是 flanneld。
- 上述流程要正确工作还有一个重要的前提,那就是 docker0 网桥的地址范围必须是 Flannel 为宿主机分配的子网。
可以看到,Flannel UDP 模式提供的其实是一个三层的 Overlay 网络(几层其实就是看封装的是几层数据包,UDP内部封装的是IP包,是没有Mac信息的。 也可以认为flannel是一个纯3层的设备),即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。
实际上,相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程,由于使用到了 flannel0 这个 TUN 设备,仅在发出 IP 包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下所示:
第一次,用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
第二次,IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
第三次,flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。
此外,我们还可以看到,Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。
所以说,在进行系统级编程的时候,有一个非常重要的优化原则:要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行。这也是为什么,Flannel 后来支持的 VXLAN 模式,逐渐成为了主流的容器网络方案的原因。
VXLAN模式
VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。
VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。而为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。而 VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为 VXLAN 本身就是 Linux 内核中的一个模块)。
基于VTEP设备进行“隧道”通信的流程,如下所示:
图中每台宿主机上都有flannel.1设备,它就是VXLAN的VTEP设备,既有IP地址,也有MAC地址。
container-1发送原始IP包,目的地址为10.1.16.3 ==> docker0 网桥收到该包,将其路由到本机的flannel.1设备 ==> flanneld 进程在所有节点上维护了所有VTEP设备的路由信息,在该网段(10.1.16.0/24)的IP包,都由flannel.1设备发出,并且目的VTEP设备的IP地址是10.1.16.0 ==> 通过查询ARP记录找到目的VTEP设备的MAC地址(也是由flanneld进程在节点启动时自动添加到所有Node上的)==> Linux内核开始二层封包,Inner Ethernet Header为目的VTEP设备的MAC地址,Inner IP Header仍是container-2的IP地址不变 ==> Linux 内核会在“内部数据帧”前加上一个特殊的 VXLAN 头,用来表示这个帧实际上是一个 VXLAN 要使用的数据帧 ==> Linux内核将数据帧封装为UDP包 ==> flanneld进程负责维护FDB(Forwarding Database)的转发数据库,保存着目的VTEP设备MAC地址与该宿主机IP(10.168.0.3/24)的对应关系 ==> 开始封包,Outer IP Header 是 Node2 的 IP(10.168.0.3/24)==> Outer Ethernet Header 是 Node2 的 MAC 地址(通过ARP查询得知) ==> 完成封包,发送给 Node2 ==> Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header 并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备 ==> flannel.1 设备则会进一步拆包,取出“原始 IP 包” ==> 原始 IP 包进入container-2 容器的 Network Namespace 中。
其中:
- 最新版本的 Flannel 并不依赖 L3 MISS 事件和 ARP 学习,而会在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。
- VXLAN 头里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。
总结
本篇文章介绍了 Flannel UDP 和 VXLAN 模式的工作原理。这两种模式其实都可以称作“隧道”机制,也是很多其他容器网络插件的基础。比如 Weave 的两种模式,以及 Docker 的 Overlay 模式。
从上面的例子可以看到,**VXLAN 模式组建的覆盖网络,其实就是一个由不同宿主机上的 VTEP 设备,也就是 flannel.1 设备组成的虚拟二层网络。对于 VTEP 设备来说,它发出的“内部数据帧”就仿佛是一直在这个虚拟的二层网络上流动。这也正是覆盖网络的含义。 **