文章目录

  • 迁移概述
  • 迁移模型
  • 传输方式
  • 迁移准备
  • 内存迁移
  • 源端
  • 迁移发起
  • 迁移准备
  • 迁移拷贝
  • 迁移结束
  • 目的端
  • 迁移发起
  • 迁移拷贝


迁移概述

迁移模型

  • qemu内存迁移的有三个阶段:
  1. 标脏所有的内存页
  2. 迭代迁移所有脏页,直到剩余脏页降低到一定水线
  3. 暂停虚拟机,一次性迁移剩余脏页,然后迁移设备状态,启动目的端虚拟机
  • 迁移第一阶段会把所有页标脏,首次迁移肯定会传输所有内存页,第二次迁移前如果计算得到的剩余脏页降低到水线以下,可以暂停虚拟机剩余脏页一次性迁移完,因此迁移最理想的状态是迭代两次;当虚拟机内存变化大时,会不断有脏页产生,迟迟不能降到水线以下,内存变化越大迁移越难收敛,最糟糕的情况是内存脏页永远无法降到水线以下,迁移永远无法完成
  • 针对上述问题,qemu提出postcopy迁移模式,把传统迁移模式称为precopy,两种模型的不同点在于第二次及其之后的内存脏页拷贝时机不同。precopy模型的脏页拷贝在目的端虚拟机启动之前必须完成;postcopy模型的脏页拷贝在启动之后还会继续。
  • postcopy的内存迁移也有三个阶段:
  1. 迁移设备状态
  2. 标脏所有内存页,将源端所有内存页拷贝到目的端,启动虚拟机
  3. 当目的端虚机访问到内存脏页时,会触发缺页异常,qemu从源端拷贝脏页对应内存

传输方式

  1. fd:qemu接收来自其它进程传入的fd,将数据写入。
  2. tcp:qemu往tcp sockets中写入数据。
  3. unix:qemu往unix sockets中写入数据。
  4. 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个阶段:
  1. 迁移发起阶段,发送内存迁移头部,标志迁移开始,头部信息包括魔数,版本以及虚机的配置信息(machine type,page bits等)
  2. 迁移准备阶段,标脏所有的内存块,发送所有内存块的名字和大小
  3. 迁移拷贝阶段,迁移内存块数据,计算迁移内存速度
  4. 迁移结束阶段,统计内存块中剩余脏数据大小,对比迁移速度,评估是否可以暂停虚机一次性迁移所有内存

迁移发起

  • 迁移发起阶段发送迁移头部信息,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);
    }       
}

qemu 存储迁移 qemu文件交换_qemu 存储迁移

迁移准备

  • 迁移准备阶段主要作两件事,一是标脏所有内存,二是发送所有需要迁移的内存块名字和大小到目的端
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 存储迁移 qemu文件交换_文件描述符_02

目的端

  • 目的端迁移的核心流程在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是内存,没有版本判断这个机制。