linux 上 vxlan 简介
这篇文章我们就讲讲在 linux 机器上怎么手动搭建 vxlan overlay 网络。
Linux 对 vxlan 协议的支持时间并不久,2012 年 Stephen Hemminger 才把相关的工作合并到 kernel 中,并最终出现在 kernel 3.7.0 版本。为了稳定性和很多的功能,你可以会看到某些软件推荐在 3.9.0 或者 3.10.0 以后版本的 kernel 上使用 vxlan。
NOTE:如果可以,尽量使用比较新版本的 kernel,以免出现因为内核版本太低导致功能或者性能上的问题。
本文的实验环境:
- MacBook Pro: 1.4 GHz Intel Core i5
- virtualbox 5.1.8
- vagrant 2.0.0
- OS: ubuntu 16.04
- Linux Kernel: 4.4.0-83-generic
我用 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 网络,两台机器构成一个 vxlan 网络,每台机器上有一个 vtep,vtep 通过它们的 IP 互相通信。这次实验完成后的网络结构如下图所示:
首先使用 ip
命令创建我们的 vxlan interface:
1 | $ ip link add vxlan0 type vxlan \ |
上面这条命令创建一个名字为 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
查看它的详细信息:
1 | root@node1:~# ip -d link show dev vxlan0 |
NOTE:ip
命令来自于 iproute2
软件包,它主要用来做网络相关的操作。
接下来,为刚创建的 interface 配置 IP 地址并启用它:
1 | $ ip addr add 10.20.1.2/24 dev vxlan0 |
执行结束之后,会发现路由表项多了下面的内容,所有 10.20.1.0/24
网段的 IP 地址要通过 vxlan0 来转发:
1 | root@node0:~# ip route |
同时,vxlan0 fdb 表项中的内容如下:
1 | root@node1:~# bridge fdb |
这个表项的意思是说,默认的而 vtep 对端地址为 192.168.8.101
,换句话说,如果接收到的报文添加上 vxlan 头部之后都会发到 192.168.8.101
。
在另外一台虚拟机(192.168.8.101
)上也进行相同的配置,要保证 VNI 也是 42,dstport 也是 4789,并修改 vtep 的地址和 remote IP 地址到相应的值。测试两台 vtep 的连通性:
1 | root@node0:~# ping -c 3 10.20.1.3 |
这种点对点的 vxlan 网络只能两两通信,实际用处不大。下面我们介绍多节点怎么组成 vxlan 网络进行通信。
多播模式的 vxlan
如果 vxlan 要使用多播模式,那么底层的网络结构需要支持多播的功能,幸运的是,virtualbox 本身就支持多播,不需要额外设置。
要组成同一个 vxlan 网络,vtep 必须能感知到彼此的存在。多播组本来的功能就是把网络中的某些节点组成一个虚拟的组,所以 vxlan 最初想到用多播来实现是很自然的事情。
这个实验和前面一个非常相似,只不过主机之间不是点对点的连接,而是通过多播组成一个虚拟的整体。最终的网络架构也很相似(为了简单图中只有两个主机,但这个模型可以容纳多个主机组成 vxlan 网络):
可能听起来要加入同一个多播组挺复杂的,但实际上非常简单,先上命令:
1 | $ ip link add vxlan0 type vxlan \ |
这里最重要的参数是 group 239.1.1.1
表示把 vtep 加入到这个多播组。关于多播的原理和使用不是这篇文章的重点,这里选择的多播 IP 地址也没有特殊的含义,关于多播的内容可以自行了解。
运行上面的命令之后,一样添加了对应的路由,不同是的 fdb 表项:
1 | root@node0:~# bridge fdb |
这里默认表项的 dst 字段的值变成了多播地址 239.1.1.1
,而不是之前对方的 vtep 地址。同理给所有需要通信的节点进行上述配置,可以验证他们能通过 10.20.1.0/24 网络互相访问。
我们来分析这个模式下 vxlan 通信的过程:
在配置完成之后,vtep 通过 IGMP 加入同一个多播网络 239.1.1.1
。
- 发送 ping 报文到 10.20.1.3,查看路由表,报文会从 vxlan0 发出去
- 内核发现 vxlan0 的 IP 是 10.20.1.2/24,和目的 IP 在同一个网段,所以在同一个局域网,需要知道对方的 MAC 地址,因此会发送 ARP 报文查询
- ARP 报文源 MAC 地址为 vxlan0 的 MAC 地址,目的 MAC 地址为全 1 的广播地址
- vxlan 根据配置(VNI 42)添加上头部
- 因为不知道对方 vtep 在哪台主机上,根据配置,vtep 会往多播地址 239.1.1.1 发送多播报文
- 多播组中所有的主机都会受到这个报文,内核发现是 vxlan 报文,会根据 VNI 发送给对应的 vtep
- vtep 去掉 vxlan 头部,取出真正的 ARP 请求报文。同时 vtep 会记录
<源 MAC 地址 - vtep 所在主机 IP 地址>
信息到 fdb 表中 - 如果发现 ARP 不是发送给自己的,直接丢弃;如果是发送给自己的,则生成 ARP 应答报文
- 应答报文目的 MAC 地址是发送方 vtep 的 MAC 地址,而且 vtep 已经通过源报文学习到了 vtep 所在的主机,因此会直接单播发送给目的 vtep。因此 vtep 不需要多播,就能填充所有的头部信息
- 应答报文通过 underlay 网络直接返回给发送方主机,发送方主机根据 VNI 把报文转发给 vtep,vtep 解包取出 ARP 应答报文,添加 arp 缓存到内核。并根据报文学习到目的 vtep 所在的主机地址,添加到 fdb 表中
- vtep 已经知道了通信需要的所有信息,后续 ICMP 的 ping 报文都是单播进行的
在这个过程中,在主机上抓包更容易看到通信的具体情况,下面是 ARP 请求报文的详情:
可以看到 vxlan 报文可以分为三块:
- 里层(图中最下面)是 overlay 网络中实体看到的报文(比如这里的 ARP 请求),它们和经典网络的通信报文没有任何区别,除了因为 MTU 导致有些报文比较小
- 然后是 vxlan 头部,我们最关心的字段 VNI 确实是 42
- 最外层(图中最上面)是 vtep 所在主机的通信报文头部。可以看到这里 IP 地址为多播
239.1.1.1
,目的 MAC 地址也是多播对应的地址
而 ARP 应答报文不是多播而是单播的事实也能看出来:
从上面的通信过程,可以看出不少信息:
- 多播其实就相当于 vtep 之间的广播,报文会发给所有的 vtep,但是只有一个会做出应答
- vtep 会通过接收到的报文学习
MAC - VNI - Vtep IP
的信息,减少后续不必要的多播报文 - 对于 overlay 网络中的通信实体来说,整个通信过程对它们的透明的,它们认为自己的通信过程和经典网络没有区别
通信结束之后,可以在主机上看到保存的 ARP 缓存:
1 | root@node0:~# ip neigh |
以及 vtep 需要的 fdb 缓存:
1 | root@node0:~# bridge fdb |
利用 bridge 来接入容器
尽管上面的方法能够通过多播实现自动化的 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:
1 | $ ip link add vxlan0 type vxlan \ |
然后创建网桥 br0
,把 vtep interface 绑定到上面:
1 | $ ip link add br0 type bridge |
下面模拟把容器加入到网桥的操作,创建一个 network namespace,和一对 veth pair:
1 | ip netns add container1 |
为了方便操作,我把上面的过程写成了一个脚本
1 | /usr/bin/env sh |
使用这个脚本,下面的命令就能方便地创建另外一个容器:
1 | $ ./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 网络的秘密。
手动维护 vtep 组
经过上面几个实验,我们来思考一下为什么要使用多播。因为对 overlay 网络来说,它的网段范围是分布在多个主机上的,因此传统 ARP 报文的广播无法直接使用。要想做到 overlay 网络的广播,必须把报文发送到所有 vtep 在的节点,这才引入了多播。
如果有一种方法能够不通过多播,能把 overlay 的广播报文发送给所有的 vtep 主机的话,也能达到相同的功能。当然在维护 vtep 网络组之前,必须提前知道哪些 vtep 要组成一个网络,以及这些 vtep 在哪些主机上。
Linux 的 vxlan 模块也提供了这个功能,而且实现起来并不复杂。创建 vtep interface 的时候不使用 remote
或者 group
参数就行:
1 | $ ip link add vxlan0 type vxlan \ |
这个 vtep interface 创建的时候没有指定多播地址,当第一个 ARP 请求报文发送时它也不知道要发送给谁。但是我们可以手动添加默认的 FDB 表项,比如:
1 | $ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101 |
这样的话,如果不知道对方 VTEP 的地址,就会往选择默认的表项,发到 192.168.8.101
和 192.168.8.102
,相当于手动维护了一个 vtep 的多播组。
在所有的节点的 vtep 上更新对应的 fdb 表项,就能实现 overlay 网络的连通。整个通信流程和多播模式相同,唯一的区别是,vtep 第一次会给所有的组内成员发送单播报文,当然也只有一个 vtep 会做出应答。
使用一些自动化工具,定时更新 FDB 表项,就能动态地维护 VTEP 的拓扑结构。
这个方案解决了在某些 underlay 网络中不能使用多播的问题,但是并没有解决多播的另外一个问题:每次要查找 MAC 地址要发送大量的无用报文,如果 vtep 组节点数量很大,那么每次查询都发送 N 个报文,其中只有一个报文真正有用。
手动维护 fdb 表
如果提前知道目的容器 MAC 地址和它所在主机的 IP 地址,也可以通过更新 fdb 表项来减少广播的报文数量。
创建 vtep 的命令:
1 | $ ip link add vxlan0 type vxlan \ |
这次我们添加了 nolearning
参数,这个参数告诉 vtep 不要通过收到的报文来学习 fdb 表项的内容,因为我们会自动维护这个列表。
然后可以添加 fdb 表项告诉 vtep 容器 MAC 对应的主机 IP 地址:
1 | $ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101 |
如果知道了对方的 MAC 地址,vtep 搜索 fdb 表项就知道应该发送到哪个对应的 vtep 了。需要注意是的,这个情况还是需要默认的表项(那些全零的表项),在不知道容器 IP 和 MAC 对应关系的时候通过默认方式发送 ARP 报文去查询对方的 MAC 地址。
需要注意的是,和上一个方法相比,这个方法并没有任何效率上的改进,只是把自动学习 fdb 表项换成了手动维护(当然实际情况一般是自动化程序来维护),第一次发送 ARP 请求还是会往 vtep 组发送大量单播报文。
当时这个方法给了我们很重要的提示:如果实现知道 vxlan 网络的信息,vtep 需要的信息都是可以自动维护的,而不需要学习。
手动维护 ARP 表
除了维护 fdb 表,arp 表也是可以维护的。如果能通过某个方式知道容器的 IP 和 MAC 地址对应关系,只要更新到每个节点,就能实现网络的连通。
但是这里有个问题,我们需要维护的是每个容器里面的 ARP 表项,因为最终通信的双方是容器。到每个容器里面(所有的 network namespace)去更新对应的 ARP 表,是件工作量很大的事情,而且容器的创建和删除还是动态的,。linux 提供了一个解决方案,vtep 可以作为 arp 代理,回复 arp 请求,也就是说只要 vtep interface 知道对应的 IP - MAC
关系,在接收到容器发来的 ARP 请求时可以直接作出应答。这样的话,我们只需要更新 vtep interface 上 ARP 表项就行了。
创建 vtep interface 需要加上 proxy
参数:
1 | $ ip link add vxlan0 type vxlan \ |
这条命令和上部分相比多了 proxy
参数,这个参数告诉 vtep 承担 ARP 代理的功能。如果收到 ARP 请求,并且自己知道结果就直接作出应答。
当然我们还是要手动更新 fdb 表项来构建 vtep 组,
1 | $ bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.8.101 |
然后,还需要为 vtep 添加 arp 表项,所有要通信容器的 IP - MAC
二元组都要加进去。
1 | $ ip neigh add 10.20.1.3 lladdr d6:d9:cd:0a:a4:28 dev vxlan0 |
在要通信的所有节点配置完之后,容器就能互相 ping 通。当容器要访问彼此,并且第一次发送 ARP 请求时,这个请求并不会发给所有的 vtep,而是当前由当前 vtep 做出应答,大大减少了网络上的报文。
借助自动化的工具做到实时的表项(fdb 和 arp)更新,这种方法就能很高效地实现 overlay 网络的通信。
动态更新 arp 和 fdb 表项
尽管前一种方法通过动态更新 fdb 和 arp 表避免多余的网络报文,但是还有一个的问题:为了能够让所有的容器正常工作,所有可能会通信的容器都必须提前添加到 ARP 和 fdb 表项中。但并不是网络上所有的容器都会互相通信,所以添加的有些表项(尤其是 ARP 表项)是用不到的。
Linux 提供了另外一种方法,内核能够动态地通知节点要和哪个容器通信,应用程序可以订阅这些事件,如果内核发现需要的 ARP 或者 fdb 表项不存在,会发送事件给订阅的应用程序,这样应用程序从中心化的控制拿到这些信息来更新表项,做到更精确的控制。
要收到 L2(fdb)miss,必须要满足几个条件:
- 目的 MAC 地址未知,也就是没有对应的 fdb 表项
- fdb 中没有全零的表项,也就是说默认规则
- 目的 MAC 地址不是多播或者广播地址
要实现这种功能,创建 vtep 的时候需要加上额外的参数:
1 | $ ip link add vxlan0 type vxlan \ |
这次多了两个参数 l2miss
和 l3miss
:
l2miss
:如果设备找不到 MAC 地址需要的 vtep 地址,就发送通知事件l3miss
:如果设备找不到需要 IP 对应的 MAC 地址,就发送通知事件
ip monitor
命令能做到这点,监听某个 interface 的事件,具体用法请参考 man 手册。
1 | root@node0:~# ip monitor all dev vxlan0 |
如果从本节点容器 ping 另外一个节点的容器,就先发生 l3 miss,这是 l3miss 的通知事件,:
1 | root@node0:~# ip monitor all dev vxlan0 |
l3miss
是说这个 IP 地址,vtep 不知道它对应的 MAC 地址,因此要手动添加 arp 记录:
1 | $ ip neigh replace 10.20.1.3 \ |
上面这条命令设置的 nud reachable
参数意思是,这条记录有一个超时时间,系统发现它无效一段时间会自动删除。这样的好处是,不需要手动去删除它,删除后需要通信内核会再次发送通知事件。 nud
是 Neighbour Unreachability Detection
的缩写, 当然根据需要这个参数也可以设置成其他值,比如 permanent
,表示这个记录永远不会过时,系统不会检查它是否正确,也不会删除它,只有管理员也能对它进行修改。
这时候还是不能正常通信,接着会出现 l2miss 的通知事件:
1 | root@node0:~# ip monitor all dev vxlan0 |
类似的,这个事件是说不知道这个容器的 MAC 地址在哪个节点上,所以要手动添加 fdb 记录:
1 | 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,就能实现更复杂的网络结构。