xbzrle 技术可以减少 VM 停机和热迁移的时间,当 VM 存在密集型写内存的工作负载时这种优化尤其明显。对于大型的企业应用如:SAP 和 ERP 系统,或者说任何使用稀疏内存模型的系统来说都有很好的优化作用。
1、xbzrle 简介
xbzrle,全称 Xor Based Zero Run Length Encoding,它是一种差异压缩算法,用来计算前后内存页差异,并对差异生成压缩数据的一种算法。
热迁移和 VM downtime 操作需要将虚拟机内存迁移到另一台物理机或者保存到磁盘中。
- 传统的做法是,不断地同步在热迁移或 downtime 过程中出现的脏页(dirty pages)数据,每个脏页数据通常为 4K;
但是如果 VM 上运行着一个频繁写内存的工作负载,传统的移动内存方式会遇到性能瓶颈,脏页不断产生,不断同步。
- xbzrle 的做法是,在一个脏页中通常被修改的只是一小部分数据,4K 中绝大部分数据未被修改,xbzrle 计算上次传输和当前内存的差异,获得一个差异 update,通过 LEB128 编码方式将该 update 整合为 xbzrle 格式,之后只需要在目标机上同步该差异数据即可。
使用 xbzrle 方式同步内存,需要在源 VM 上保存之前内存页的 cache,用来和当前内存页比较计算 updates。该 cache 是一个 hash table,并可以通过地址对其进行访问。
cache 越大,越有机会遇到要更新的内存;反之 cache 越小,越容易发生缓存 miss。
这个 cache 的值在热迁移期间也是可以修改的。
2、xbzrle 压缩格式
xbzrle 压缩格式需要体现出之前和当前的内存页差异,zero 用来表示未改变的值。页面数据增量就是使用 zero runs 和 non zero runs 来表示。
- zero run 使用它的长度(以 bytes 为单位)来表示
- non zero run 使用它的长度(以 bytes 为单位)和修改后的新数据来表示
- run 长度使用 ULEB128 进行编码
xbzrle 可以有多个有效编码,但是发送方为了减少计算成本会选择发送更长的编码。
1 |
|
- 以发送方的角度来看,从 cache(默认 64MB)的旧内存页中检索信息,并使用 xbzrle 来压缩内存页的增量 updates;
- 以接收方的角度来看,将 updates 通过 xbzrle 解压缩并合并到已有的内存页中。
该项工作是基于一项公开的研究结果:VEE 2011: Evaluation of Delta Compression Techniques for Efficient Live Migration of Large Virtual Machines by Benoit, Svard, Tordsson and Elmroth
在此之上,采用 XBZRLE 对增量编码器 XBRLE 进行了改进。
对于典型的工作负载,xbzrle 可实现 2-2.5 GB/s 的持续带宽,这使得它非常适合在线实时编码,例如面对热迁移所需的编码。
示例:
old buffer:
1001 zeros
05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 68 00 00 6b 00 6d
3074 zeros
new buffer:
1001 zeros
01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 68 00 00 67 00 69
3074 zeros
encoded buffer:
encoded length 24
e9 07 0f [01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f] 03 01 [67] 01 01 [69]
3、Cache 更新策略
将热迁移的内存页面持续缓存在 cache 中以减少缓存丢失是有效的。xbzrle 使用一个 counter 记录每个 page 的年龄。每当内存的脏位图同步,counter 就增加。若检测到 cache 冲突,xbzrle 将会驱逐年龄超过阈值的 page。
4、在 QEMU 中使用 xbzrle
在 QEMU monitor 中可以直接使用 hmp 命令查看当前 qemu 对 migrate_capabilities 支持能力:
1 |
|
virsh 中没有直接查看 migrate_capabilities 的 API,可以使用 qmp 的query-migrate-capabilities
指令(所有与 migration 相关的的指令可在/dapi/migration.json 中查找)
virsh # qemu-monitor-command a229570dd59145ef8545a4cf381cb8c8 {\"execute\": \"query-migrate-capabilities\"}
{"return":"xbzrle: off\r\nrdma-pin-all: off\r\nauto-converge: off\r\nzero-blocks: off\r\ncompress: off\r\nevents: on\r\npostcopy-ram: off\r\nx-colo: off\r\nrelease-ram: off\r\nreturn-path: off\r\npause-before-switchover: off\r\nx-multifd: off\r\ndirty-bitmaps: off\r\nlate-block-activate: off\r\n","id":"libvirt-684"}
之后的所有做法与此类似。
在 virsh 中可以在迁移时直接指定
--comp-methods
为 xbzrle,并使用--comp-xbzrle-cache
设置 page cache。
在我的实验环境中 QEMU 的 xbzrle 功能是关闭的。使用 hmp 命令开启它:
{qemu} migrate_set_capability xbzrle on
设置 cache 大小,
{qemu} migrate_set_cache_size 256m
注意在 v2.11.0 中migrate_set_cache_size
被弃用了,使用新的方式指定:
{qemu} migrate_set_parameter xbzrle-cache-size 256m
开始迁移:
{qemu} migrate -d tcp:destination.host:4444
{qemu} info migrate
capabilities: xbzrle: on
Migration status: active
transferred ram: A kbytes
remaining ram: B kbytes
total ram: C kbytes
total time: D milliseconds
duplicate: E pages
normal: F pages
normal bytes: G kbytes
cache size: H bytes
xbzrle transferred: I kbytes
xbzrle pages: J pages
xbzrle cache miss: K
xbzrle overflow: L
virsh 下可直接使用virsh migrate
命令
注意一下迁移过程中的一些参数:
- xbzrle cache-miss:到目前为止缓存丢失的次数,缓存丢失率高意味着 cache size 太低;
- xbzrle overflow:译码中溢出的次数,在此情况下增量不能被压缩,当内存页的更改过大或者存在太多小更改的时候会出现这种情况,例如每隔一个 byte 修改一个 byte。
更多有关 qemu migrate 的信息可参考另一篇博文:
libvirt->QEMU 热迁移
5、xbzrle 算法实现(QEMU)
5.1、源码分析
QEMU 中实现了 xbzrle,并且一共只有两个函数:xbzrle_encode_buffer
和xbzrle_decode_buffer
qemu/migrate/xbzrle.h
头文件非常简单,只暴露了两个函数定义:
1 |
|
这两个函数调用路径
ram_load()->ram_load_precopy()->load_xbzrle()->xbzrle_decode_buffer()
ram_save_iterate()/ram_save_complete()->ram_find_and_save_block()->ram_save_host_page()->ram_save_target_page()->ram_save_page->save_xbzrle_page()->xbzrle_encode_buffer()
qemu/migrate/xbzrle.c
中就是这两个函数的具体实现:
1 |
|
解码部分代码更为简单
1 |
|
xbzrle 算法实现关键在于xbzrle_encode_buffer
中 xor 的计算方式,以及uleb128_decode_small
和uleb128_encode_small
。
ULEB128 算法的作用在于,在解码的时候可以根据最后一字节的第 8 位(0x80)判断是否已经完成一个run
的解析。
QEMU 中在
util/cutils.c
下实现了uleb128_decode_small
和uleb128_encode_small
,具体的 ULEB128 算法分析可参考我之前的博客 LEB128 编码。
5.2、示例讲解
下面结合第 2 节的示例来讲解:
old buffer:
1001 zeros
05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 68 00 00 6b 00 6d
3074 zeros
new buffer:
1001 zeros
01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 68 00 00 67 00 69
3074 zeros
新旧 buffer 之间头 1001 个字节和后 3074 个字节完全相同,即1001 zeros
和3074
zeros。
从第 1002 个字节到 1016 字节处不同,1017 1018 字节相同,1019 不同,1020 相同,1021 又不同,排列如下:
old:
05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 68 [00 00] 6b [00] 6d
new:
01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 68 [00 00] 67 [00] 69
相同处使用[]
括起来。下面开始计算:
- 对 zrun 的长度 1001,进行 ULEB128 编码,得到
0xe9 07
; - 从 01 到 0f 长度为 16,对 nzrun 的长度 16 进行编码,得到
0x0f
; - 将 16 字节长度的 xor(差异)原样保存;
- 对接下来的两个字节的 zrun 编码,得到
0x3
; - 接下来是一个 nzrun 字节,得到
0x1
,将一个字节的差异原样保存0x67
; - 以下同理
最终得到结果如下,zrun 部分使用()
,nzrun 部分使用{}
,xor 部分使用[]
。
(e9 07) {0f [01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f]} (03) {01 [67]} (01) {01 [69]}
6、xbzrle 性能优化测试
对 xbzrle 性能优化测试,测试环境为:
- guestVM:
- machine pc-i440fx-rhel7.6.0,accel=kvm
- cpu smp 4,mem 8G
- Host:
- 两台主机均为虚拟主机,但两台虚拟主机所在物理机不同。
- cpu 8,mem 16G
- QEMU migrate 设置:
- 对比设置
启动命令行如下:
1 |
|
6.1、无负载
xbzrle off:
1 |
|
已传输的字节数>正常发送的字节数>(总字节数 - 零页的数)。
注意已传输字节数不等于正常发送的字节数,考虑到校验的问题。
xbzrle on:
1 |
|
两者差异不大,且 xbzrle 未能转运任何字节。
6.2、mem_write 测试
6.2.1、低负载
使用简单的写内存
程序mem_test
来验证开启 xbzrle 前后性能的优化效果:
1 |
|
xbzrle off:
1 |
|
相比无负载时,总时间更久,脏页同步次数更多,downtime 实际相差不大,setup 相差较大,吞吐量等相差不大。
xbzrle on:
1 |
|
由 xbzrle 实际转发的数据量为 37kbytes,但是其中修改了 81 次脏页。downtime 时间显著减少,总迁移时间变化不大。
6.2.2、高负载
分析上面的测试结果,脏页产生速度不够快,xbzrle 只转发了 81 页,未能体现优势。我们加大脏页产生速度,将程序线程数加到 200;
注意高负载需要使用-d 参数,当脏页产生速度大于传输速度的时候,可能会出现无法完成传输的情况
xbzrle off:
1 |
|
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
capabilities: xbzrle: off rdma-pin-all: off auto-converge: off zero-blocks: off compress: off events: off postcopy-ram: off x-colo: off release-ram: off return-path: off pause-before-switchover: off x-multifd: off dirty-bitmaps: off late-block-activate: off
Migration status: active
total time: 10545458 milliseconds
expected downtime: 846 milliseconds
setup: 165 milliseconds
transferred ram: 345495990 kbytes
throughput: 260.76 mbps
remaining ram: 17064 kbytes
total ram: 8405832 kbytes
duplicate: 1812040 pages
skipped: 0 pages
normal: 86201395 pages
normal bytes: 344805580 kbytes
dirty sync count: 30466
page size: 4 kbytes
dirty pages rate: 10003 pages
xbzrle on:
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
capabilities: xbzrle: on rdma-pin-all: off auto-converge: off zero-blocks: off compress: off events: off postcopy-ram: off x-colo: off release-ram: off return-path: off pause-before-switchover: off x-multifd: off dirty-bitmaps: off late-block-activate: off
Migration status: completed
total time: 53209 milliseconds
downtime: 104 milliseconds
setup: 517 milliseconds
transferred ram: 1701088 kbytes
throughput: 261.95 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 1738003 pages
skipped: 0 pages
normal: 420614 pages
normal bytes: 1682456 kbytes
dirty sync count: 6
page size: 4 kbytes
cache size: 67108864 bytes
xbzrle transferred: 66 kbytes
xbzrle pages: 152 pages
xbzrle cache miss: 54071
xbzrle cache miss rate: 0.97
xbzrle overflow : 470
这次的数据有问题
6.2.3、极高负载
6.3、mem_balloon 测试
1 |
|
xbzrle off:
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
capabilities: xbzrle: off rdma-pin-all: off auto-converge: off zero-blocks: off compress: off events: off postcopy-ram: off x-colo: off release-ram: off return-path: off pause-before-switchover: off x-multifd: off dirty-bitmaps: off late-block-activate: off
Migration status: completed
total time: 18120 milliseconds
downtime: 74 milliseconds
setup: 168 milliseconds
transferred ram: 589795 kbytes
throughput: 266.81 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 2003257 pages
skipped: 0 pages
normal: 142768 pages
normal bytes: 571072 kbytes
dirty sync count: 6
page size: 4 kbytes
xbzrle on:
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
capabilities: xbzrle: on rdma-pin-all: off auto-converge: off zero-blocks: off compress: off events: off postcopy-ram: off x-colo: off release-ram: off return-path: off pause-before-switchover: off x-multifd: off dirty-bitmaps: off late-block-activate: off
Migration status: completed
total time: 17163 milliseconds
downtime: 19 milliseconds
setup: 168 milliseconds
transferred ram: 559722 kbytes
throughput: 267.33 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 2033242 pages
skipped: 0 pages
normal: 135185 pages
normal bytes: 540740 kbytes
dirty sync count: 6
page size: 4 kbytes
cache size: 67108864 bytes
xbzrle transferred: 54 kbytes
xbzrle pages: 290 pages
xbzrle cache miss: 6837
xbzrle cache miss rate: 0.00
xbzrle overflow : 0
1 |
|
xbzrle off:
6.4、总结
迁移的性能其实就是 VM 迁移总字节流大小(初始字节流和脏页字节流)、迁移速度、脏页生成速度之间的关系。
各数据之间的关系如下:
-
迁移总时间等于准备时间加上迁移时间和停机时间
total_time = setup_time + migrate_time + down_time
-
迁移时间等于总迁移字节流除以平均吞吐速度,平均吞吐速度可配置,但是它受物理因素限制(如 I/O 速度、网络速度)
migrate_time = total_ram_size / throughput
-
总迁移字节流等于初始迁移字节流(即开始迁移时,固定的内存数,之后内存变动标记为 dirty)加上脏数据字节流,初始内存字节流由 VM 实际使用的内存数决定,未使用的内存数为 duplicate,默认为 0 不进行迁移。
total_ram_size = origin_ram_size + dirty_ram_size
-
脏字节流大小由脏页生成速度决定和迁移时间决定,准备时间和停机时间不生成脏页,脏页生成速度由 VM 中写内存的频率决定,若存在密集型写内存应用,生成脏页速度过快,迁移速度不够,则可能永远无法迁移成功。
dirty_ram_size = dirty_ram_speed * migrate_time
-
迁移公式 \((tpSpeed-drSpeed)*mTime = oSize/tpSpeed\)
oSize/tpSpeed
可视为常量T
,化简: \((tpSpeed-drSpeed)*mTime=T\) 可以得出结论如果迁移速度大于脏页生成速度,那么随着时间的增加,总能迁移完,如果迁移速度小于脏页生成速度,那么则永远无法完成迁移。 -
优化方式,优化方式有多种,使用 xbzrle 编码,压缩脏页数据,即减小
drSpeed
,或者降低 srvVM 的 cpu 频率,同样也减小drSpeed
。
注意:在完成初始字节流的传输之前,不会使用到 xbzrle,因此在将 origin_ram_size 传输完成之前,不会有 xbzrle 字节流传输。