云平台高可用之虚拟机迁移

作者:Diting0x


CSysSec注: 本文来自Diting0x个人博客,主要介绍虚拟机迁移技术,包括虚拟机迁移主要算法以及在KVM/QEMU平台上的迁移细节。
转载本文请务必注明,文章出处:《云平台高可用之虚拟机迁移》与作者信息:Diting0x


  • 什么是云平台高可用
  • 虚拟机迁移主要算法
  • 实现 Pre-copy
  • 思考

什么是云平台高可用

云,一个再熟悉不过的词了。大家都看到了云计算的商业价值,于是乎各大IT公司都纷纷提出自己的云计算服务,有Amozon的EC2,微软的Azure,谷歌的Google AppEngine,腾讯云,百度云,阿里云等。说到Google AppEngine,相信许多用goagent翻墙党都用过这个东西,但估计大家都只忙着使用goagent,却忽略了AppEngine其中提供的云服务。如今,云无处不在,从当今主流产品看,云平台需要满足以下几个需求:

  • 第一,满足多用户的大规模并发访问,想想2015年的双十一,官方数据表明Taobao每秒钟要处理14万订单交易,平均每天也有七八千万访客(具体数据待我问阿里朋友)
  • 第二,处理海量数据,14年,google平均每天要处理57.4亿次查询,处理的数据高达100PB;阿里云的大数据产品ODPS在6个小时内就能处理100PB的数据
  • 第三,提供可持续的服务,如今云平台的用户如此之大,分秒级的停机时间将影响数百万用户,导致数十万美元损失,不管从用户角度还是公司本身角度来看,停机时间过长都是不可容忍的

针对需求一和需求二,当前云平台的解决方案大多是通过分布式协议及系统来组织管理大量的廉价设备,以提供良好的可扩展性,从而满足大量用户的高并发访问需求以及对海量数据的处理能力,这不在本文考虑范围之内,不多叙说。本文只针对第三个需求,提供可持续的服务,也就是云计算平台中常说的高可用性(high availability)。可用性的准确定义是,在需要的外部资源得到保证的前提下,系统在规定的条件和时间内处于可执行功能状态的能力,高可用则定义为用来保障系统可用性达到某一预定水平的系统设计和技术手段。目前,许多云服务提供商都声称自己的云平台能保证较高的可用性,但实际上,近年来云平台的失效事件频繁发生。具体事件,可搜索了解一下。

虚拟化技术是云计算平台的核心支撑技术,虚拟机的强隔离性有效解决了资源的共享使用问题,大大支撑了云计算平台的资源聚合、负载均衡、节能、可扩展等特性。为保证云平台的高可用性,基于虚拟机备份思想的技术应运而生,也是当前云平台为保证高可用性的最主要途径。虚拟机备份思想,主要包括三个方面:

  • 第一,快照回滚(snapshot&rollback),将虚拟机备份状态保存在持久化存储系统中, 在虚拟机因上层软件或底层硬件故障失效后,可以加载备份状态并恢复到之前的运行 状态继续运行
  • 第二,热备技术(hot-standby),将虚拟机执行状态实时传输到目的端计算节点,在检测到源计算节点失效后,目的端的虚拟机状态可立刻恢复并持续提供任务
  • 第三,虚拟机迁移(migration),将虚拟机运行时状态从一台计算节点传输到另一台计算节点,保证虚拟机在源计算节点因失效或维护而停机时可以在目的端继续执行

本文旨在讲解虚拟机迁移技术

虚拟机迁移主要算法

目前运用最广泛最原始的算法是预拷贝(pre-copy)算法和后拷贝(post-copy)算法,Pre-copy算法也被集成在主流虚拟机平台中如Xen,KVM,VMWare的官方源码中, Post-copy虽还没被各主流虚拟机平台集成,但个人实现起来也不是什么难事。 下面主要介绍这两种算法:

Pre-copy, 先引用顶会 NSDI’05 Live Migration of Virtual Machines 论文中的一张图,描述了pre-copy算法的时间线

pre-copy steps
简而言之,大致思想就是先迭代的迁移整个内存的所有页面,迭代过程中,如果页面有更新,则再迁移更新过的页面,直到满足一个条件让迭代过程收敛(这个条件可以自己根据不同情况合理设置),最后再迁移剩余的页面、cpu、寄存器等状态以及外部设备。
贴一个基于Qemu/kvm 1.1.2的pre-copy算法主要代码:

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
int ram_save_live(QEMUFile *f, int stage, void *opaque)
{
ram_addr_t addr;
uint64_t bytes_transferred_last;
double bwidth = 0;
uint64_t expected_time = 0;
int ret;
if (stage < 0) {
memory_global_dirty_log_stop();
return 0;
}
memory_global_sync_dirty_bitmap(get_system_memory());
if (stage == 1) {
RAMBlock *block;
bytes_transferred = 0;
last_block = NULL;
last_offset = 0;
sort_ram_list();
/* Make sure all dirty bits are set */
QLIST_FOREACH(block, &ram_list.blocks, next) {
for (addr = 0; addr < block->length; addr += TARGET_PAGE_SIZE) {
if (!memory_region_get_dirty(block->mr, addr, TARGET_PAGE_SIZE,
DIRTY_MEMORY_MIGRATION)) {
memory_region_set_dirty(block->mr, addr, TARGET_PAGE_SIZE);
}
}
}
memory_global_dirty_log_start();
qemu_put_be64(f, ram_bytes_total() | RAM_SAVE_FLAG_MEM_SIZE);
QLIST_FOREACH(block, &ram_list.blocks, next) {
qemu_put_byte(f, strlen(block->idstr));
qemu_put_buffer(f, (uint8_t *)block->idstr, strlen(block->idstr));
qemu_put_be64(f, block->length);
}
}
bytes_transferred_last = bytes_transferred;
bwidth = qemu_get_clock_ns(rt_clock);
while ((ret = qemu_file_rate_limit(f)) == 0) {
int bytes_sent;
bytes_sent = ram_save_block(f);
bytes_transferred += bytes_sent;
if (bytes_sent == 0) { /* no more blocks */
break;
}
}
if (ret < 0) {
return ret;
}
bwidth = qemu_get_clock_ns(rt_clock) - bwidth;
bwidth = (bytes_transferred - bytes_transferred_last) / bwidth;
/* if we haven't transferred anything this round, force expected_time to a
* a very high value, but without crashing */
if (bwidth == 0) {
bwidth = 0.000001;
}
/* try transferring iterative blocks of memory */
if (stage == 3) {
int bytes_sent;
/* flush all remaining blocks regardless of rate limiting */
while ((bytes_sent = ram_save_block(f)) != 0) {
bytes_transferred += bytes_sent;
}
memory_global_dirty_log_stop();
}
qemu_put_be64(f, RAM_SAVE_FLAG_EOS);
expected_time = ram_save_remaining() * TARGET_PAGE_SIZE / bwidth;
return (stage == 2) && (expected_time <= migrate_max_downtime());
}
  • 第一阶段,(14-42),代码所有的内存被标记为脏页并初始化页面更新追踪机制
  • 第二阶段,(47-55),如果页面被标记为脏页,则传输这些页面,页面脏位被重置,如果有程序修改页面,页面的脏位又可设置。一般来说,第二阶段是迭代过程最长的
  • 第三阶段,(71-79),那些修改过的却还没有来得及被传输到目的端的页面可以用来计算停机时间,设置一个目标停机时间,当达到这个值的时候,停止第二阶段的迭代过程,进入第三阶段,这时源虚拟机被暂停,将剩余的页面、CPU等状态一同传输到目的端,目的端再重新恢复虚拟机。

pre-copy总体来说能带来很小的停机时间,但不太适合写密集型的负载,写密集型负载会大量更新页面,使得迭代过程结束后的剩余页面增多,延长停机时间。

下面再来看看post-copy算法

Post-copy算法的思想是先暂停源虚拟机,把能保证一次正常运行的最小运行集(所有的CPU状态)传输到目的端,目的端恢复虚拟机的执行,若需要内存页,则产生页错误,主动从源虚拟机中获取。Post-copy能保证尽可能的做到个内存也最多只传输一次,避免pre-copy算法迭代过程中的重复传输;由于不断地从源端获取丢失页,不可避免地带来性能损失。VEE’09 Post-Copy Based Live Virtual Machine Migration Using Adaptive Pre-Paging and Dynamic Self-Ballooning 利用了一种称之为adaptive pre-paging的方法来减少页错误,adaptive pre-paging能尽可能的预测出目的端下一个需要的页面,从而减少页面传输的次数。

实现 Pre-copy

这一章节主要讲述KVM/Qemu关于Pre-copy迁移算法的实现,基于qemu-kvm-1.1.2版本。首先看一下源码中的hmp-commands.hx文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
.name = "migrate",
.args_type = "detach:-d,blk:-b,inc:-i,uri:s",
.params = "[-d] [-b] [-i] uri",
.help = "migrate to URI (using -d to not wait for completion)"
"\n\t\t\t -b for migration without shared storage with"
" full copy of disk\n\t\t\t -i for migration without "
"shared storage with incremental copy of disk "
"(base image shared between src and destination)",
.mhandler.cmd = hmp_migrate,
},
STEXI
@item migrate [-d] [-b] [-i] @var{uri}
@findex migrate
Migrate to @var{uri} (using -d to not wait for completion).
-b for migration with full copy of disk
-i for migration with incremental copy of disk (base image is shared)
ETEXI

每一个Qemu相关命令都需要在此文件中注册,如savevm,snapshot,migrate等,如果想自定义命令,亦是如此,关于如何修改KVM/Qemu源码,可以结合我的 上一篇 文章.

Qemu-kvm利用hmp-commands.hx这个文件保存相应的命令行参数以及常量,然后使用hxtool工具产生对应的头文件hmp-commands.h./x86_64-softmmu文件夹中,这个过程自动进行。注,STEXI与ETEXI之间的内容属于注释内容。从代码中可看到,与迁移命令migrate相对应的处理函数是hmp_migrate,从hmp_migrate函数开始,会依次调用qmp_migrate,tcp_start_outgoing_migration,migrate_fd_connect, migrate_fd_put_ready,具体可看源码,不一一详细介绍。

重点说一下migrate_fd_connect函数与migrate_fd_put_ready函数,
migrate_fd_connect函数主要调用了qemu_savevm_state_begin函数进行迁移工作的初始化工作(对应于前文说的迁移过程的第一阶段),而migrate_fd_connect函数主要调用了qemu_savevm_state_iterate函数(对应第二阶段)与qemu_savevm_state_complete函数(对应第三阶段),这里注意,此三个函数(qemu_savevm_state_beging,qemu_savevm_state_iterate,qemu_savevm_state_complete)里面代码结构非同类似,必有蹊跷,这里贴出其中的qemu_savevm_state_iterate函数:

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
int qemu_savevm_state_iterate(QEMUFile *f)
{
SaveStateEntry *se;
int ret = 1;
QTAILQ_FOREACH(se, &savevm_handlers, entry) {
if (se->save_live_state == NULL)
continue;
/* Section type */
qemu_put_byte(f, QEMU_VM_SECTION_PART);
qemu_put_be32(f, se->section_id);
ret = se->save_live_state(f, QEMU_VM_SECTION_PART, se->opaque);
if (ret <= 0) {
/* Do not proceed to the next vmstate before this one reported
completion of the current stage. This serializes the migration
and reduces the probability that a faster changing state is
synchronized over and over again. */
break;
}
}
if (ret != 0) {
return ret;
}
ret = qemu_file_get_error(f);
if (ret != 0) {
qemu_savevm_state_cancel(f);
}
return ret;
}

三个函数都会有一句代码

ret = se->save_live_state(f, QEMU_VM_SECTION_PART, se->opaque); 

只是其中参数不同。

这个save_live_state是什么?

注意,非常重要,存在于虚拟机迁移的核心结构体SaveStateEntry中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct SaveStateEntry {
QTAILQ_ENTRY(SaveStateEntry) entry;
char idstr[256];
int instance_id;
int alias_id;
int version_id;
int section_id;
SaveSetParamsHandler *set_params;
SaveLiveStateHandler *save_live_state;
SaveStateHandler *save_state;
LoadStateHandler *load_state;
const VMStateDescription *vmsd;
void *opaque;
CompatEntry *compat;
int no_migrate;
int is_ram;
} SaveStateEntry;

此结构体存储了虚拟机迁移的用到的所有数据结构,主要包括被传输设备的存储格式以及被调用的具体设备的迁移功能函数. 那指针 *save_live_state 到底做了什么,一一追踪可发现,在vl.c文件中的main函数中(整个qemu程序的开始),针对ram设备,可发现如下一段代码:

register_savevm_live(NULL, "ram", 0, 4, NULL, ram_save_live, NULL, ram_load, NULL);

正是register_savem_live函数将ram_save_live指针传递给了save_live_state,前文说了,ram_save_live便是真正执行迁移工作的函数,这里如果需要自定义迁移工作,修改ram_save_live注册到register_savevm_live函数中就行了。了解清楚这一连串的函数调用关系,便能彻底明白迁移的每一步工作。

思考

本文重点介绍了pre-copy迁移算法的详细过程,并简单介绍了post-copy算法,两个算法各有优缺点,也都各有改进之处。虚拟机迁移的初衷是保证云平台的高可用性,高可用性要尽量减少提供服务的云主机的宕机时间即停机时间,在此同时,也应尽量减少迁移过程中带来的性能开销,就像post-copy若不断的缺页,虽保证了极短的宕机时间,但如果性能损失太大也是无法接受的。目前多数优化迁移算法的工作主要是采取减少传输的内存数据量来实现,而为了减少内存数据量,又有:

  • 压缩内存
  • 基于hash指纹找出相同或类似页面去重
  • 尽可能传输不必要的页面如free页面等

除此之外,也有工作不传输整个内存页面,而是传输内存页面到外部设备的映射关系,目的端则靠此映射关系从外设获取数据。这里不一一列出相关论文,若有兴趣深入者,可自行查阅。笔者也有一部分工作提出了相应的思路与实现,之后会有专门文章作详细介绍。

如果你对迁移算法的优化有什么看法或什么建议,可留言,也可直接与我邮件联系。

参考

崔磊先生的博士论文

NSDI’05 Live Migration of Virtual Machines

VEE’09 Post-Copy Based Live Virtual Machine Migration Using Adaptive Pre-Paging and Dynamic Self-Ballooning


CSysSec注: 本文来自Diting0x个人博客,主要介绍虚拟机迁移技术,包括虚拟机迁移主要算法以及在KVM/QEMU平台上的迁移细节。
转载本文请务必注明,文章出处:《云平台高可用之虚拟机迁移》与作者信息:Diting0x