基于libvirt kvm macvtap的虚拟化解决方案

Posted on Thu 24 November 2016 in Ops

前言

这其实是很早以前折腾的东西,但是感觉网上的资料不是很清楚,现在补一下,以免以后想不起来。

macvlan 与 macvtap

其实之所以会用macvtap来做网络端口复用是因为libvirt默认用了它,后来折腾的过程中看了代码,理解了原理, 才明白它相比于以前的bridge方案确实有一些优势,如果虚拟机的流量确实很大,可以用这套方案,来减少物理机的CPU和 网卡的压力。

macvtap与macvlan实际上是内核里面的两个特性,用于在物理网卡后面接一些虚拟端口,复用物理端口,但是利用了网卡 的一个较新的特性,所以从性能上来说比纯虚拟交换性能更高,属于一种半虚拟化方案。

macvlan实际上和虚拟机并不是紧耦合的,你也可以在自己的机器上开一个macvlan做试验:

~> sudo ip link add link eno1 type macvlan

注意把eno1替换成你的物理端口名

好了,你现在有一个macvlan0了,你可以试试

~> sudo ip link set macvlan0 up
~> sudo dhcpcd macvlan0

如果你的网络里有slaac或dhcp6,那么你应该能顺利地拿到IPv6地址。如果你的网络里有dhcp,那么你应该能顺利地拿到IPv4 地址。

你应该会注意到,macvlan端口的mac地址与物理端口的mac地址是不同的。

macvtap实际上是在macvlan创建的虚拟端口后面接了一个字符设备,方便某些场景(比如虚拟机)。

macvlan实现

以下是macvlan的实现中用到的数据结构:

 +------------------------------------------------------------------------------------------------+
 |                                                                                                |
 |                         +------------------------------------------------------------------+   |
 |                         |                    +---------------+                             |   |
 |  register_rx_handler    |               +---->macvlan_device0|                             |   |
 |        +----------------------+         |    +---------------+    +-------------------+    |   |
 |        |                |     |         |    |    priv_data+------>       vlan0       |    |   |
 |        |                | +---+----+    |    |               |    +-------------------+    |   |
 |   +----v-------+        +->  port  |    |    |               |    |    lowerdevice+------------+
 +---> phy_device |          +--------+    |    |               |    |                   |    |
     +------------+          |passthru|    |    |               |    |         port+----------+
                             |        |    |    |               |    |                   |
                             |  vlans+-----+    |               |    |         mode      |
                             |        |    |    |               |    |                   |
                             +--------+    |    |               |    |                   |
                                           |    +---------------+    |                   |
                                           |                         |                   |
                                           |    +---------------+    +-------------------+
                                           +---->macvlan_device1|
                                           |    +---------------+
                                           |
                                           |    +---------------+
                                           +---->macvlan_device2|
                                                +---------------+

对于每一个物理设备phy_device,在第一个macvlan设备创建的时候,会创建一个port, 它会注册一个rx_handler来处理phy_device收到的frame,再将其分发给macvlan设备。

如果macvlan工作在passthru模式上,那么port上只允许attach一个macvlan_device,否则会维护一个vlans列表, 每一个成员对应该物理设备下的一个macvlan设备。

如果你对于网卡驱动开发比较熟悉的话,这里的macvlan_device就是网卡设备所对应的数据结构了。 它的priv_data里存了一些指针,用于访问phy_device和port。

为什么说macvlan用到了物理网卡的特性呢?我们都知道,除非在网卡上设置了promisc、allmulti等flag, 否则网卡只会把符合mac地址的包传到总线上,操作系统不会收到其他包,因此内核不需要花费大量的CPU时间来处理中断, 把不相关的包drop掉。而新的网卡不仅支持根据其本身的mac地址来过滤包,还支持操作系统主动向其添加白名单, 让指定的包通过网卡的过滤,送到操作系统。

macvlan实际上就是利用了这个特性,主动把macvlan设备上的过滤列表添加到物理设备的过滤列表里, 依然利用物理网卡来过滤不相关的包,同时又放行了macvlan设备所需要的包。一般来说现在的网卡都支持这个特性, 如果不支持这个特性,macvlan基本上就没什么优势了(这是我认为的,首先我没有这样的设备,其次我没有读过bridge的代码, 因此不一定准确)。

NOTE: ldd3里对于网卡接口的描述已经过时了,特别是多播部分的set_multicast_list这个接口,实际已经改名字了。

macvlan设备是支持串联的,你可以在macvlan设备上挂载macvlan设备。在内核模块里, 它会把新的macvlan设备直接挂到物理设备的port上,因此性能上不会有损失。

虚拟化方案

libvirt 和 KVM 都是比较成熟的方案,这里就不赘述了。下面说一下三者配合的时候遇到的一些问题。

虚拟机里收不到多播包

上面macvlan的介绍里已经提过了,macvlan设备的过滤列表是会同步到物理设备上的,所以问题在于虚拟机里的网卡上的过滤 列表如何同步到物理机上的macvlan设备。

libvirt的这个 commit里添加了 相应的支持,原理上是通过监听qemu的NIC_RX_FILTER_CHANGED事件,进行同步。

与此同时,在这个 commit里设置了 一个开关,只有trustGuestRxFilters被设置为yes时上面的机制才会工作。

这里对于trustGuestRxFilters有一些介绍,简而言之,为了让 虚拟机收到多播包,你需要:

  1. libvirt版本大于1.2.10
  2. trustGuestRxFilters=yes
  3. 虚拟机的网卡model用virtio(其他model不支持)

如果你的环境不支持上面的需求,可以简单地workaround一下:在物理机上设置macvtap网卡的allmulticast flag:

~> ip link set macvtap0 allmulticast on

把macvtap0替换成你的macvtap端口

这样虚拟机就可以收到所有多播的包了。但是一方面对性能有影响,另一方面存在一些安全隐患。

最新版本的macvlan实现会在自己被打开allmutlicast时自动打开物理网卡的allmulticast, 如果你打开macvtap设备的allmulti之后依然收不到多播的包,可能是内核不够新, 可以尝试手动开一下物理网卡的allmulticast。

虚拟机之间及物理机与虚拟机的通信

macvlan有bridge、VEPA、private、passthru 4种工作模式,其中private和passthru我没用过, 这里主要讲另外两种。

VEPA(Virtual Edge Port Aggregator)是默认的工作模式,它的初衷是希望由物理的交换机来进行所有包交换, 这样可以在交换机上统一配置DHCP filtering之类的策略。

因此这个模式下的macvlan会把所有的包都扔到外部端口上去,期待交换机进行包交换, 把目的地址为本机机器的包再传回来。很多交换机考虑安全原因(防止包打环)是不支持这样的行为的, 但是一些较新的交换机上有这样一个特性,叫hairpin、VEPA或者802.1Qbg。

bridge模式则考虑到某些情况下需要更高效的macvlan之间的通信,因此会在内存中进行包交换,提高速度。

但是无论哪种模式,在没有外部交换机的支持的情况下,都是不可能支持物理端口到macvlan端口的包交换的。 上面的原理部分已经提到了,macvlan的port是在物理端口注册了一个rx_handler, 它只会对物理端口收到的包进行处理,而物理端口发出去的包macvlan是不会看到的。

private模式我没有细看,但应该是drop掉了目的端口为其他macvlan端口的包。

综上,结论如下:

  • 对于有交换机支持的网络中,使用VEPA模式和bridge模式都可以实现物理机与虚拟机之间的所有通信。
  • 在无交换机支持的网络中,
    • 使用VEPA模式,虚拟机之间及物理机与虚拟机之间不能进行任何形式的通信;
    • 使用bridge模式,虚拟机之间可以正常通信,虚拟机与物理机不能正常通信。

IPv6 DAD出错

DAD(Duplicate Address Detection)的相关过程在rfc2462

IPv6的DAD工作方式是向特定的多播组发送Neighbor Solicitation,在一段时间内看是否收到Neighbor Solicitation或者 Neighbor Advertisement,如果收到了,认为出现了地址冲突,这个地址就不会被使用。

我们实际遇到的情况是,虚拟机的发送的Neighbor Solicitation会立刻被自己收到,因此地址始终都处于冲突状态。

这里其实比较奇怪,因为刚刚看 代码 的时候发现,macvlan是不会把macvlan端口发出来的包又送回到原端口的, 有可能是因为当时调试的时候开了allmulticast或者promisc, 进一步确认需要再看一下代码