文章目录
- 迁移概述
- 迁移模型
- 传输方式
- 迁移准备
- 内存迁移
- 源端
- 迁移发起
- 迁移准备
- 迁移拷贝
- 迁移结束
- 目的端
- 迁移发起
- 迁移拷贝
迁移概述
迁移模型
- qemu内存迁移的有三个阶段:
- 标脏所有的内存页
- 迭代迁移所有脏页,直到剩余脏页降低到一定水线
- 暂停虚拟机,一次性迁移剩余脏页,然后迁移设备状态,启动目的端虚拟机
- 迁移第一阶段会把所有页标脏,首次迁移肯定会传输所有内存页,第二次迁移前如果计算得到的剩余脏页降低到水线以下,可以暂停虚拟机剩余脏页一次性迁移完,因此迁移最理想的状态是迭代两次;当虚拟机内存变化大时,会不断有脏页产生,迟迟不能降到水线以下,内存变化越大迁移越难收敛,最糟糕的情况是内存脏页永远无法降到水线以下,迁移永远无法完成
- 针对上述问题,qemu提出postcopy迁移模式,把传统迁移模式称为precopy,两种模型的不同点在于第二次及其之后的内存脏页拷贝时机不同。precopy模型的脏页拷贝在目的端虚拟机启动之前必须完成;postcopy模型的脏页拷贝在启动之后还会继续。
- postcopy的内存迁移也有三个阶段:
- 迁移设备状态
- 标脏所有内存页,将源端所有内存页拷贝到目的端,启动虚拟机
- 当目的端虚机访问到内存脏页时,会触发缺页异常,qemu从源端拷贝脏页对应内存
传输方式
- fd:qemu接收来自其它进程传入的fd,将数据写入。
- tcp:qemu往tcp sockets中写入数据。
- unix:qemu往unix sockets中写入数据。
- exec:qemu通过标准输入/输出传输数据。
通常libvirt控制的虚机迁移都使用fd迁移,这是一种方便管理的迁移方式,由上层应用程序(libvirt)打开文件描述符,qemu只负责往描述符里面发送数据,这样可以把迁移的控制层和实现层完全分开。
迁移准备
- 迁移前首先由libvirt进程打开目的端的socket fd,准备好目的端的迁移进程,之后将fd传递给qemu,写入内存数据。有一点需要注意,libvirt准备好目的端socket fd后,将fd传给了qemu继续写入内容,这是通过linux高级进程通信实现的,文件描述符在两个独立进程间传递,共享文件偏移量以及文件状态等。
- libvirt传送fd给qemu的目的,是为了将struct file指针共享给qemu,这样qemu可以继libvirt之后,写入内存信息到fd。这种情况与fork之后,父子进程共享打开的文件描述符一样。不过这里不是父子进程,因此需要用传送文件描述符的方式实现。libvirt传送fd给qemu的示意图如下:
内存迁移
源端
- qemu的内存迁移从
qmp_migrate
开始,核心函数是migration_thread
,中间的流程不做介绍,大致如下:
qmp_migrate()
fd_start_outgoing_migration(s, p, &local_err) /* fd迁移 */
int fd = monitor_get_fd(cur_mon, fdname, errp) /* 取出libvirt传入的fd */
ioc = qio_channel_new_fd(fd, errp); /* 将fd封装成QIOChannel */
migration_channel_connect(s, ioc, NULL, NULL)
migrate_fd_connect
qemu_thread_create(&s->thread, "live_migration", migration_thread, s, QEMU_THREAD_JOINABLE)
- 真正开始迁移内存是在
migration_thread
中,分为4个阶段:
- 迁移发起阶段,发送内存迁移头部,标志迁移开始,头部信息包括魔数,版本以及虚机的配置信息(machine type,page bits等)
- 迁移准备阶段,标脏所有的内存块,发送所有内存块的名字和大小
- 迁移拷贝阶段,迁移内存块数据,计算迁移内存速度
- 迁移结束阶段,统计内存块中剩余脏数据大小,对比迁移速度,评估是否可以暂停虚机一次性迁移所有内存
迁移发起
- 迁移发起阶段发送迁移头部信息,magic和version是固定需要发送的,配置section作为可选信息,视具体machine类型而定
migration_thread
qemu_savevm_state_header(s->to_dst_file)
void qemu_savevm_state_header(QEMUFile *f)
{
trace_savevm_state_header();
qemu_put_be32(f, QEMU_VM_FILE_MAGIC);
qemu_put_be32(f, QEMU_VM_FILE_VERSION);
if (migrate_get_current()->send_configuration) {
qemu_put_byte(f, QEMU_VM_CONFIGURATION);
vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0);
}
}
迁移准备
- 迁移准备阶段主要作两件事,一是标脏所有内存,二是发送所有需要迁移的内存块名字和大小到目的端
migration_thread
qemu_savevm_state_setup
void qemu_savevm_state_setup(QEMUFile *f)
{
SaveStateEntry *se;
Error *local_err = NULL;
int ret;
trace_savevm_state_setup();
QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {
if (!se->ops || !se->ops->save_setup) {
continue;
}
if (se->ops && se->ops->is_active) {
if (!se->ops->is_active(se->opaque)) {
continue;
}
}
save_section_header(f, se, QEMU_VM_SECTION_START);
ret = se->ops->save_setup(f, se->opaque);
save_section_footer(f, se);
if (ret < 0) {
qemu_file_set_error(f, ret);
break;
}
}
if (precopy_notify(PRECOPY_NOTIFY_SETUP, &local_err)) {
error_report_err(local_err);
}
}
- SaveStateEntry主要分为两类,一类是ram类型,以"ram" SaveStateEntryse为代表,定义了ops接口;另一类是VMState类型,这类se定义了vmsd字段,代表设备状态。准备阶段针对内存块的全局链表
ram_list.blocks
,它将所有可迁移的内存块都标脏,然后发送所有内存块名字和长度,流程如下:
ram_save_setup
/* step1: 内存标脏*/
ram_init_all
ram_init_bitmaps
/* 标脏所有内存块,将ram_list上所有RAMBlock.bmap设置为1 */
ram_list_init_bitmaps()
/* 通知监听qemu地址空间的所有listener,开启脏页记录 */
memory_global_dirty_log_start()
migration_bitmap_sync_precopy(rs)
/*step2: 发送所有内存块名字和长度 */
RAMBLOCK_FOREACH_MIGRATABLE(block) {
qemu_put_byte(f, strlen(block->idstr));
qemu_put_buffer(f, (uint8_t *)block->idstr, strlen(block->idstr));
qemu_put_be64(f, block->used_length);
if (migrate_postcopy_ram() && block->page_size != qemu_host_page_size) {
qemu_put_be64(f, block->page_size);
}
if (migrate_ignore_shared()) {
qemu_put_be64(f, block->mr->addr);
qemu_put_byte(f, ramblock_is_ignored(block) ? 1 : 0);
}
}
- 这里稍微分析一下Qemu通知内核记录脏页和同步脏页的流程
1. Qemu通知内核开启脏页记录:
memory_global_dirty_log_start
/* 标记开启全局标脏,当新添加的内存时也会开启脏页记录
该标记会在listener_add_address_space流程中用到
*/
global_dirty_log = true;
/* 通知所有实现了log_global_start回调的内存Listener,开启全局标脏
这里kvm并没有实现,所以到这一步时没有通知内核
*/
MEMORY_LISTENER_CALL_GLOBAL(log_global_start, Forward);
/* 设置标记,更新地址空间的所有区域 */
memory_region_update_pending = true;
/* 更新地址空间所有区域,这里才真正通知内核开启内存标脏 */
memory_region_transaction_commit();
/* 这里的标记在之前被设置为true,因此进入这个路径 */
if (memory_region_update_pending) {
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
address_space_set_flatview(as);
address_space_update_ioeventfds(as);
}
}
address_space_set_flatview
address_space_update_topology_pass
if (frnew->dirty_log_mask & ~frold->dirty_log_mask) {
MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start,
frold->dirty_log_mask, frnew->dirty_log_mask);
}
kvm_log_start
kvm_section_update_flags
kvm_slot_update_flags
/* 设置脏页开启标记KVM_MEM_LOG_DIRTY_PAGES
内核根据该标记跟踪内存脏页
*/
mem->flags = kvm_mem_flags(mr)
flags |= KVM_MEM_LOG_DIRTY_PAGES
kvm_set_user_memory_region
/* 遍历所有匹配的slot,对每个slot设置脏页开启标志
这个地方真正通知内核记录脏页
*/
kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)
2. 同步内核的脏页记录Qemu的ram_list数据结构中:
migration_bitmap_sync_precopy
migration_bitmap_sync
memory_global_dirty_log_sync
memory_region_sync_dirty_bitmap
/* 回调kvm实现的日志同步接口 */
listener->log_sync(listener, &mrs)
kvm_log_sync
kvm_physical_sync_dirty_bitmap
/* 从内核查询脏页记录 */
kvm_vm_ioctl(s, KVM_GET_DIRTY_LOG, &d)
/* 将内核查询到的脏页记录同步到用户态 */
kvm_get_dirty_pages_log_range(&subsection, d.dirty_bitmap)
cpu_physical_memory_set_dirty_lebitmap
/* Qemu的脏页保存在ram_list.dirty_memory[]数组中
首先获取它的指针
*/
for (i = 0; i < DIRTY_MEMORY_NUM; i++) {
blocks[i] = qatomic_rcu_read(&ram_list.dirty_memory[i])->blocks;
}
/* 将内核态的脏页内容保存到ram_list.dirty_memory[]数组中 */
qatomic_or(&blocks[DIRTY_MEMORY_MIGRATION][idx][offset], temp);
3. 将ram_list数据结构中的脏页记录同步到每个RAMBlock的bmap位图中
migration_bitmap_sync_precopy
migration_bitmap_sync
/* 针对每个RAMBlock,设置bmap位图 */
RAMBLOCK_FOREACH_NOT_IGNORED(block) {
ramblock_sync_dirty_bitmap(rs, block, 0, block->used_length);
}
cpu_physical_memory_sync_dirty_bitmap
/* 将ram_list.dirty_memory[]的数据给bmap */
unsigned long *dest = rb->bmap;
src = qatomic_rcu_read(&ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION])->blocks;
迁移拷贝
- 迁移拷贝阶段只做一件事情,拷贝内存数据到目的端,流程如下:
migration_thread
migration_iteration_run
qemu_savevm_state_iterate
/* 发送section type到目的端,标记part section的开始 */
save_section_header(f, se, QEMU_VM_SECTION_PART)
/* 拷贝内存 */
ret = se->ops->save_live_iterate(f, se->opaque)
save_section_footer(f, se)
- 内存拷贝针对"ram" se,调用"ram" se定义的
save_live_iterate
,流程如下:
ram_save_iterate
/* 发送内存数据到目的端,返回发送的内存页个数 */
pages = ram_find_and_save_block(rs, false)
/* 从位图中查找上一次拷贝之后,新增的脏页,如果找到了,拷贝脏页 */
found = find_dirty_block(rs, &pss, &again)
if (found) {
pages = ram_save_host_page(rs, &pss, last_stage);
}
ram_save_target_page
ram_save_page
save_normal_page
-
save_normal_page
实现对一个内存页的迁移,函数如下:
/*
* directly send the page to the stream
*
* Returns the number of pages written.
*
* @rs: current RAM state
* @block: block that contains the page we want to send
* @offset: offset inside the block for the page
* @buf: the page to be sent
* @async: send to page asyncly
*/
static int save_normal_page(RAMState *rs, RAMBlock *block, ram_addr_t offset,
uint8_t *buf, bool async)
{
/* 发送内存页头部信息 */
ram_counters.transferred += save_page_header(rs, rs->f, block,
offset | RAM_SAVE_FLAG_PAGE);
/* 发送内存页的内容 */
if (async) {
qemu_put_buffer_async(rs->f, buf, TARGET_PAGE_SIZE,
migrate_release_ram() &
migration_in_postcopy());
} else {
qemu_put_buffer(rs->f, buf, TARGET_PAGE_SIZE);
}
ram_counters.transferred += TARGET_PAGE_SIZE;
ram_counters.normal++;
return 1;
}
迁移结束
- qemu每拷贝一次内存之前,会统计一次剩余的脏页数量,对比域值后决定是否一次性迁移:
migration_iteration_run
/* 统计剩余脏页数量 */
qemu_savevm_state_pending(s->to_dst_file, s->threshold_size, &pend_pre,
&pend_compat, &pend_post);
pending_size = pend_pre + pend_compat + pend_post;
/* 比较剩余脏页数量与阈值大小 */
if (pending_size && pending_size >= s->threshold_size) {
......
} else {
/* 结束迁移 */
migration_completion(s);
return MIG_ITERATE_BREAK;
}
- 简单解释一下脏页统计流程,它的过程和迁移之前第一次脏页同步类似
qemu_savevm_state_pending
se->ops->save_live_pending <=> ram_save_pending
migration_bitmap_sync_precopy
migration_bitmap_sync
/* 调用内核接口将kvm记录的脏页取出保存在ram_list.dirty_bitmap中 */
memory_global_dirty_log_sync
memory_region_sync_dirty_bitmap
kvm_log_sync
/* 将ram_list.dirty_bitmap中保存的信息分别放到每个RAMBlock的bmap中 */
ramblock_sync_dirty_bitmap
cpu_physical_memory_sync_dirty_bitmap
- 迁移结束阶段有主要作三个事情,暂停cpu,拷贝内存,拷贝VMState,都在
migration_completion
里实现,如下:
migration_completion
/* 暂停虚机 */
vm_stop_force_state(RUN_STATE_FINISH_MIGRATE)
qemu_savevm_state_complete_precopy
/* 拷贝内存*/
save_section_header(f, se, QEMU_VM_SECTION_END)
ret = se->ops->save_live_complete_precopy(f, se->opaque) <=> ram_save_complete
ram_save_complete
if (!migration_in_postcopy()) {
/* 如果不是postcopy迁移,结束前再同步一次脏页 */
migration_bitmap_sync_precopy(rs);
/* 从全局的位图dirty_memory中同步脏页信息到RAMBlock */
migration_bitmap_sync
}
save_section_footer(f, se)
/* 拷贝VMState*/
save_section_header(f, se, QEMU_VM_SECTION_FULL)
vmstate_save(f, se, vmdesc)
save_section_footer(f, se)
目的端
- 目的端迁移的核心流程在
qemu_loadvm_state
中实现,流程如下:
qmp_migrate_incoming
qemu_start_incoming_migration
fd_start_incoming_migration
fd_accept_incoming_migration
migration_channel_process_incoming
migration_ioc_process_incoming
migration_incoming_process
process_incoming_migration_co
qemu_loadvm_state
- qemu_loadvm_state函数比较长,基本流程和发送端对应,我们逐一介绍。
迁移发起
- 迁移发起阶段,解析magic,version和configuration字段,同时调用setup回调函数
/* 解析magic字段 */
v = qemu_get_be32(f);
if (v != QEMU_VM_FILE_MAGIC) {
error_report("Not a migration stream");
return -EINVAL;
}
/* 解析version字段*/
v = qemu_get_be32(f);
if (v == QEMU_VM_FILE_VERSION_COMPAT) {
error_report("SaveVM v2 format is obsolete and don't work anymore");
return -ENOTSUP;
}
if (v != QEMU_VM_FILE_VERSION) {
error_report("Unsupported migration stream version");
return -ENOTSUP;
}
if (qemu_loadvm_state_setup(f) != 0) {
return -EINVAL;
}
/* 解析configuration section */
if (migrate_get_current()->send_configuration) {
if (qemu_get_byte(f) != QEMU_VM_CONFIGURATION) {
error_report("Configuration section missing");
qemu_loadvm_state_cleanup();
return -EINVAL;
}
ret = vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0);
if (ret) {
qemu_loadvm_state_cleanup();
return ret;
}
}
迁移拷贝
- 迁移拷贝主要逻辑在
qemu_loadvm_state_main
函数中,它从流中解析出section类型,由于section的格式相同,因此可以用相同方式解析section,不同类型的section,只有处理逻辑不同,如下:
int qemu_loadvm_state_main(QEMUFile *f, MigrationIncomingState *mis)
{
retry:
while (true) {
/* 从输入流中解析section类型 */
section_type = qemu_get_byte(f);
switch (section_type) {
/* start 和full section一起处理 */
case QEMU_VM_SECTION_START:
case QEMU_VM_SECTION_FULL:
ret = qemu_loadvm_section_start_full(f, mis);
......
break;
/* part和full section一起处理 */
case QEMU_VM_SECTION_PART:
case QEMU_VM_SECTION_END:
ret = qemu_loadvm_section_part_end(f, mis);
......
break;
case QEMU_VM_COMMAND:
ret = loadvm_process_command(f);
......
break;
case QEMU_VM_EOF:
/* This is the end of migration */
goto out;
default:
......
goto out;
}
}
......
}
- 因为start和full section传输的是VMState类型的section,因此需要作针对VMState field版本比较,如果不符合预期,目的端提前报错,而part和end section传输的是内存类型的section,不需要作版本判断,因此省略了这一步。这是将4类section分2种逻辑处理的主要原因。
qemu_loadvm_section_start_full
函数分析:
static int
qemu_loadvm_section_start_full(QEMUFile *f, MigrationIncomingState *mis)
{
uint32_t instance_id, version_id, section_id;
SaveStateEntry *se;
char idstr[256];
int ret;
/* Read section start */
section_id = qemu_get_be32(f);
if (!qemu_get_counted_string(f, idstr)) {
error_report("Unable to read ID string for section %u",
section_id);
return -EINVAL;
}
instance_id = qemu_get_be32(f);
version_id = qemu_get_be32(f);
ret = qemu_file_get_error(f);
if (ret) {
error_report("%s: Failed to read instance/version ID: %d",
__func__, ret);
return ret;
}
trace_qemu_loadvm_state_section_startfull(section_id, idstr,
instance_id, version_id);
/* Find savevm section */
se = find_se(idstr, instance_id);
if (se == NULL) {
error_report("Unknown savevm section or instance '%s' %d. "
"Make sure that your current VM setup matches your "
"saved VM setup, including any hotplugged devices",
idstr, instance_id);
return -EINVAL;
}
/* Validate version */
if (version_id > se->version_id) {
error_report("savevm: unsupported version %d for '%s' v%d",
version_id, idstr, se->version_id);
return -EINVAL;
}
se->load_version_id = version_id;
se->load_section_id = section_id;
/* Validate if it is a device's state */
if (xen_enabled() && se->is_ram) {
error_report("loadvm: %s RAM loading not allowed on Xen", idstr);
return -EINVAL;
}
ret = vmstate_load(f, se);
if (ret < 0) {
error_report("error while loading state for instance 0x%x of"
" device '%s'", instance_id, idstr);
return ret;
}
if (!check_section_footer(f, se)) {
return -EINVAL;
}
return 0;
}
- 函数从输入流中解析出section_id,idstr和instance_id,然后根据idstr,instance_id这两个输入在savevm_state.handlers中查找满足条件的唯一SaveStateEntry,只有idstr和instance_id都相等的se,才符和条件。查找过程如下:
static SaveStateEntry *find_se(const char *idstr, int instance_id)
{
SaveStateEntry *se;
QTAILQ_FOREACH(se, &savevm_state.handlers, entry) {
if (!strcmp(se->idstr, idstr) &&
(instance_id == se->instance_id ||
instance_id == se->alias_id))
return se;
/* Migrating from an older version? */
if (strstr(se->idstr, idstr) && se->compat) {
if (!strcmp(se->compat->idstr, idstr) &&
(instance_id == se->compat->instance_id ||
instance_id == se->alias_id))
return se;
}
}
return NULL;
}
- 从这里我们可以更加确认,savevm_state.handlers维护的链表中可以有相同idstr的se,如果这样,它们用instance_id或者alias_id来区分。因此SaveStateEntry.idstr这个域表示的仅仅是相同类型se的名字,同类se中还有不同se实例,这些实例在链表中通过instance_id和alias_id区分。
- 根据输入流中的idstr找到本地SaveStateEntry之后,继续解析流中的version_id字段,将其同SaveStateEntry.version_id字段比较,如果输入流中解析的版本比本地的高,说明源端的VMState有过修改并增加了版本ID,而目的端还未做修改,这是种低版本迁移高版本的情况,不能被允许,因此报错。具体的原理,请参考qemu设备迁移
/* Validate version */
if (version_id > se->version_id) {
error_report("savevm: unsupported version %d for '%s' v%d",
version_id, idstr, se->version_id);
return -EINVAL;
}
- 版本判断完成之后,就是调用vmstate_load加载VMState。完成对start/full section的解析。对于part/end section,处理逻辑类似,但少去了版本判断这一个环境,因为这两个section是内存,没有版本判断这个机制。