跨主机通信,Flannel项目,这是CoreOS公司主推的容器网络方案。Flannel项目本身只是一个框架,真正提供容器网络功能的是Flannel的后端实现,目前Flannel支持三种后端实现:
三种不同的后端实现,代表了三种容器跨主机网络的主流实现方法。
假设有两台宿主机,目标:c1访问c2。
宿主机 | 容器 | IP | docker0网桥地址 |
---|---|---|---|
node1 | c1 | 100.96.1.2 | 100.96.1.1/24 |
node2 | c2 | 100.96.2.3 | 100.96.2.1/24 |
这种情况下,c1容器里的进程发起的IP包,其源地址是
100.96.1.2
,目的地址是100.96.2.3
,由于目的地址100.96.2.3
不在node1的docker0网桥的网段里,所以这个IP包会被交给默认路由规则,通过容器的网关进入docker0网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上。此时,这个IP包的下一个目的地,就取决于宿主机上的路由规则。
此时,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
可以看到,由于IP包的目的地址是100.96.2.3
,它匹配不到本机docker0网桥对应的100.96.1.0/24
,只能匹配到第二条,也就是100.96.0.0/16
对应的这条路由规则,从而进入到一个叫作flannel0的设备中。
flannel0设备的类型是一个TUN设备(Tunnel设备)。在Linux设备中,TUN设备是一个工作在三层(Network Layer)的虚拟网络设备。TUN设备的功能非常简单,即,在操作系统内核和用户应用程序之间传递IP包。
100.96.2.3
,就把它发送给了node2宿主机。在Flannel管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个子网,以上面的例子来说,node1的子网是
100.96.1.0/24
,c1的IP地址是100.96.1.2
,node2的子网是100.96.2.0/24
,c2的IP地址是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
flanneld进程在处理从flannel0传入的IP包时,就可以根据目的IP地址,匹配对应的子网,从Etcd中找到这个子网对应的宿主机IP地址是10.168.0.3
,如下所示:
etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"10.168.0.3"}
对应flanneld来说,只要node1和node2互通,那么flanneld作为node1上的普通进程就可以通过IP地址与node2通信。
通过一个普通的宿主机之间的UDP通信,一个UDP包就从node1到达了node2。
node2上的路由表,也node1上的类似,如下所示:
# 在 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 docker0 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
这个IP包的目的地址是100.96.2.3
,这与第三条(100.96.2.0/24
)网段对应的路由规则匹配更加精确。Linux内核就会按照这条路由规则,把这个IP包转发给docker0网桥。然后docker0网桥扮演二层交换机的角色,将数据包发送给正确的端口,进而通过Veth Pair设备进入到c2的Network Namespace。
c2返回给C1的数据包,通过上述过程相反的路径回到c1。
上述流程要正确工作的一个重要前提,docker0网桥的地址范围必须是Flannel为宿主机分配的子网。以Node1为例,需要给它上面的Docker Daemon启动时配置如下的bip参数。
FLANNEL_SUBNET=100.96.1.1/24
dockerd --bip=$FLANNEL_SUBNET ...
Flannel UDP模式的跨主机通信的基本过程如下图所示:
Flannel UDP提供的是一个三层的Overlay Network,即,首先对发出端的IP包进行UDP封装,然后在接受端进行解封装拿到原始的IP包,进而把这个IP包转发给目标容器。就像Flannel在不同宿主机上的两个容器之间打通了一条隧道,使得两个容器能够直接使用IP地址进行通信,而无需关心容器和宿主机的分布情况。
UDP模式的严重性能问题在于,相比于宿主机直接通信,这种模式多了flanneld的处理过程。这个过程使用TUN,仅仅在发送IP包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下图:
UDP封装(Encapsulation)和解封装(Decapsulation)的过程是在用户态进行的。在Linux操作系统中,上下文的切换和用户态的操作代价比较高,这是UDP模式性能不好的主要原因。
在系统级编程时,非常重要的一个优化原则,减少用户态和内核态的切换次数,并且把核心的处理逻辑放在内核态执行。这也是VXLAN模式成为主流容器网络方案的原因。
Virtual Extensible LAN(虚拟可扩展局域网),是Linux内核本身就支持的一种网络虚拟化技术。所以VXLAN可以完全在内核态实现上述封装和解封装的工作,从而通过与上述相似的隧道机制,构建出覆盖网络(overlay network)。
VXLAN的设计思想:在现有三层网络之上,覆盖一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的主机(虚拟机、容器)之间,可以像在同一个局域网那样自由通信。这些主机可以分布在不用的宿主机甚至不同的物理机房。
为了能够在二层网络上打通隧道,VXLAN会在宿主机上设置一个特殊的网络设备(VTEP,VXLAN Tunnel End Point隧道虚拟端点)作为隧道的两端。
VTEP设备的作用,与flanneld进程很相似。只不过它进行封装和解封装的对象,是二层数据帧(Ethernet frame),而且整个工作流程在内核里完成。因为VXLAN本身就是在Linux内核中的一个模块。
基于VTEP设备进行隧道通信的流程如下图:
在每台主机上有一个叫flannel.1的设备,这就是VXLAN所需要的VETP设备,它既有IP地址也有MAC地址。
假设C1的IP地址是10.1.15.2
,要访问C2的IP地址是10.1.16.3
。与UDP模式的流程类似。
10.1.16.3
的IP包,会先出现在docker0网桥为了能够将这个IP数据包封装并且发送到正确的宿主机,VXLAN需要找到这条隧道的出口,即目的宿主机的VETP设备,这些设备信息由每台宿主机的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/4`网段的IP包,都需要经过flannel.1设备发出,随后被发往的网关地址是`10.1.16.0`
每个宿主机上的VETP设备之间需要构建一个虚拟的二层网络,即通过二层数据帧进行通信。即源VETP设备将原始IP包加上MAC地址封装成一个二层数据帧,发送到目的端VETP设备。
根据前面添加的路由记录,知道了目的VETP设备的IP地址,利用ARP表,根据三层IP地址查询对应的二层MAC地址。这里使用的ARP记录,也是flanneld进程在node2节点启动时,自动添加在node1上的。如下所示:
# 在 Node 1 上
ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
# IP地址`10.1.16.0`对应的MAC地址是`5e:f8:4f:00:e3:37`
最新版的Flannel不依赖L3 MISS实现和ARP学习,而会在每台节点启动时,把它的VETP设备对应的ARP记录直接放在其他每台宿主机上。
有了MAC地址,Linux内核就可以开始二层封包工作,二层帧的格式如下:
上面封装的二层数据帧中的MAC地址是VETP的地址,对于宿主机网络来说没有实际意义,因此在Linux内核中需要把这个二层数据帧进一步封装成宿主机网络里的普通数据帧,这样就能通过宿主机eth0网卡进行传输。为了实现这个封装,Linux内核会在封装好的二层数据帧前加上一个特殊的VXLAN头,表示这是一个VXLAN要使用的数据帧。然后Linux内核会把这个数据帧封装进一个UDP包里发出去。
VXLAN头里面有一个重要的标志VNI,这个是VTEP设备识别某个数据帧是不是应该归自己处理的重要标识。在Flannel中,VNI默认为1,这与宿主机上的VETP设备的名称flannel.1 匹配。
与UDP模式类似,在宿主机看来,只会认为是自己的flannel.1在向另一台宿主机的flannel.1发起一次普通的UDP链接。但是在这个UDP包中只包含了flannel.1的MAC地址,而不知道应该发给哪一台宿主机,所有flannel.1设备实际上要扮演网桥的角色,在二层网络进行UDP包转发。
在Linux内核里,网桥设备进行转发的依据,来自FDB(Forwarding Database)转发数据库。flanneld进程也需要负责维护这个flannel.1网桥对应的FDB信息,具体内容如下。
# 在 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
# 发往5e:f8:4f:00:e3:37MAC地址的二层数据帧,应该通过flannel.1设备,发送到IP地址为10.168.0.3的主机,这就是node2的IP地址
然后就是一个正常的宿主机网络上的封包工作。
Linux在这个IP包前面加上二层数据帧(Outer Ethernet Header),并把node2的MAC地址(node1的ARP表要学习的内容,无需Flannel维护)填写进去,封装后的数据帧如下所示。
封包完成后,node1上的flannel.1设备就可以把这个数据帧从node1的eth0网卡发出去,这个帧经过宿主机网络来到node2的eth0网卡。
node2的内核网络栈会发现这个数据帧里面的VXLAN头,VNI=1,内核进行拆包,根据数据帧的VNI值,把它交给node2的flannel.1设备。
flannel.1设备继续拆包,取出原始IP包,下面的步骤就是单机容器网络的处理流程。
最终IP包进入c2容器的Network Namespace。
VXLAN 模式组建的覆盖网络,其实就是一个由不同宿主机上的 VTEP 设备,也就是 flannel.1 设备组成的虚拟二层网络。对于 VTEP 设备来说,它发出的“内部数据帧”就仿佛是一直在这个虚拟的二层网络上流动。这,也正是覆盖网络的含义。