分类: 虚拟化
2014-08-12 15:33:09
虚拟机活迁移揭秘
by沈东良(良少)http://blog.csdn.net/shendl
前几天有个朋友问我vmware虚拟机活迁移后台是怎样实现的。我给他讲解了KVM活迁移的原理。今天就在这里写出来分享。
vmware是闭源的,因此无从知道它的活迁移究竟是怎么做的。但是KVM的功能比vmware并不少,也实现了活迁移。Vmware的活迁移应该在原理上和KVM相同吧。
vmware和kvm的虚拟机活迁移,都需要image保存在共享存储上,如SAN,NAS等共享硬件设备,Lustre,MogileFS,Ceph等分布式文件系统上。这样,活迁移只需要迁移内存和虚拟硬件设备寄存器即可。
其实KVM现在也支持image不共享的活迁移。两台电脑上最初image是相同的。在一台虚拟机启动后,它的image的内容可能会发生变化。KVM的活迁移可以把变化的内容copy到目标虚拟机的image中,以使它们完全一致。
KVM活迁移,支持多种通讯格式,包括:tcp,ssh,文件路径等。用户还可以通过编写插件,支持更多的途径。
voidqemu_start_incoming_migration(constchar*uri)
{
constchar*p;
if(strstart(uri, "tcp:",&p))
tcp_start_incoming_migration(p);
#if!defined(WIN32)
elseif(strstart(uri, "exec:",&p))
exec_start_incoming_migration(p);
elseif(strstart(uri, "unix:",&p))
unix_start_incoming_migration(p);
elseif(strstart(uri, "fd:",&p))
fd_start_incoming_migration(p);
#endif
else
fprintf(stderr, "unknownmigration protocol: %s/n", uri);
}
这个函数表明了KVM支持的迁移通道的种类。
本文基于qemu-kvm-0.12.1.2描述。
我使用一台电脑上的一个虚拟机实验活迁移。
首先,使用kvm正常打开一个虚拟机。sudokvm ./xp1.qcow2
然后再使用kvm命令打开同一个虚拟机。使用如下命令:sudo kvm ./xp1.qcow2 -incoming tcp:0.0.0.0:11111
读者可能会说,两台虚拟机同时使用同一个image,会造成image数据的丢失,可能会彻底破坏整个虚拟硬盘的数据完整性,从而造成数据丢失,甚至操作系统都无法启动!
是的,但这里的-incoming选项,实际上并没有真正启动虚拟机。它首先创建TCP等链接,准备接受虚拟机迁移的数据传入,然后就暂停了虚拟机的执行。直到虚拟机迁移完成后,才会恢复进入虚拟机运行状态。因此,它在迁移完成前,并没有操作虚拟磁盘,因此不会造成如上的问题。
然后,在网络上播放电影时,按下Ctrl-2切入KVM的monitor模式。
输入migrate -d tcp:127.0.0.1:11111 命令。
然后可以输入infomigrate 查看实时的迁移状态。迁移完成后,我们可以看到第二个虚拟机从第一台虚拟机开始迁移的地方开始运行了。状态完全一致,网络也没有断!
在第一台虚拟机上,按下Ctrl-1切入虚拟机界面,可以看到我们已经无法操作这个界面了。因此它进入了暂停状态。
KVM虚拟机活迁移至此就结束了。
使用virt-manager也可以实现对KVM等等虚拟机的活迁移。但它实际上也是使用KVM活迁移方法来实现的。
使用virt-manager活迁移KVM虚拟机的步骤请看:《KVM虚拟机在物理主机之间迁移的实现
》http://www.ibm.com/developerworks/cn/linux/l-cn-mgrtvm2/index.html 一文。
KVM使用qemu-options.hx这个文件保存KVM命令行参数和对应的常量。然后使用一种技术,产生对应的C头文件和源文件。
libvirt也使用了同样的技术,忘了名字了。
qemu-option.h和qemu-option.c文件中有KVM命令行参数的一些辅助代码。
如上节所术,KVM活迁移的目标虚拟机会进入暂停状态,等待活迁移结束。
KVM活迁移的源虚拟机,需要使用monitor的命令实施迁移和监控迁移的状态。
qemu-monitor.hx文件保存了monitor的命令和对应的响应函数。
Migrate命令的配置:
STEXI
@itemnmi @var{cpu}
Injectan NMI on the given CPU (x86 only).
ETEXI
{
.name = "migrate",
.args_type = "detach:-d,blk:-b,inc:-i,uri:s",
.params = "[-d] [-b] [-i] uri",
.help = "migrate to URI (using -dto 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)",
.user_print= monitor_user_noop,
.mhandler.cmd_new= do_migrate,
},
可见do_migrate函数是响应migrate命令的函数。
/*
*保存虚拟机状态入口
**/
typedefstructSaveStateEntry {
QTAILQ_ENTRY(SaveStateEntry)entry;
charidstr[256];
intinstance_id;
intversion_id;
intsection_id;
SaveSetParamsHandler*set_params;
SaveLiveStateHandler*save_live_state;
SaveStateHandler*save_state;
LoadStateHandler*load_state;
constVMStateDescription*vmsd;
void*opaque;
}SaveStateEntry;
structSaveStateEntry是虚拟机活迁移的核心结构体。
staticQTAILQ_HEAD(savevm_handlers, SaveStateEntry) savevm_handlers =
QTAILQ_HEAD_INITIALIZER(savevm_handlers);
/*TODO:Individual devices generally have very little idea about the rest
ofthe system, so instance_id should be removed/replaced.
Meanwhile pass -1 as instance_idif you do not already have a clearly
distinguishing id for allinstances of your device class.
独立设备,所以instance_id应该是可删除/可替换的。
如果你的设备类型的所有实例不是很清楚其中的分别,那么传递-1给instance_id
*/
intregister_savevm_live(constchar*idstr,
intinstance_id,
intversion_id,
SaveSetParamsHandler*set_params,
SaveLiveStateHandler*save_live_state,
SaveStateHandler*save_state,
LoadStateHandler*load_state,
void*opaque)
{
SaveStateEntry*se;
se= qemu_mallocz(sizeof(SaveStateEntry));
pstrcpy(se->idstr,sizeof(se->idstr),idstr);
se->version_id= version_id;
se->section_id= global_section_id++;
se->set_params= set_params;
se->save_live_state= save_live_state;
se->save_state= save_state;
se->load_state= load_state;
se->opaque= opaque;
se->vmsd= NULL;
if(instance_id == -1) {
se->instance_id= calculate_new_instance_id(idstr);
}else{
se->instance_id= instance_id;
}
/*add at the end of list
*驱动加到队列里
**/
QTAILQ_INSERT_TAIL(&savevm_handlers,se, entry);
return0;
}
所有支持虚拟机活迁移的虚拟设备,都需要调用register_savevm_live方法,提供保存状态的SaveLiveStateHandler*save_live_state函数,供活迁移开始时被调用。
注册后,SaveStateEntry对象就加入了savevm_handlers链表中。
块设备活迁移的注册代码:
/*
*块设备活迁移初始化
**/
voidblk_mig_init(void)
{
QSIMPLEQ_INIT(&block_mig_state.bmds_list);
QSIMPLEQ_INIT(&block_mig_state.blk_list);
register_savevm_live("block",0, 1, block_set_params, block_save_live,
NULL,block_load, &block_mig_state);
}
正是因为块设备注册了SaveStateEntry对象,才使KVM能够支持image不共享的活迁移。
do_migrate函数调用:
//真正的迁移方法
voidmigrate_fd_connect(FdMigrationState*s)
{
intret;
//返回QemuFile对象
s->file= qemu_fopen_ops_buffered(s,
s->bandwidth_limit,
migrate_fd_put_buffer,
migrate_fd_put_ready,
migrate_fd_wait_for_unfreeze,
migrate_fd_close);
dprintf("beginningsavevm/n");
ret =qemu_savevm_state_begin(s->mon,s->file,s->mig_state.blk,
s->mig_state.shared);
if(ret < 0) {
dprintf("failed,%d/n", ret);
migrate_fd_error(s);
return;
}
migrate_fd_put_ready(s);
}
ret= qemu_savevm_state_begin(s->mon,s->file,s->mig_state.blk,
s->mig_state.shared);
依次调用了每一个注册了register_savevm_live的设备的 SaveLiveStateHandler*save_live_state 函数,以保存活状态。
对于块设备,它会调用到:
/*
*对块设备启用ditrymap跟踪。 就是分配dirty_bitmap内存。
**/
voidbdrv_set_dirty_tracking(BlockDriverState*bs, intenable)
{
int64_tbitmap_size;
if(enable) {
if(!bs->dirty_bitmap){
bitmap_size =(bdrv_getlength(bs) >> BDRV_SECTOR_BITS) +
BDRV_SECTORS_PER_DIRTY_CHUNK* 8 - 1;
bitmap_size /=BDRV_SECTORS_PER_DIRTY_CHUNK * 8;
bs->dirty_bitmap= qemu_mallocz(bitmap_size);
}
}else{
if(bs->dirty_bitmap){
qemu_free(bs->dirty_bitmap);
bs->dirty_bitmap= NULL;
}
}
}
调用了bs->dirty_bitmap= qemu_mallocz(bitmap_size);
这个dirty_bitmap用于记录在此(活迁移开始)之后所有写入数据的扇区。
//位图的数组
unsignedlong*dirty_bitmap;它是long类型的数组。
/*Return < 0 if error. Important errors are:
-EIO generic I/O error (may happen for all errors)
-ENOMEDIUM No media inserted.
-EINVAL Invalid sectornumber or nb_sectors
-EACCES Trying to write aread-only device
*/
intbdrv_write(BlockDriverState*bs, int64_tsector_num,
constuint8_t*buf, intnb_sectors)
{
BlockDriver*drv = bs->drv;
if(!bs->drv)
return-ENOMEDIUM;
if(bs->read_only)
return-EACCES;
if(bdrv_check_request(bs, sector_num, nb_sectors))
return-EIO;
//设置哪些扇区脏了。这次写的扇区脏了!
if(bs->dirty_bitmap){
set_dirty_bitmap(bs, sector_num,nb_sectors, 1);
}
//qcow2格式没有定义这个函数?
returndrv->bdrv_write(bs,sector_num, buf, nb_sectors);
}
如:bdrv_write函数会在bs->dirty_bitmap不为空时,调用set_dirty_bitmap。
#defineBDRV_SECTORS_PER_DIRTY_CHUNK 2048
/*
*设置脏位图
*
**/
staticvoidset_dirty_bitmap(BlockDriverState*bs, int64_tsector_num,
intnb_sectors, intdirty)
{
int64_tstart, end;
unsignedlongval, idx, bit;
start = sector_num /BDRV_SECTORS_PER_DIRTY_CHUNK;
end = (sector_num + nb_sectors -1) / BDRV_SECTORS_PER_DIRTY_CHUNK;
for(; start <= end; start++) {
idx = start / (sizeof(unsignedlong)* 8);
bit = start % (sizeof(unsignedlong)* 8);
val = bs->dirty_bitmap[idx];
if(dirty) {
val |= 1 << bit;
} else{
val &= ~(1 <
}
bs->dirty_bitmap[idx]= val;
}
}
bitmap的一个bit表示2048个扇区,每一个扇区是512字节,因此一个bit就表示1MB字节。因此,dirty_bitmap只要1KB大小就可以表示8GB的硬盘。只要1MB大小就可以表示8TB的硬盘。内存是非常节省的。
这也同样意味着,虚拟机即使写一个扇区,在迁移时,也会同时迁移2048个扇区。
但是,我们知道,Linux内核的IO调度系统会合并相邻的块(一般是4KB大小)的读写请求。并且,一般的 文件系统都会尽量把文件的数据按照扇区的升序排列。
我们知道,硬盘的磁头定位很慢,但是数据读写还是很快的。因此一次多读写一些数据和一次少读写数据的性能差别并不大。
KVM的块设备活迁移正是利用了磁盘和文件系统的这个特点,用2048个扇区表示一个字节,大大减少了dirty_bitmap在内存中的大小。
除了bdrv_write函数外,其他所有写入虚拟磁盘数据的函数,都会调用set_dirty_bitmap函数。包括:bdrv_write_compressed,bdrv_reset_dirty,bdrv_aio_writev函数。
qemu_savevm_state_begin函数开始各个虚拟设备的迁移准备后,最后调用migrate_fd_put_ready函数。
/*
*在块迁移完毕后保存内存
**/
voidmigrate_fd_put_ready(void*opaque)
{
FdMigrationState*s = opaque;
if(s->state!= MIG_STATE_ACTIVE) {
dprintf("put_readyreturning because of non-active state/n");
return;
}
dprintf("iterate/n");
/*
*遍历每一种注册savevm的设备对象的函数,实现设备内存的活迁移。
ret= se->save_live_state(mon, f, QEMU_VM_SECTION_PART,se->opaque);
**/
if(qemu_savevm_state_iterate(s->mon,s->file)== 1) {
intstate;
intold_vm_running = vm_running;
dprintf("doneiterating/n");
//这里暂停虚拟机
vm_stop(0);
//传输所有还没有传输的数据块
qemu_aio_flush();
bdrv_flush_all();
//如果内存迁移失败,那么恢复虚拟机运行。返回迁移失败
if((qemu_savevm_state_complete(s->mon,s->file))< 0) {
if(old_vm_running) {
vm_start();
}
state = MIG_STATE_ERROR;
} else{
state = MIG_STATE_COMPLETED;
}
//迁移完成,释放资源
migrate_fd_cleanup(s);
s->state= state;
}
}
qemu_savevm_state_iterate函数,遍历每一种注册savevm的设备对象的函数,实现设备状态(包括虚拟硬盘)的活迁移。
这样,脏扇区和虚拟机的内存,硬件设备状态都迁移走了。
然后执行vm_stop(0);暂停虚拟机的执行。开始第二轮迁移,把上次迁移后的产生的新的脏数据也迁移走。因为虚拟机已经暂停了,因此不会再产生新的脏数据了。
最后完成虚拟机的迁移,do_migrate函数执行完毕。源虚拟机处于暂停状态。
如果虚拟机迁移失败,那么虚拟机恢复运行。
我们实际考察一下块设备的活迁移函数。
#defineQEMU_VM_SECTION_START 0x01
#defineQEMU_VM_SECTION_PART 0x02
#defineQEMU_VM_SECTION_END 0x03
#defineQEMU_VM_SECTION_FULL 0x04
不同的迁移阶段,传递不同的stage到函数中,执行不同的工作。
/*
*块活保存函数
**/
staticintblock_save_live(Monitor*mon, QEMUFile*f, intstage, void*opaque)
{
dprintf("Entersave live stage %d submitted %d transferred %d/n",
stage,block_mig_state.submitted, block_mig_state.transferred);
if(stage < 0) {
blk_mig_cleanup(mon);
return0;
}
if(block_mig_state.blk_enable!= 1) {
/*no need to migrate storage */
qemu_put_be64(f,BLK_MIG_FLAG_EOS);
return1;
}
if(stage == 1) {
init_blk_migration(mon, f);
/*start track dirty blocks */
set_dirty_tracking(1);
}
//把迁移块队列中的所有数据传送掉。
flush_blks(f);
if(qemu_file_has_error(f)) {
blk_mig_cleanup(mon);
return0;
}
/*control the rate of transfer */
while((block_mig_state.submitted+
block_mig_state.read_done)* BLOCK_SIZE <
qemu_file_get_rate_limit(f)){
//显示完成的比率,如果==1,完成
if(blk_mig_save_bulked_block(mon, f, 1) == 0) {
/*no more bulk blocks for now */
break;
}
}
flush_blks(f);
if(qemu_file_has_error(f)) {
blk_mig_cleanup(mon);
return0;
}
if(stage == 3) {
//同步传输数据
/*
* 如果没有完成,一致传输,知道完成。
* */
while(blk_mig_save_bulked_block(mon, f, 0) != 0) {
/*empty */
}
/*
*此时,所有磁盘数据应该已经同步了。
*现在,把脏块全部同步过去。
**/
blk_mig_save_dirty_blocks(mon,f);
//完成了,释放资源
blk_mig_cleanup(mon);
/*report completion */
qemu_put_be64(f, (100 <
if(qemu_file_has_error(f)) {
return0;
}
monitor_printf(mon, "Blockmigration completed/n");
}
qemu_put_be64(f,BLK_MIG_FLAG_EOS);
return((stage == 2) && is_stage2_completed());
}
如果你给KVM增加了一个新的虚拟设备,并且希望这个设备能够支持活迁移,那么你必须调用register_savevm_live函数,注册活迁移的回调函数。
KVM活迁移过程中,有几个过程,大致上包括:
start---开始活迁移的准备工作
第一轮迁移扇区、内存、寄存器
stop暂停虚拟机
第二轮迁移扇区、内存、寄存器
成功结束迁移,并销毁start时的一些资源。 或者迁移失败,恢复虚拟机的运行。
PS: