https://cizixs.com/2017/09/28/linux-vxlan/
vxlan 协议的介绍文章主要介绍了 vxlan 协议的理论知识,从产生的背景到报文的格式等等,和所有的计算机知识一样,理论必须结合实践理解才能更深刻,这篇文章我们就讲讲在 linux 机器上怎么手动搭建 vxlan overlay 网络。
Linux 对 vxlan 协议的支持时间并不久,2012 年 Stephen Hemminger 才把相关的工作合并到 kernel 中,并最终出现在 kernel 3.7.0 版本。为了稳定性和很多的功能,你可以会看到某些软件推荐在 3.9.0 或者 3.10.0 以后版本的 kernel 上使用 vxlan。
NOTE:如果可以,尽量使用比较新版本的 kernel,以免出现因为内核版本太低导致功能或者性能上的问题。
本文的实验环境:
我用 vagrant 启动了三台虚拟机,它们之间通信的 IP 地址(也就是 underlay 网络)为:
192.168.8.100
192.168.8.101
192.168.8.102
而要创建的 overlay 网络网段为 10.20.1.0/24
,实验目的就是 vxlan 能够通过 overlay IP 互相连通。
我们先来搭建一个最简单的 vxlan 网络,两台机器构成一个 vxlan 网络,每台机器上有一个 vtep,vtep 通过它们的 IP 互相通信。这次实验完成后的网络结构如下图所示:
首先使用 ip
命令创建我们的 vxlan interface:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
remote 192.168.8.101 \
local 192.168.8.100 \
dev enp0s8
上面这条命令创建一个名字为 vxlan0
,类型为 vxlan
的网络 interface,后面是 vxlan interface 需要的参数:
id 42
:指定 VNI 的值,这个值可以在 1 到 2^24 之间dstport
:vtep 通信的端口,linux 默认使用 8472(为了保持兼容,默认值一直没有更改),而 IANA 分配的端口是 4789,所以我们这里显式指定了它的值remote 192.168.8.101
:对方 vtep 的地址,类似于点对点协议local 192.168.8.100
:当前节点 vtep 要使用的 IP 地址dev enp0s8
:当节点用于 vtep 通信的网卡设备,用来读取 IP 地址。注意这个参数和 local
参数含义是相同的,在这里写出来是为了告诉大家有两个参数存在执行完之后,系统就会创建一个名字为 vxlan0
的 interface,可以用 ip -d link
查看它的详细信息:
root@node1:~# ip -d link show dev vxlan0
4: vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether ba:d4:0a:f8:41:7c brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 42 remote 192.168.8.101 dev enp0s8 srcport 0 0 dstport 4789 ageing 300 addrgenmode eui64
NOTE:ip
命令来自于 iproute2
软件包,它主要用来做网络相关的操作。
接下来,为刚创建的 interface 配置 IP 地址并启用它:
$ ip addr add 10.20.1.2/24 dev vxlan0
$ ip link set vxlan0 up
执行结束之后,会发现路由表项多了下面的内容,所有 10.20.1.0/24
网段的 IP 地址要通过 vxlan0 来转发:
root@node0:~# ip route
10.20.1.0/24 dev vxlan0 proto kernel scope link src 10.20.1.2
同时,vxlan0 fdb 表项中的内容如下:
root@node1:~# bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101 via enp0s8 self permanent
这个表项的意思是说,默认的而 vtep 对端地址为 192.168.8.101
,换句话说,如果接收到的报文添加上 vxlan 头部之后都会发到 192.168.8.101
。
在另外一台虚拟机(192.168.8.101
)上也进行相同的配置,要保证 VNI 也是 42,dstport 也是 4789,并修改 vtep 的地址和 remote IP 地址到相应的值。测试两台 vtep 的连通性:
root@node0:~# ping -c 3 10.20.1.3
PING 10.20.1.3 (10.20.1.3) 56(84) bytes of data.
64 bytes from 10.20.1.3: icmp_seq=1 ttl=64 time=1.84 ms
64 bytes from 10.20.1.3: icmp_seq=2 ttl=64 time=0.462 ms
64 bytes from 10.20.1.3: icmp_seq=3 ttl=64 time=0.427 ms
--- 10.20.1.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.427/0.911/1.844/0.659 ms
这种点对点的 vxlan 网络只能两两通信,实际用处不大。下面我们介绍多节点怎么组成 vxlan 网络进行通信。
如果 vxlan 要使用多播模式,那么底层的网络结构需要支持多播的功能,幸运的是,virtualbox 本身就支持多播,不需要额外设置。
要组成同一个 vxlan 网络,vtep 必须能感知到彼此的存在。多播组本来的功能就是把网络中的某些节点组成一个虚拟的组,所以 vxlan 最初想到用多播来实现是很自然的事情。
这个实验和前面一个非常相似,只不过主机之间不是点对点的连接,而是通过多播组成一个虚拟的整体。最终的网络架构也很相似(为了简单图中只有两个主机,但这个模型可以容纳多个主机组成 vxlan 网络):
可能听起来要加入同一个多播组挺复杂的,但实际上非常简单,先上命令:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
group 239.1.1.1 \
dev enp0s8
$ ip addr add 10.20.1.2/24 dev vxlan0
$ ip link set vxlan0 up
这里最重要的参数是 group 239.1.1.1
表示把 vtep 加入到这个多播组。关于多播的原理和使用不是这篇文章的重点,这里选择的多播 IP 地址也没有特殊的含义,关于多播的内容可以自行了解。
运行上面的命令之后,一样添加了对应的路由,不同是的 fdb 表项:
root@node0:~# bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 239.1.1.1 via enp0s8 self permanent
这里默认表项的 dst 字段的值变成了多播地址 239.1.1.1
,而不是之前对方的 vtep 地址。同理给所有需要通信的节点进行上述配置,可以验证他们能通过 10.20.1.0/24 网络互相访问。
我们来分析这个模式下 vxlan 通信的过程:
在配置完成之后,vtep 通过 IGMP 加入同一个多播网络 239.1.1.1
。
<源 MAC 地址 - vtep 所在主机 IP 地址>
信息到 fdb 表中在这个过程中,在主机上抓包更容易看到通信的具体情况,下面是 ARP 请求报文的详情:
可以看到 vxlan 报文可以分为三块:
239.1.1.1
,目的 MAC 地址也是多播对应的地址而 ARP 应答报文不是多播而是单播的事实也能看出来:
从上面的通信过程,可以看出不少信息:
MAC - VNI - Vtep IP
的信息,减少后续不必要的多播报文通信结束之后,可以在主机上看到保存的 ARP 缓存:
root@node0:~# ip neigh
10.20.1.3 dev vxlan0 lladdr d6:d9:cd:0a:a4:28 STALE
以及 vtep 需要的 fdb 缓存:
root@node0:~# bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 239.1.1.1 via enp0s8 self permanent
d6:d9:cd:0a:a4:28 dev vxlan0 dst 192.168.8.101 self
尽管上面的方法能够通过多播实现自动化的 overlay 网络构建,但是通信的双方只有 vtep,在实际的生产中,每台主机上都有几十台甚至上百台的虚拟机或者容器需要通信,因此我们需要找到一种方法能够把这些通信实体组织起来。
在 linux 中把同一个网段的 interface 组织起来正是网桥(bridge,或者 switch,这两个名称等价)的功能,因此这部分我们介绍如何用网桥把多个虚拟机或者容器放到同一个 vxlan overlay 网络中。最终实现的网络架构如下图所示:
因为创建虚拟机或者容器比较麻烦,我们用 network namespace 来模拟,从理论上它们是一样的。关于 network namespace 和 veth pair 的基础知识,请参考我之前的介绍文章。
对于每个容器/虚拟机,我们创建一个 network namespace,并通过一对 veth pair 把容器中的 eth0 网络连接到网桥上。同时 vtep 也会放到网桥上,以便能够对报文进行 vxlan 相关的处理。
首先我们创建 vtep interface,使用的是多播模式:
创建 vtep interface:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
group 239.1.1.1 \
local 192.168.8.100 \
dev enp0s8
然后创建网桥 br0
,把 vtep interface 绑定到上面:
$ ip link add br0 type bridge
$ ip link set vxlan100 master bridge
$ ip link set vxlan100 up
$ ip link set br0 up
下面模拟把容器加入到网桥的操作,创建一个 network namespace,和一对 veth pair:
ip netns add container1
# 创建 veth pair,并把一端加到网桥上
ip link add veth0 type veth peer name veth1
ip link set dev veth0 master br0
ip link set dev veth0 up
# 配置容器内部的网络和 IP
ip link set dev veth1 netns container1
ip netns exec container1 ip link set lo up
ip netns exec container1 ip link set veth1 name eth0
ip netns exec container1 ip addr add 10.20.1.2/24 dev eth0
ip netns exec container1 ip link set eth0 up
为了方便操作,我把上面的过程写成了一个脚本。
使用这个脚本,下面的命令就能方便地创建另外一个容器:
$ ./set_container br0 container4 10.20.1.4/24
用同样的方法在另外一台主机上配置 vxlan 网络,添加 IP 为 10.20.1.3/24 的容器,并测试它们的连通性。
容器通信过程和前面的实验类似,只不过这里容器发出的 ARP 报文会被网桥转发给 vxlan0
,然后 vxlan0
添加 vxlan 头部通过多播来找到对方的 MAC 地址。
从逻辑上可以认为,在 vxlan1
的帮助下同一个 vxlan overlay 网络中的容器是连接到同一个网桥上的,示意图如下:
多播实现很简单,不需要中心化的控制。但是不是所有的网络都支持多播,而且需要事先规划多播组和 VNI 的对应关系,在 overlay 网络数量比较多时也会很麻烦,多播也会导致大量的无用报文在网络中出现。现在很多云计算的网络都会通过自动化的方式来发现 vtep 和 MAC 信息,也就是自动构建 vxlan 网络。下面的几个部分,我们来解开自动化 vxlan 网络的秘密。
经过上面几个实验,我们来思考一下为什么要使用多播。因为对 overlay 网络来说,它的网段范围是分布在多个主机上的,因此传统 ARP 报文的广播无法直接使用。要想做到 overlay 网络的广播,必须把报文发送到所有 vtep 在的节点,这才引入了多播。
如果有一种方法能够不通过多播,能把 overlay 的广播报文发送给所有的 vtep 主机的话,也能达到相同的功能。当然在维护 vtep 网络组之前,必须提前知道哪些 vtep 要组成一个网络,以及这些 vtep 在哪些主机上。
Linux 的 vxlan 模块也提供了这个功能,而且实现起来并不复杂。创建 vtep interface 的时候不使用 remote
或者 group
参数就行:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
dev enp0s8
这个 vtep interface 创建的时候没有指定多播地址,当第一个 ARP 请求报文发送时它也不知道要发送给谁。但是我们可以手动添加默认的 FDB 表项,比如:
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.102
这样的话,如果不知道对方 VTEP 的地址,就会往选择默认的表项,发到 192.168.8.101
和 192.168.8.102
,相当于手动维护了一个 vtep 的多播组。
在所有的节点的 vtep 上更新对应的 fdb 表项,就能实现 overlay 网络的连通。整个通信流程和多播模式相同,唯一的区别是,vtep 第一次会给所有的组内成员发送单播报文,当然也只有一个 vtep 会做出应答。
使用一些自动化工具,定时更新 FDB 表项,就能动态地维护 VTEP 的拓扑结构。
这个方案解决了在某些 underlay 网络中不能使用多播的问题,但是并没有解决多播的另外一个问题:每次要查找 MAC 地址要发送大量的无用报文,如果 vtep 组节点数量很大,那么每次查询都发送 N 个报文,其中只有一个报文真正有用。
如果提前知道目的容器 MAC 地址和它所在主机的 IP 地址,也可以通过更新 fdb 表项来减少广播的报文数量。
创建 vtep 的命令:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
dev enp0s8 \
nolearning
这次我们添加了 nolearning
参数,这个参数告诉 vtep 不要通过收到的报文来学习 fdb 表项的内容,因为我们会自动维护这个列表。
然后可以添加 fdb 表项告诉 vtep 容器 MAC 对应的主机 IP 地址:
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.102
$ bridge fdb append 52:5e:55:58:9a:ab dev vxlan0 dst 192.168.8.101
$ bridge fdb append d6:d9:cd:0a:a4:28 dev vxlan0 dst 192.168.8.102
如果知道了对方的 MAC 地址,vtep 搜索 fdb 表项就知道应该发送到哪个对应的 vtep 了。需要注意是的,这个情况还是需要默认的表项(那些全零的表项),在不知道容器 IP 和 MAC 对应关系的时候通过默认方式发送 ARP 报文去查询对方的 MAC 地址。
需要注意的是,和上一个方法相比,这个方法并没有任何效率上的改进,只是把自动学习 fdb 表项换成了手动维护(当然实际情况一般是自动化程序来维护),第一次发送 ARP 请求还是会往 vtep 组发送大量单播报文。
当时这个方法给了我们很重要的提示:如果实现知道 vxlan 网络的信息,vtep 需要的信息都是可以自动维护的,而不需要学习。
除了维护 fdb 表,arp 表也是可以维护的。如果能通过某个方式知道容器的 IP 和 MAC 地址对应关系,只要更新到每个节点,就能实现网络的连通。
但是这里有个问题,我们需要维护的是每个容器里面的 ARP 表项,因为最终通信的双方是容器。到每个容器里面(所有的 network namespace)去更新对应的 ARP 表,是件工作量很大的事情,而且容器的创建和删除还是动态的,。linux 提供了一个解决方案,vtep 可以作为 arp 代理,回复 arp 请求,也就是说只要 vtep interface 知道对应的 IP - MAC
关系,在接收到容器发来的 ARP 请求时可以直接作出应答。这样的话,我们只需要更新 vtep interface 上 ARP 表项就行了。
创建 vtep interface 需要加上 proxy
参数:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
dev enp0s8 \
nolearning \
proxy
这条命令和上部分相比多了 proxy
参数,这个参数告诉 vtep 承担 ARP 代理的功能。如果收到 ARP 请求,并且自己知道结果就直接作出应答。
当然我们还是要手动更新 fdb 表项来构建 vtep 组,
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101
$ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.102
$ bridge fdb append 52:5e:55:58:9a:ab dev vxlan0 dst 192.168.8.101
$ bridge fdb append d6:d9:cd:0a:a4:28 dev vxlan0 dst 192.168.8.102
然后,还需要为 vtep 添加 arp 表项,所有要通信容器的 IP - MAC
二元组都要加进去。
$ ip neigh add 10.20.1.3 lladdr d6:d9:cd:0a:a4:28 dev vxlan0
$ ip neigh add 10.20.1.4 lladdr 52:5e:55:58:9a:ab dev vxlan0
在要通信的所有节点配置完之后,容器就能互相 ping 通。当容器要访问彼此,并且第一次发送 ARP 请求时,这个请求并不会发给所有的 vtep,而是当前由当前 vtep 做出应答,大大减少了网络上的报文。
借助自动化的工具做到实时的表项(fdb 和 arp)更新,这种方法就能很高效地实现 overlay 网络的通信。
尽管前一种方法通过动态更新 fdb 和 arp 表避免多余的网络报文,但是还有一个的问题:为了能够让所有的容器正常工作,所有可能会通信的容器都必须提前添加到 ARP 和 fdb 表项中。但并不是网络上所有的容器都会互相通信,所以添加的有些表项(尤其是 ARP 表项)是用不到的。
Linux 提供了另外一种方法,内核能够动态地通知节点要和哪个容器通信,应用程序可以订阅这些事件,如果内核发现需要的 ARP 或者 fdb 表项不存在,会发送事件给订阅的应用程序,这样应用程序从中心化的控制拿到这些信息来更新表项,做到更精确的控制。
要收到 L2(fdb)miss,必须要满足几个条件:
要实现这种功能,创建 vtep 的时候需要加上额外的参数:
$ ip link add vxlan0 type vxlan \
id 42 \
dstport 4789 \
dev enp0s8 \
nolearning \
proxy \
l2miss \
l3miss
这次多了两个参数 l2miss
和 l3miss
:
l2miss
:如果设备找不到 MAC 地址需要的 vtep 地址,就发送通知事件l3miss
:如果设备找不到需要 IP 对应的 MAC 地址,就发送通知事件ip monitor
命令能做到这点,监听某个 interface 的事件,具体用法请参考 man 手册。
root@node0:~# ip monitor all dev vxlan0
如果从本节点容器 ping 另外一个节点的容器,就先发生 l3 miss,这是 l3miss 的通知事件,:
root@node0:~# ip monitor all dev vxlan0
[nsid current]miss 10.20.1.3 STALE
l3miss
是说这个 IP 地址,vtep 不知道它对应的 MAC 地址,因此要手动添加 arp 记录:
$ ip neigh replace 10.20.1.3 \
lladdr b2:ee:aa:42:8b:0b \
dev vxlan0 \
nud reachable
上面这条命令设置的 nud reachable
参数意思是,这条记录有一个超时时间,系统发现它无效一段时间会自动删除。这样的好处是,不需要手动去删除它,删除后需要通信内核会再次发送通知事件。 nud
是 Neighbour Unreachability Detection
的缩写, 当然根据需要这个参数也可以设置成其他值,比如 permanent
,表示这个记录永远不会过时,系统不会检查它是否正确,也不会删除它,只有管理员也能对它进行修改。
这时候还是不能正常通信,接着会出现 l2miss 的通知事件:
root@node0:~# ip monitor all dev vxlan0
[nsid current]miss lladdr b2:ee:aa:42:8b:0b STALE
类似的,这个事件是说不知道这个容器的 MAC 地址在哪个节点上,所以要手动添加 fdb 记录:
root@node0:~# bridge fdb add b2:ee:aa:42:8b:0b dst 192.168.8.101 dev vxlan0
在通信的另一台机器上执行响应的操作,就会发现两者能 ping 通了。
上面提出的所有方案中,其中手动的部分都可以使用程序来自动完成,需要的信息一般都是从集中式的控制中心获取的,这也是大多数基于 vxlan 的 SDN 网络的大致架构。当然具体的实现不一定和某种方法相同,可能是上述方法的变形或者组合,但是设计思想都是一样的。
虽然上述的实验中,为了简化图中只有两台主机,而且只有一个 vxlan 网络,但是利用相同的操作很容易创建另外一个 vxlan 网络(必须要保证 vtep 的 VNI 值不同,如果使用多播,也要保证多播 IP 不同),如下图所示:
主机会根据 VNI 来区别不同的 vxlan 网络,不同的 vxlan 网络之间不会相互影响。如果再加上 network namespace,就能实现更复杂的网络结构。