跨主机容器间的通信 中简单介绍了容器跨主机网络的问题,本节就是深入详细说明这个问题。

首先从CoreOS公司推出的Flannel项目说起,这是一个容器网络方案,本身只是一种框架。其实现在后端,有三种实现。

  1. VXLAN
  2. host-gw
  3. UDP

这三种方式,也是三种容器跨主网络的主流实现方法。

从最简单的UDP模式开始,讲解容器“跨主网络”的实现原理。

UDP模式

首先搭建一个环境。需要两台主机。每个宿主机上都运行有容器

  • Node1上有一个容器container-1。ip地址为100.96.1.2,docker0网桥为100.96.1.1/24。
  • Node2上有一个容器container-2,ip地址是100.96.2.3,docker0网桥为100.96.2.1/24。

现在是container-1要访问container-2。因此IP包的源地址为100.96.1.2,目的地址为100.96.2.3。目的地址并不在node1的docker0网桥的网段里,所以会交给默认路由规则,通过容器的网关进入宿主机。Flannel已经创建出了一系列的路由规则,Node1示例如下:

# 在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 docker0  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

可以看到docker0的网段不匹配,会进入到名为flannel0的设备当中。这是一个TUN设备。

Linux中,TUN设备是一种工作在第三层的虚拟网络设备。TUN设备的功能非常简单,即:在操作系统内核与用户应用程序之间传递IP包

就是说,发送给flannle0设备的IP包,会交给创建这个设备的应用程序,也就是Flannel进程。同样,Flannel进程发送给flannel0设备的IP包,会出现在宿主机的网络栈中,然后根据宿主机的路由表进行下一步处理。

这个过程总结一下就是:container-1发送的IP包经过docker0出现在宿主机,然后根据路由表进入flannel0设备,宿主机上的flannel进程就会收到这个IP包,然后根据这个IP包的目的地址,将它转发给Node2宿主机。

那么现在一个关键的问题来了:

flanneld是如何知道IP地址对应的容器,是运行在Node2上的?

这就用到了Flannel项目里的一个非常重要的概念,子网(subnet)

==在Flannel管理的容器网络里,一台宿主机上的所有容器都属于该宿主机被分配的一个子网==。Node1的子网是100.96.1.0/24,container-1的地址是100.96.1.2,node2的子网是100.96.2.0/24,container-2的地址是100.96.2.3。这些子网与宿主机的对应关系,保存在Etcd中。

$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/100.96.1.0-24
/coreos.com/network/subnets/100.96.2.0-24
/coreos.com/network/subnets/100.96.3.0-24

所以flannel进程处理flannel0传入的IP包时,就可以根据目的IP地址匹配到对应的子网,从Etcd中找到这个子网对应的宿主机的IP地址

$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"10.168.0.3"}

这个过程有点像隧道技术,就是flannel收到conainter-1发送给container-2的IP包之后,目的IP地址解析出container-2所在主机的IP地址,然后将整个IP数据包封装到一个UDP包里。这个UDP包的源IP是Node1的IP,目的IP是Node2的IP。而两台机器上都运行着flannel进程,接受到这个UDP包之后拆开获得两个容器的IP地址。接下来flannel进程只需要把这个IP包发送给它所管理的TUN设备即flannel0设备即可。

上述流程要正确工作还有一个重要的前提,那就是 docker0 网桥的地址范围必须是 Flannel 为宿主机分配的子网。这个很容易实现,以 Node 1 为例,你只需要给它上面的 Docker Daemon 启动时配置如下所示的 bip 参数即可:

$ FLANNEL_SUBNET=100.96.1.1/24
$ dockerd --bip=$FLANNEL_SUBNET ...

基于Flannel的UDP模式因为其严重的性能问题已经被废弃,因为单单只是发送IP包的过程就需要经过三次用户态和内核态之间的数据拷贝,如下图

  • 第一次:用户态的容器发出的IP包经过docker0网桥进入内核态。
  • 第二次:IP包根据路由表进入TUN(flannel0)设备,从而回到用户态的flanneld进程。
  • 第三次:flanneld进行UDP封包之后重新进入内核态。将UDP包通过宿主机的eth0发出去。 我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行

这就是为什么VXLAN成为了主流的容器网络方案的原因。

VXLAN模式

VXLAN,即Virtual Extensible LAN(虚拟可扩展局域网),是Linux内核本身就支持一种网络虚拟化技术。VXLAN可以完全在内核态实现上述封装和解封装的工作,从而构建出覆盖网络(Overlay Network)。

VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。

而为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。这个设备的作用类似于前面的flanneld进程,但是它进行封装和解封的不是三层的IP包,而是二层的数据帧。并且整个工作的执行流程都在内核内完成

基于VTEP设备的通信流程如下图:

从图中可以看到每台主机都有名为flannel.1的设备,这就是VXLAN所需的VTEP设备,它既有IP地址也有MAC地址。

container-1发出的IP包会先出现再docker0网桥,然后路由到本机的flannel.1设备进行处理。为了能够将原始IP包发送到正确的宿主机,VXLAN就需要找到这条隧道的出口,即:目的宿主机的VTEP设备。这个设备的信息就是由每台宿主机上的flanneld进程负责维护的。

Node2启动并加入Flannel网络之后,在Node1(所有其它节点)上,flanneld就会添加一条如下所示的路由规则

$ 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

这条规则的意思是:凡是发往 10.1.16.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,并且,它最后被发往的网关地址是:10.1.16.0。

VTEP设备之间组成一个虚拟二层网络,通过二层的数据帧进行通信。所以源VTEP设备需要对源IP包加上一个目的MAC地址,组成一个二层数据帧,然后发送给目的VTEP设备。所以需要知道目的VTEP设备的MAC地址

MAC地址要通过ARP,这里的ARP记录,也是flanneld进程在Node2节点启动后,自动添加在Node1上的

# 在Node 1上
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT

现在可以了解到,Flanneld会在每台节点启动的时候将它的VTEP设备对应的ARP记录,直接下放到其它每台宿主机上

那么源VTEP设备将目的VTEP设备的MAC地址加到原始IP包的前面即可,这是由Linux内核完成的。

但是这个封装VTEP设备MAC地址的的数据帧对于宿主机来说并没有意义,把它叫做内部数据帧,宿主机之间想要通过是需要通过宿主机的eth0网卡的信息进行传输的。因此需要封装成宿主机可用的外部数据帧,这个数据帧内部装载着内部数据帧。

Linux会在内部数据帧前添加一个VXLAN头来表示这是一个VXLAN要使用的数据帧。这个头里有一个标志VNI,这是VTEP设备识别某个数据帧是否由自己处理的重要标识。然后Linux内核再将这个数据帧封装到一个UDP包中发送出去

在这种场景中,flannel.1设备实际上扮演一个网桥的角色,在二层网络进行UDP包的转发。而在Linux内核里,网桥设备进行转发的依据,来自一个叫做FDB(Forwarding Database)的转发数据库。这个数据库的信息也是由flanneld进程负责维护的。通过这个信息能够找到封装的UDP包要发送的节点的信息。因为最终还是需要通过两个宿主机之间的网络进行数据交换的,而FDB中就记录了VTEP设备的MAC地址与其宿主机IP的信息。

# 在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

简述一下这个过程。每当由节点加入flannel网络,就会在其它节点加入到此VTEP的路由规则和MAC。然后Node1中的容器于Node2中的容器通信的时候,就会发送到VTEP设备。 这是作为一个二层设备网桥使用的,于是加上目的VTEP设备的MAC地址形成内部数据帧。这个帧需要封装成UDP包,为了标识内部数据帧是VXLAN要使用的数据帧,就需要在封装成UDP包前加上一个VXLAN头。

最后UDP是一个4层传输层包,需要加上IP头封装成IP包,在加上目的主机MAC地址封装成数据帧最后发送出去。 封装的结构如下:

接收到这个数据帧的Node2的内核网络栈进行拆包,发现了VXLANhi后,会根据包中的VNI的值,交给对应的flannel.1设备,而flannel.1进一步拆包得到原址IP包,最终到达了容器的Network Namespace中。

还是隧道机制,但是整个封装过程全部在内核状态完成。


tags: 容器网络