2011年(38)
分类: LINUX
2011-05-17 00:21:43
虚拟机活迁移揭秘
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, 文件路径等。用户还可以通过编写插件,支持更多的途径。
void qemu_start_incoming_migration ( const char *uri)
{
const char *p;
if (strstart(uri, " tcp :" , &p))
tcp_start_incoming_migration(p);
#if !defined(WIN32)
else if (strstart(uri, " exec :" , &p))
exec_start_incoming_migration(p);
else if (strstart(uri, " unix :" , &p))
unix_start_incoming_migration(p);
else if (strstart(uri, " fd :" , &p))
fd_start_incoming_migration(p);
#endif
else
fprintf(stderr, "unknown migration protocol: %s\n" , uri);
}
这个函数表明了 KVM 支持的迁移通道的种类。
本文基于 qemu-kvm-0.12.1.2 描述。
KVM 活迁移实验
我使用一台电脑上的一个虚拟机实验活迁移。
首先,使用 kvm 正常打开一个虚拟机。 sudo kvm ./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 命令。
然后可以输入 info migrate 查看实时的迁移状态。 迁移完成后,我们可以看到第二个虚拟机从第一台虚拟机开始迁移的地方开始运行了。状态完全一致,网络也没有断!
在第一台虚拟机上,按下 Ctrl-1 切入虚拟机界面,可以看到我们已经无法操作这个界面了。因此它进入了暂停状态。
KVM 虚拟机活迁移至此就结束了。
使用 virt-manager 也可以实现对 KVM 等等虚拟机的活迁移。但它实际上也是使用 KVM 活迁移方法来实现的。
使用 virt-manager 活迁移 KVM 虚拟机的步骤请看:《 KVM 虚拟机在物理主机之间迁移的实现
》 http://www.ibm.com/developerworks/cn/linux/l-cn-mgrtvm2/index.html 一文。
KVM 活迁移内幕.hx 文件
KVM 使用 qemu-options.hx 这个文件保存 KVM 命令行参数和对应的常量。然后使用一种技术,产生对应的 C 头文件和源文件。
libvirt 也使用了同样的技术,忘了名字了。
qemu-option.h 和 qemu-option.c 文件中有 KVM 命令行参数的一些辅助代码。
如上节所术, KVM 活迁移的目标虚拟机会进入暂停状态,等待活迁移结束。
KVM 活迁移的源虚拟机,需要使用 monitor 的命令实施迁移和监控迁移的状态。
qemu-monitor.hx 文件保存了 monitor 的命令和对应的响应函数。
Migrate 命令的配置:
STEXI
@item nmi @var{cpu}
Inject an 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 -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)",
.user_print = monitor_user_noop,
.mhandler.cmd_new = do_migrate ,
},
可见 do_migrate 函数是响应 migrate 命令的函数。
struct SaveStateEntry 结构体
/*
* 保存虚拟机状态入口
* */
typedef struct SaveStateEntry {
QTAILQ_ENTRY(SaveStateEntry) entry ;
char idstr [256];
int instance_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 ;
} SaveStateEntry ;
struct SaveStateEntry 是虚拟机活迁移的核心结构体。
static QTAILQ_HEAD(savevm_handlers, SaveStateEntry) savevm_handlers =
QTAILQ_HEAD_INITIALIZER(savevm_handlers);
/* TODO : Individual devices generally have very little idea about the rest
of the system, so instance_id should be removed/replaced.
Meanwhile pass -1 as instance_id if you do not already have a clearly
distinguishing id for all instances of your device class.
独立设备,所以 instance_id 应该是可删除 / 可替换的。
如果你的设备类型的所有实例不是很清楚其中的分别,那么传递 -1 给 instance_id
*/
int register_savevm_live ( const char *idstr,
int instance_id,
int version_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 );
return 0;
}
所有支持虚拟机活迁移的虚拟设备,都需要调用 register_savevm_live 方法,提供保存状态的 SaveLiveStateHandler *save_live_state 函数,供活迁移开始时被调用。
注册后, SaveStateEntry 对象就加入了 savevm_handlers 链表中。
块设备活迁移的注册代码:
/*
* 块设备活迁移初始化
* */
void blk_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 函数
do_ migrate 函数调用:
// 真正的迁移方法
void migrate_fd_connect ( FdMigrationState *s)
{
int ret;
// 返回 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( "beginning savevm \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 内存。
* */
void bdrv_set_dirty_tracking ( BlockDriverState *bs, int enable)
{
int64_t bitmap_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 用于记录在此(活迁移开始)之后所有写入数据的扇区。
// 位图的数组
unsigned long * 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 sector number or nb_sectors
-EACCES Trying to write a read-only device
*/
int bdrv_write ( BlockDriverState *bs, int64_t sector_num,
const uint8_t *buf, int nb_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 格式没有定义这个函数 ?
return drv-> bdrv_write (bs, sector_num, buf, nb_sectors);
}
如: bdrv_write 函数会在 bs-> dirty_bitmap 不为空时,调用 set_dirty_bitmap 。
#define BDRV_SECTORS_PER_DIRTY_CHUNK 2048
/*
* 设置脏位图
*
* */
static void set_dirty_bitmap ( BlockDriverState *bs, int64_t sector_num,
int nb_sectors, int dirty)
{
int64_t start, end;
unsigned long val, 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 ( unsigned long ) * 8);
bit = start % ( sizeof ( unsigned long ) * 8);
val = bs-> dirty_bitmap [idx];
if (dirty) {
val |= 1 << bit;
} else {
val &= ~(1 << bit);
}
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 函数。
/*
* 在块迁移完毕后保存内存
* */
void migrate_fd_put_ready ( void *opaque)
{
FdMigrationState *s = opaque;
if (s-> state != MIG_STATE_ACTIVE) {
dprintf( "put_ready returning 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) {
int state;
int old_vm_running = vm_running;
dprintf( "done iterating\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 函数执行完毕。源虚拟机处于暂停状态。
如果虚拟机迁移失败,那么虚拟机恢复运行。
块设备的活迁移函数我们实际考察一下块设备的活迁移函数。
#define QEMU_VM_SECTION_START 0x01
#define QEMU_VM_SECTION_PART 0x02
#define QEMU_VM_SECTION_END 0x03
#define QEMU_VM_SECTION_FULL 0x04
不同的迁移阶段,传递不同的 stage 到函数中,执行不同的工作。
/*
* 块活保存函数
* */
static int block_save_live ( Monitor *mon, QEMUFile *f, int stage, void *opaque)
{
dprintf( "Enter save live stage %d submitted %d transferred %d\n" ,
stage, block_mig_state.submitted, block_mig_state.transferred);
if (stage < 0) {
blk_mig_cleanup(mon);
return 0;
}
if (block_mig_state. blk_enable != 1) {
/* no need to migrate storage */
qemu_put_be64(f, BLK_MIG_FLAG_EOS);
return 1;
}
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);
return 0;
}
/* 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);
return 0;
}
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 << BDRV_SECTOR_BITS) | BLK_MIG_FLAG_PROGRESS);
if (qemu_file_has_error(f)) {
return 0;
}
monitor_printf(mon, "Block migration 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: