系统级服务的无扰动升级(non distruptive upgrade,下文简称为热升级)对服务的快速迭代开发有非常重要的意义。虚拟交换机(vSwitch)作为虚拟网络的入口,需求多变,但频繁升级断网会影响虚机上运行的业务。此外,一般每台宿主机上只有一个虚拟交换机,在架构上也不好做主备。因此热升级技术对 vSwitch 的快速迭代至关重要。
本文介绍了我们在 DPDK based Open vSwtich(下简称 ovs-dpdk)上的热更新技术的实践,希望和业界同行共同探讨。
现状
- Open vSwitch(下简称 ovs)已有的“热升级”方案基本是为 ovs-kernel 而实现的。在 ovs-kernel 中,vswitchd 进程是慢路径,kernel 模块是快速路径。升级 vswitchd 进程时可不替换 kernel 模块,让大部分流量经过 kernel 中 flow cache 转发,减少网络扰动。升级 kernel 模块则可能会有较长的断网风险。这块的详细信息可以参考链接: http://docs.openvswitch.org/en/latest/intro/install/general/?highlight=hot%20upgrade#hot-upgrading
- ovs-dpdk 中,快速路径和慢速路径都集成在 vswitchd 进程中,如果简单的重启 vswtichd,由于 DPDK 的初始化(主要是大页初始化,1G 大页耗时约 600ms)、网卡的初始化(实测 Mellanox CX5 驱动初始化耗时接近 1s)都比较耗时,因此会有秒级断网。
- 为了实现快速的迭代开发,降低升级导致的断网带来的业务扰动,需要开发 ovs-dpdk 的热升级特性。
方案与折衷
实现热更新有很多实现方式:
- 插件式升级(形象的说就是热补丁):将主要的包处理逻辑变成动态链接库,通过热加载插件的方式,避免耗时的 dpdk 初始化和网卡初始化。(DPDK 主从模式也可被认为是这种方式的变体)
优点:常规升级断网时间非常小,可以做到纳秒级。
缺点:框架升级和插件升级成为了两种升级,框架和插件之间必然有相互调用的 API 和共享 数据结构,开发人员需要保持框架和插件之间的 ABI(Application Binary Interface)一致。一个简单的例子是流表的结构体可能会在框架和插件之间共享,如 果升级修改了流表的结构体,框架也需要升级,否则会因为不一致而导致内存错误。
- 双进程升级(形象的说就是热替换):通过硬件资源冗余,让新老 ovs 在升级中并存,老的 ovs 继续进行转发,使得流量不会断,新的 ovs 完成初始化之后,接管流量。
优点:新旧 ovs 是两个进程,不需要做任何兼容,只需要遵守新旧进程之间的信息同步的通 信协议即可。
缺点:需要资源冗余,需要网卡和内存冗余以同时满足新旧 ovs 的升级需求(2x 内存加 2x VF),断网时间也会更长一些。
业界一般采用双进程实现热升级,这种方式的覆盖面更广,工程实现上相对来说也更容易一些。下图描述了双进程热升级的一般流程。
OVS 热更新的工作原理并不复杂,就是实现上需要考虑不少琐碎的工程细节。主要通过代码上精细的控制使得升级断网时间较小。
二段式设计
我们把热升级的整个流程分成两个阶段(二段式),这里的设计有点借鉴虚拟机进行热迁移的过程。在第一阶段,网络的转发服务不会停止,并且一旦升级出现故障,整个系统是可以自动回滚的。只有到达第二阶段,网络会出现断网。在第二阶段,如果出现问题,只能断网重启服务,系统不再具有回滚能力。在实现上,我们给 ovs 开启了一个 JSON-RPC 服务用于热升级。当 ovs 进程启动时,会自动检测是否有老进程的存在。如果存在,则主动通过 JSON-RPC 发起热升级请求。接收到热升级请求的 ovs 进程则开启两阶段升级:
- 第一阶段:
- 老 ovs 进程释放各种独占资源:比如 ovsdb 的数据库锁,pid 文件改名,unixctl server path 等资源。此时老 ovs 不能通过 ovsdb 获取最新配置,但是 PMD 线程依旧运行,转发并未停止。
- 释放资源之后,新老进程开始进行状态同步,主要是一些 OVS 的 megaflow 同步,网络 tap 设备的 fd 同步等。
- 同时,老的 ovs 会将所有的 OpenFlow 规则进行备份。
- 这一切完成之后,老进程会返回 JSON-RPC 的请求的响应。如果上述过程无问题,则返回成功,升级继续。否则返回失败,新进程会退出,升级失败。同时老进程会状态回滚,将之前释放的资源又重新申请回来,回到正常的工作状态。
- 新进程获得各种状态信息,开始正常初始化。包括 DPDK 初始化内存,ovs 从 ovsdb 中获取网桥、网卡等各种配置开始初始化等。在这个过程中,如果出现异常,新进程 crash,会导致 JSON-RPC 这个 unix socket 连接中断。一旦老进程检测到这个连接中断,就认为新进程初始化失败,自动回滚。
- 新进程加载之前备份的 OpenFlow 规则。这之间的 OpenFlow 规则变更,会由 local controller 记录并下发。
- 新进程发起第二阶段 JSON-RPC 请求。
- 第二阶段:
- 老进程退出。
- 新进程开启 pmd 线程开始转发。
- 升级结束。
下图简单的描述了升级时序图:
由升级时序图可以看出,热升级第一阶段里新 ovs 进程完成了最耗时的初始化工作,同时老 ovs 进程一直在进行转发,并没有断网。断网只是在第二阶段老 ovs 退出和新 ovs 启动 PMD 线程之间发生。
在升级过程中,我们利用了 MLNX 网卡一个特性:同一个网卡设备可以被两个独立的 dpdk 进程同时打开,并且流量会被同时镜像到两个进程中去。如果使用其他的网卡,可以通过多 VF 配合流表规则切换的方法达到同样效果。这里就不再展开叙述了。
断网时间评估
我们考察了两种虚拟网卡后端的断网时间:
- 采用 representer + VF 的方式,
- 采用 vhost-user + virtio 的方式。
测试方法
两台宿主机上两台虚机互相 ping,使用ping -i 0.01。两次 ping 之间相隔 10ms,最后统计在升级中,ping 包未回的数量即表示升级断网时间。 iperf -i 1测试,观察 TCP 吞吐是否会受到影响。
representer + VF 方式
ping 测试
10 次试验结果如下:
1524 packets transmitted, 1524 received, +36 duplicates, 0% packet loss, time 16747ms
623 packets transmitted, 622 received, +29 duplicates, 0% packet loss, time 6830ms
662 packets transmitted, 662 received, +30 duplicates, 0% packet loss, time 7263ms
725 packets transmitted, 724 received, +28 duplicates, 0% packet loss, time 7955ms
636 packets transmitted, 635 received, +28 duplicates, 0% packet loss, time 6973ms
752 packets transmitted, 750 received, +27 duplicates, 0% packet loss, time 8251ms
961 packets transmitted, 961 received, +31 duplicates, 0% packet loss, time 10551ms
737 packets transmitted, 737 received, +29 duplicates, 0% packet loss, time 8084ms
869 packets transmitted, 869 received, +27 duplicates, 0% packet loss, time 9543ms
841 packets transmitted, 840 received, +28 duplicates, 0% packet loss, time 9228ms
发现最多出现 1 个丢包,说明断网时间最长 10ms。但是发现了很多 duplicates 的包,这个是因为 mlnx 网卡在 switchdev 模式下有一个 bug:当出现双进程时,本应该流量被镜像到另一个进程,结果 mlnx 网卡让一个进程收到了两个重复包,而另一个进程没有包。目前 mlnx 已经确认了这个问题。
iperf 测试
观测到升级期间,有 1s 速率减半,由满速率 22Gbps 到 13Gbps,估计是和 MLNX bug 引发的重传包和升级断网有关。1s 之后迅速回到 22Gbps。
vhost-user + virtio 模式
ping 测试
断网时间在 70~80ms 左右。通过对日志分析,发现是老进程在退出时,反复进行了 vhost 重连导致,这块通过继续优化,断网时间会进一步降低。这里就不再展开了。
iperf 测试
和 VF 方式结果类似,也是吞吐会有一定的下降,升级完成后立刻恢复。