QEMU 迁移 xbzrle 压缩

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
2
3
4
5
6
7
8
9
10
11
# zrun 和 nzrun 是交替的,并且一个 page 一定以 zrun 开头
# 当 zrun 长度为 0 时,在编码后以 0x00 代替

page = zrun nzrun
       | zrun nzrun page

zrun = lengthc

nzrun = length byte...

length = uleb128 encoded integer
  • 以发送方的角度来看,从 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
2
{qemu} info migrate_capabilities
{qemu} xbzrle: off , ...

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_bufferxbzrle_decode_buffer

qemu/migrate/xbzrle.h头文件非常简单,只暴露了两个函数定义:

1
2
3
4
5
6
7
8
#ifndef QEMU_MIGRATION_XBZRLE_H
#define QEMU_MIGRATION_XBZRLE_H

int xbzrle_encode_buffer(uint8_t *old_buf, uint8_t *new_buf, int slen,
                         uint8_t *dst, int dlen);

int xbzrle_decode_buffer(uint8_t *src, int slen, uint8_t *dst, int dlen);
#endif

这两个函数调用路径

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
int xbzrle_encode_buffer(uint8_t *old_buf, uint8_t *new_buf, int slen,
                         uint8_t *dst, int dlen)
{
	....

    while (i < slen) {
        /* overflow */
        if (d + 2 > dlen) {
            return -1;
        }

        /* 计算 zero run 的长度 */
        /* not aligned to sizeof(long) */
        /* 未能 8 字节对齐的一小段,1byte 1byte 增加到 zrun 中 */
        res = (slen - i) % sizeof(long);
        while (res && old_buf[i] == new_buf[i]) {
            zrun_len++;
            i++;
            res--;
        }

        /* word at a time for speed */
        /* 已对齐的,1word 1word 增加到 zrun 中 */
        if (!res) {
            while (i < slen &&
                   (*(long *)(old_buf + i)) == (*(long *)(new_buf + i))) {
                i += sizeof(long);
                zrun_len += sizeof(long);
            }

            /* go over the rest */
            /* 直到任然有一部分不同,1byte 1byte 增加*/
            while (i < slen && old_buf[i] == new_buf[i]) {
                zrun_len++;
                i++;
            }
        }

        /* 如果已经到尾部则无需再增加 zrun,即最后的 zrun 放弃 */
        /* buffer unchanged */
        if (zrun_len == slen) {
            return 0;
        }

        /* skip last zero run */
        if (i == slen) {
            return d;
        }

        /* 编码计算 zero run 长度,放入 dst 中 */
        d += uleb128_encode_small(dst + d, zrun_len);

        /* 清零 zrun,下次使用,位移到不匹配的字节处,开始计算 nzrun 长度 */
        zrun_len = 0;
        nzrun_start = new_buf + i;

        /* overflow */
        if (d + 2 > dlen) {
            return -1;
        }
        /* not aligned to sizeof(long) */
        res = (slen - i) % sizeof(long);
        while (res && old_buf[i] != new_buf[i]) {
            i++;
            nzrun_len++;
            res--;
        }

        /* word at a time for speed, use of 32-bit long okay */
        if (!res) {
            /* truncation to 32-bit long okay */
            unsigned long mask = (unsigned long)0x0101010101010101ULL;
            while (i < slen) {
                unsigned long xor;
                /* 找到字中第一个 xor 后为 0x00 的字节 */
                xor = *(unsigned long *)(old_buf + i)
                    ^ *(unsigned long *)(new_buf + i);
                if ((xor - mask) & ~xor & (mask << 7)) {
                    /* found the end of an nzrun within the current long */
                    while (old_buf[i] != new_buf[i]) {
                        nzrun_len++;
                        i++;
                    }
                    break;
                } else {
                    i += sizeof(long);
                    nzrun_len += sizeof(long);
                }
            }
        }
        
		/* 编码计算 no zero run 长度,放入 dst 中 */
        d += uleb128_encode_small(dst + d, nzrun_len);
        /* overflow */
        if (d + nzrun_len > dlen) {
            return -1;
        }
        /* 将差异部分原样放入 dst 中 */
        memcpy(dst + d, nzrun_start, nzrun_len);
        d += nzrun_len;
        nzrun_len = 0;
    }

    return d;
}

解码部分代码更为简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int xbzrle_decode_buffer(uint8_t *src, int slen, uint8_t *dst, int dlen)
{
	...

    while (i < slen) {

        /* zrun */
        if ((slen - i) < 2) {
            return -1;
        }
		/* 可以确定第一段一定是 zrun,解码得到 zrun 长度 */
        ret = uleb128_decode_small(src + i, &count);
        if (ret < 0 || (i && !count)) {
            return -1;
        }
        i += ret;
        d += count;

        /* overflow */
        if (d > dlen) {
            return -1;
        }
		
        /* 可以确定在 zrun 后一定接着 nzrun,解码得到 nzrun 长度 */
        /* nzrun */
        if ((slen - i) < 2) {
            return -1;
        }

        ret = uleb128_decode_small(src + i, &count);
        if (ret < 0 || !count) {
            return -1;
        }
        i += ret;

        /* overflow */
        if (d + count > dlen || i + count > slen) {
            return -1;
        }
		
        /* 将 nzrun 后相应长度的字节放入目标内存页 */
        memcpy(dst + d, src + i, count);
        d += count;
        i += count;
    }

    return d;
}

xbzrle 算法实现关键在于xbzrle_encode_buffer中 xor 的计算方式,以及uleb128_decode_smalluleb128_encode_small

ULEB128 算法的作用在于,在解码的时候可以根据最后一字节的第 8 位(0x80)判断是否已经完成一个run的解析。

QEMU 中在util/cutils.c下实现了uleb128_decode_smalluleb128_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 zeros3074zeros。

从第 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

相同处使用[]括起来。下面开始计算:

  1. 对 zrun 的长度 1001,进行 ULEB128 编码,得到0xe9 07
  2. 从 01 到 0f 长度为 16,对 nzrun 的长度 16 进行编码,得到0x0f
  3. 将 16 字节长度的 xor(差异)原样保存;
  4. 对接下来的两个字节的 zrun 编码,得到0x3
  5. 接下来是一个 nzrun 字节,得到0x1,将一个字节的差异原样保存0x67
  6. 以下同理

最终得到结果如下,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 性能优化测试,测试环境为:

  1. guestVM:
    1. machine pc-i440fx-rhel7.6.0,accel=kvm
    2. cpu smp 4,mem 8G
  2. Host:
    1. 两台主机均为虚拟主机,但两台虚拟主机所在物理机不同。
    2. cpu 8,mem 16G
  3. QEMU migrate 设置:
  4. 对比设置

启动命令行如下:

1
2
3
4
5
#srcVM
$ /usr/libexec/qemu-kvm -machine pc-i440fx-rhel7.6.0,accel=kvm -m 8G -smp 4,sockets=1,cores=4,threads=1 -hda /mnt/902f9d2d469345b7a7364bd774b60802.qcow2 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 --monitor stdio

#detVM
$ /usr/libexec/qemu-kvm -machine pc-i440fx-rhel7.6.0,accel=kvm -m 8G -smp 4,sockets=1,cores=4,threads=1 -hda /mnt/902f9d2d469345b7a7364bd774b60802.qcow2 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 --monitor stdio -incoming tcp:0:4444

6.1、无负载

xbzrle off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(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
# common status
Migration status: completed         # 迁移状态:完成
total time: 15549 milliseconds      # 迁移总用时:15549ms
downtime: 113 milliseconds          # srcVM 停机时间:113ms
setup: 170 milliseconds             # 发出 qmp 指令后到开启迁移之前消耗的时间:170ms
# ram migration status
transferred ram: 508788 kbytes		# 已传输的字节数:496M
throughput: 268.24 mbps				# 吞吐量:268.24mbps
remaining ram: 0 kbytes             # 剩余未迁移 ram 大小: 0Kbytes
total ram: 8405832 kbytes           # 迁移的总 ram 大小:8G
duplicate: 1982121 pages            # 零页的数量(即未使用的 ram 数,无需迁移):1982121 页 = 7.56G 
skipped: 0 pages					# 跳过的零页数:0
normal: 122602 pages				# 正常发送的页数:122602 页
normal bytes: 490408 kbytes         # 正常发送的 bytes 数,恰好为正常迁移页数的 4 倍:478.9M
dirty sync count: 4                 # 脏页同步次数:4 次
page size: 4 kbytes                 # 页大小:4K

已传输的字节数>正常发送的字节数>(总字节数 - 零页的数)。

注意已传输字节数不等于正常发送的字节数,考虑到校验的问题。

xbzrle on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(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: 15314 milliseconds		# 迁移总用时:15314ms
downtime: 73 milliseconds			# srcVM 停机时间:73ms
setup: 172 milliseconds				# 发出 qmp 指令后到开启迁移之前消耗的时间:172ms

transferred ram: 502343 kbytes		# 已传输的字节数:502343 kbytes
throughput: 268.91 mbps				# 吞吐量:268.91mbps
remaining ram: 0 kbytes				# 剩余未迁移的字节数: 0Kbytes
total ram: 8405832 kbytes			# 迁移过程中涉及到的字节数:8G
duplicate: 1982094 pages			# 零页的数量:1982094 页
skipped: 0 pages					# 跳过的页数:0
normal: 120994 pages				# 正常发送的页数
normal bytes: 483976 kbytes			# 正常发送 bytes 数,恰好为正常发送页数的 4 倍
dirty sync count: 3					# 脏页同步次数:3 次
page size: 4 kbytes					# 页大小:4K
cache size: 67108864 bytes          # xbzrle cache:64M

xbzrle transferred: 0 kbytes		# 由 xbzrle 转发给 detVM 的数据:0
xbzrle pages: 0 pages				# 由 xbzrle 转发给 detVM 的页:0
xbzrle cache miss: 1613				# 发生 cache miss 次数:1613 次
xbzrle cache miss rate: 0.00		# cache miss 率:0.0
xbzrle overflow : 0					# 溢出次数:0

两者差异不大,且 xbzrle 未能转运任何字节。

6.2、mem_write 测试

6.2.1、低负载

使用简单的写内存程序mem_test来验证开启 xbzrle 前后性能的优化效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void* mem_eat(void *arg)
{
  int per_page = 4 * 1024;
  int page_num = 100 * 1024;
  char *buf;
  static int num = 0;

  buf = (char*)calloc(page_num, per_page);

  printf("....%d...\n", num++);

  while(1){
    int i;
    int j;
    for (i = 0; i < per_page; i++){
      for (j = 0; j < page_num; j++){
        buf[per_page*i + j]++;
      }
    }
  }

}

int main()
{
  int thread_num = 10;
  int i;

  for(i = 0; i < thread_num; i++){
    pthread_t p;
    pthread_create(&p, NULL, mem_eat, NULL);
    pthread_detach(p);
  }

  while(1);
}

xbzrle off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(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: 25181 milliseconds
downtime: 127 milliseconds
setup: 427 milliseconds

transferred ram: 799857 kbytes
throughput: 260.33 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 1936131 pages
skipped: 0 pages
normal: 195328 pages
normal bytes: 781312 kbytes
dirty sync count: 6
page size: 4 kbytes

相比无负载时,总时间更久,脏页同步次数更多,downtime 实际相差不大,setup 相差较大,吞吐量等相差不大。

xbzrle on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(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: 19255 milliseconds
downtime: 103 milliseconds
setup: 256 milliseconds

transferred ram: 604696 kbytes
throughput: 257.42 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 1975722 pages
skipped: 0 pages
normal: 146537 pages
normal bytes: 586148 kbytes
dirty sync count: 5
page size: 4 kbytes
cache size: 67108864 bytes

xbzrle transferred: 37 kbytes
xbzrle pages: 81 pages
xbzrle cache miss: 17049
xbzrle cache miss rate: 0.86
xbzrle overflow : 888

由 xbzrle 实际转发的数据量为 37kbytes,但是其中修改了 81 次脏页。downtime 时间显著减少,总迁移时间变化不大。

6.2.2、高负载

分析上面的测试结果,脏页产生速度不够快,xbzrle 只转发了 81 页,未能体现优势。我们加大脏页产生速度,将程序线程数加到 200;

注意高负载需要使用-d 参数,当脏页产生速度大于传输速度的时候,可能会出现无法完成传输的情况

xbzrle off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(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: 3811965 milliseconds
downtime: 144 milliseconds
setup: 169 milliseconds

transferred ram: 124890721 kbytes
throughput: 268.39 mbps
remaining ram: 0 kbytes
total ram: 8405832 kbytes
duplicate: 1832194 pages
skipped: 0 pages
normal: 31157706 pages
normal bytes: 124630824 kbytes
dirty sync count: 13597
page size: 4 kbytes
(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
$ ./mem_balloon 200 &

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
$ ./mem_balloon 500 &

xbzrle off:

6.4、总结

迁移的性能其实就是 VM 迁移总字节流大小(初始字节流和脏页字节流)、迁移速度、脏页生成速度之间的关系。

各数据之间的关系如下:

  1. 迁移总时间等于准备时间加上迁移时间和停机时间

    total_time = setup_time + migrate_time + down_time

  2. 迁移时间等于总迁移字节流除以平均吞吐速度,平均吞吐速度可配置,但是它受物理因素限制(如 I/O 速度、网络速度)

    migrate_time = total_ram_size / throughput

  3. 总迁移字节流等于初始迁移字节流(即开始迁移时,固定的内存数,之后内存变动标记为 dirty)加上脏数据字节流,初始内存字节流由 VM 实际使用的内存数决定,未使用的内存数为 duplicate,默认为 0 不进行迁移。

    total_ram_size = origin_ram_size + dirty_ram_size

  4. 脏字节流大小由脏页生成速度决定和迁移时间决定,准备时间和停机时间不生成脏页,脏页生成速度由 VM 中写内存的频率决定,若存在密集型写内存应用,生成脏页速度过快,迁移速度不够,则可能永远无法迁移成功。

    dirty_ram_size = dirty_ram_speed * migrate_time

  5. 迁移公式 \((tpSpeed-drSpeed)*mTime = oSize/tpSpeed\) oSize/tpSpeed 可视为常量T,化简: \((tpSpeed-drSpeed)*mTime=T\) 可以得出结论如果迁移速度大于脏页生成速度,那么随着时间的增加,总能迁移完,如果迁移速度小于脏页生成速度,那么则永远无法完成迁移。

  6. 优化方式,优化方式有多种,使用 xbzrle 编码,压缩脏页数据,即减小drSpeed,或者降低 srvVM 的 cpu 频率,同样也减小drSpeed

注意:在完成初始字节流的传输之前,不会使用到 xbzrle,因此在将 origin_ram_size 传输完成之前,不会有 xbzrle 字节流传输。