Libvirt在线迁移存储简介

文章目录

  • 基本工具
    • NBD
    • qemu-nbd
    • hmp nbd cmd
    • hmp drive mirror
  • 迁移流程
    • 公共流程
    • 流程图
    • Src Begin Phase
    • Dst Prepare Phase
    • Src Perform Phase
    • Dst Finish Phase
    • Src Confirm Phase

基本工具

NBD

  • nbd(Network Block Device)顾名思义,是一个可以通过网络访问的块设备,它通过运行在远端的一个nbd server将其上的块设备或镜像文件暴露给客户端使用。nbd相比网络文件系统比如NFS,它可以为客户端提供更底层的操作,比如对设备进行分区,格式化文件系统等。
  • nbd协议定义了客户端和服务端数据传输的访问规范,但没有规定必须基于哪种通信机制来实现,常见的实现是基于unix/tcp socket,不常见的可以基于管道来实现。centos上提供了nbdkit包,它集成了创建nbd server的工具包可以用来创建nbd server,客户端可以通过guestfish来访问这个nbd server。ubuntu上提供nbd-server和nbd-client工具用于创建和访问nbd server。
  • qemu也基于nbd协议实现了自己的nbd工具,qemu-nbd工具既可以创建nbd server,可以做为客户端访问远端server的块设备信息。但是,无论是nbd-client或者qemu-nbd,它们访问的都是块设备,而linux的块设备都是在内核态,它们都依赖linux nbd模块,这个模块提供一个中间块设备/dev/nbdX,它负责将用户态的读写操作转发到远端的块设备。

qemu-nbd

  • qemu-nbd是基于nbd协议实现的工具,它可以用来创建nbd server,以此将镜像文件暴露给远端,也可以作为一个客户端工具,访问nbd server暴露的块设备。另外,它也可以将镜像文件与内核的nbd设备关联,这样可以将镜像文件作为本地块设备进行读写。下面分别介绍这两种用法:
  1. 通过qemu-nbd工具将qcow2文件导出为远端可访问设备
/* 创建一个30G大小的qcow2磁盘 */
# qemu-img create -f qcow2 test.qcow2 30G

/* 通过qemu-nbd启动一个nbd服务,将创建的磁盘通过该服务对外暴露,客户端通过访问该服务读写该NBD设备 */
# qemu-nbd --fork -f qcow2 -x node1 -p 1234 test.qcow2

/* 本地查询NBD服务暴露的块设备 */
# qemu-nbd -L -p 1234 -b localhost						
exports available: 1
 export: 'node1'
  size:  32212254720
  flags: 0xced ( flush fua trim zeroes df cache fast-zero )
  min block: 1
  opt block: 4096
  max block: 33554432
  available meta contexts: 1
  base:allocation

/* 客户端工具查询服务暴露的块设备信息 */  
# qemu-img info nbd:localhost:1234:exportname=node1

/* 客户端工具通过uri查询块设备信息 */
# qemu-img info nbd://localhost:1234/node1
  1. 将qcow2文件导出为块设备,作为文件系统使用
/* 创建磁盘文件 */
# qemu-img create -f qcow2 test.qcow2 20G

/* 将磁盘文件导出为本地的块设备需要内核的nbd模块,加载该模块 */
# modprobe nbd

/* 将/dev/nbd0块设备作为磁盘文件关联的块设备,之后读写/dev/nbd0,内核就会将其信息转发给磁盘文件 */
# qemu-nbd -c /dev/nbd0 -f qcow2 file.qcow2

/* 格式化并使用该块设备,数据对应的转发到磁盘文件 */
# fdisk -l /dev/nbd0
# mkfs.ext4  /dev/nbd0

/* 挂载文件系统 */
# mount /dev/nbd0  /root/fs

/* 卸载文件系统 */
# umount /root/fs

/* 将内核块设备与磁盘文件解关联,之后对/dev/nbd0的读写操作不再转发到磁盘文件 */
# qemu-nbd -d /dev/nbd0 

hmp nbd cmd

  • qemu除了为用户提供使用nbd的工具以外,还提供hmp/qmp命令方便上层应用程序调用(比如libvirt),通过nbd的hmp命令,我们可以在qemu monitor交互命令行中使用hmp命令启动nbd server,如下:
/* 向qemu添加一个磁盘drive,让qemu可以访问 */
(qemu) drive_add test id=vda,if=none,format=qcow2,file=/path/to/test.qcow2
(qemu) info block
vda (#block135): /home/data/ubuntu1804 (qcow2)
    Removable device: not locked, tray closed
    Cache mode:       writeback
    
/* 为虚拟机添加一个设备,其后端是刚刚增加的drive*/
(qemu) device_add virtio-blk-pci,scsi=off,drive=vda
(qemu) info block
vda (#block135): /home/data/ubuntu1804 (qcow2)
    Attached to:      /machine/peripheral-anon/device[1]/virtio-backend
    Cache mode:       writeback

/* 通过qemu内置的hmp命令在主机上启动一个nbd server */
(qemu) nbd_server_start localhost:1234

/* 将qemu的一个磁盘drive暴露给nbd server,之后客户端就可以通过访问nbd服务访问该磁盘 */
(qemu) nbd_server_add  #block181 node1

/* 主机上使用qemu-nbd工具查看暴露的块设备,和使用qemu-nbd工具启动nbd server效果相同  */
qemu-nbd -L -p 1234 -b localhost

hmp drive mirror

  • qemu内部实现了drive mirror的功能,它可以将qemu管理的任何块设备mirror到一个qemu可以访问的目的磁盘上,可以看出drive mirror命令接收两个输入,一个是源端的磁盘,它是qemu管理的一个drive,一个是目的端磁盘,它是一个qemu可以访问的磁盘,这个磁盘可以是一个本地的文件,也可以是一个通过nbd server暴露的块设备。这里我们介绍drive mirror如何mirror到一个nbd块设备:
1. 源端
/* 向qemu增加一个dirve,让其可以访问 */
(qemu) drive_add test id=vda,if=none,format=qcow2,file=/path/to/src_test.qcow2

/* 向虚机增加一个设备,后端为之前增加的drive,这一步可以跳过
 * 如果不跳过,虚机内部可以看到该磁盘,可能会增加新的数据 */
(qemu) device_add virtio-blk-pci,scsi=off,drive=vda

/* 查询源块设备信息 */
(qemu) info block
vda (#block121): /home/data/ubuntu1804 (qcow2)
    Attached to:      /machine/peripheral-anon/device[1]/virtio-backend
    Cache mode:       writeback
 
 2. 目的端
 /* 准备好和源端相同大小的磁盘 */ 
qemu-img create -f qcow2 drive_mirror_dst.qcow2 400G

/* 通过nbd server导出该磁盘 */
qemu-nbd --fork -f qcow2 -x node1 -p 1236 drive_mirror_dst.qcow2

3. 源端
/* 使用drive_mirror命令将源端的磁盘mirror到目的端 
 * -f参数表示拷贝源端完整的磁盘内容,如果不加该选项,drive_mirror只拷贝源端新增的数据
 * 如果目的磁盘已经有内容,通过指定了-n选项reuse目的端磁盘,那么drive_mirro会覆盖写已有的目的端数据
 * -n表示如果目的端有已经存在的文件,使用现有的,不要重新创建
 * 否则会重新创建目的端文件,即使目的端文件存在也会删除重建 */
(qemu) drive_mirror -n -f  #block121 nbd:localhost:1236:exportname=node1 raw

/* drive_mirror完成后,可以使用cancel命令取消mirror,否则mirror动作为一直在后台进行 */
(qemu) block_job_cancel vda

迁移流程

  • libvirt使用如下命令迁移虚拟机内存和所有磁盘:
virsh migrate --live --copy-storage-all --xml {xmlfile} --persistent-xml {xmlfile} ......
  • 参数xml指定虚机迁移到目的之后定义虚机使用的xml文件,persistent-xml指定虚机迁移到目的端之后持久化的虚机xml文件。
  • Libvirt整机迁移整个流程如下分析如下:

公共流程

  • 所有Libvirt客户端命令入口函数都是cmd{Subcommand}的形式,迁移入口对应的就是cmdMigrate函数
cmdMigrate
	doMigrate
		/* 读取迁移的命令传入的xml文件并加载到内存 */
		vshCommandOptStringReq(ctl, cmd, "xml", &opt)
		virFileReadAll(opt, VSH_MAX_XML_FILE, &xml)
		virTypedParamsAddString(¶ms, &nparams, &maxparams, VIR_MIGRATE_PARAM_DEST_XML, xml)
		vshCommandOptStringReq(ctl, cmd, "persistent-xml", &opt)
   		virFileReadAll(opt, VSH_MAX_XML_FILE, &xml)
   		virTypedParamsAddString(¶ms, &nparams, &maxparams, VIR_MIGRATE_PARAM_PERSIST_XML, xml)
   		/* 如果有--live选项,设置对应的VIR_MIGRATE_LIVE flag */
        if (vshCommandOptBool(cmd, "live"))
        	flags |= VIR_MIGRATE_LIVE;
        /* 如果有copy-storage-all选项,设置对应的VIR_MIGRATE_NON_SHARED_DISK flag */
        if (vshCommandOptBool(cmd, "copy-storage-all"))
        	flags |= VIR_MIGRATE_NON_SHARED_DISK
        /* 迁移的所有方式中,如果是peer2peer或者direct
         * 连接目的端libvirtd服务接口的工作会交给源端的libvirtd服务来做
         * 除此之外的其它方式,客户端都既要连接源端的libvirtd服务
         * 又要连接目的端的libvirtd服务 */	
       	if (flags & VIR_MIGRATE_PEER2PEER || vshCommandOptBool(cmd, "direct")) {
        	virDomainMigrateToURI3(dom, desturi, params, nparams, flags)  	
    	} else {
        	virDomainMigrate3(dom, dconn, params, nparams, flags)
    	}
/* 我们分析客户端既连接源端libvirtd daemon,又连接目的端libvirtd daemon的情况 */
virDomainMigrate3
	virDomainMigrateVersion3
    	virDomainMigrateVersion3Full
  • 流程最终会走到virDomainMigrateVersion3Full函数,该函数有一个公共的流程如下:
 Src: Begin
       - Generate XML to pass to dst
       - Generate optional cookie to pass to dst

 Dst: Prepare
       - Get ready to accept incoming VM
       - Generate optional cookie to pass to src

 Src: Perform
       - Start migration and wait for send completion
       - Generate optional cookie to pass to dst

 Dst: Finish
       - Wait for recv completion and check status
       - Kill off VM if failed, resume if success
       - Generate optional cookie to pass to src

 Src: Confirm
       - Kill off VM if success, resume if failed
  • 整机迁移既包括对虚机内存的迁移,又包括对虚机磁盘的迁移,这里我们重点介绍Libvirt如何利用qemu的drive_mirror接口和nbd server接口,实现磁盘的迁移,包括源端和目的端的所有动作。

流程图

Libvirt在线迁移存储简介_第1张图片

Src Begin Phase

  • 源端的Begin阶段主要所迁移的准备工作,包括迁移检查是否能够进行磁盘迁移、查询命令行传入的磁盘参数是否正确、查询源端的磁盘容量信息传输到目的端等。
qemuDomainMigrateBegin3Params
	qemuMigrationSrcBegin
		qemuMigrationSrcBeginPhase
/* Begin阶段入口 */		
qemuMigrationSrcBeginPhase
	/* 如果命令行参数包含了copy-storage-all和copy-storage-inc,需要检查是否具有迁移的能力 */
	if (flags & (VIR_MIGRATE_NON_SHARED_DISK | VIR_MIGRATE_NON_SHARED_INC)) {
		/* 对于隧道迁移,如果qemu的版本过低不具备BLOCKDEV的能力,则不支持迁移存储 */
        if (flags & VIR_MIGRATE_TUNNELLED) {
            if (virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_BLOCKDEV)) {
                virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
                               _("migration of non-shared storage is not supported with tunnelled migration and this QEMU"));
                return NULL;
            }
			/* 隧道迁移不允许迁移指定个数的虚机磁盘,如果要迁移,必须整个虚机的磁盘都迁移 */
            if (nmigrate_disks) {
                virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
                               _("Selecting disks to migrate is not implemented for tunnelled migration"));
                return NULL;
            }
        } else {
        	/* 如果非隧道迁移,则需要查询源端虚机的磁盘容量信息,将其加到cookie中,这里做一个标记 */
            cookieFlags |= QEMU_MIGRATION_COOKIE_NBD;
            priv->nbdPort = 0;
        }
		/* 对于命令行指定了磁盘个数的迁移,需要确认xml中是否存在相同名字的磁盘,如果不同认为虚机没有这个磁盘 */
        if (nmigrate_disks) {
            size_t i, j;
            /* Check user requested only known disk targets. */
            for (i = 0; i < nmigrate_disks; i++) {
                for (j = 0; j < vm->def->ndisks; j++) {
                    if (STREQ(vm->def->disks[j]->dst, migrate_disks[i]))
                        break;
                }

                if (j == vm->def->ndisks) {
                    virReportError(VIR_ERR_INVALID_ARG,
                                   _("disk target %s not found"),
                                   migrate_disks[i]);
                    return NULL;
                }
            }
        }
    }
    /* 生成连接目的端libvirtd daemon时需要传入的cookie信息 */
    qemuMigrationCookieFormat
    	/* 如果是磁盘迁移,需要生成nbd相关cookie,cookie中包含磁盘的名字和容量 */
        if (flags & QEMU_MIGRATION_COOKIE_NBD)
        	qemuMigrationCookieAddNBD(mig, driver, dom)
/* 进入cookie生成函数 */
qemuMigrationCookieAddNBD
	/* 为nbd cookie分配内存,设置目的端nbd server需要监听的端口,放入cookie信息中 */
    mig->nbd = g_new0(qemuMigrationCookieNBD, 1);
    mig->nbd->port = priv->nbdPort;
    
	/* 通过qmp query-block命令查询虚机的所有块设备,返回一个块设备的数组
	 * 数组的每个entry包含了块设备的相关信息,包括容量和名字,输出放到stats变量中 */
	qemuMonitorBlockStatsUpdateCapacity(priv->mon, stats, false)
		/* 遍历虚机xml的所有磁盘,获取磁盘的别名,通过别名在stats数组中查找对应的entry*/
	    for (i = 0; i < vm->def->ndisks; i++) {
	    	/* 获取磁盘的别名 */
        	virDomainDiskDefPtr disk = vm->def->disks[i];
        	qemuBlockStats *entry;
            if (!disk->info.alias ||
                !(entry = virHashLookup(stats, disk->info.alias)))
                continue;
        	}
        	mig->nbd->disks[mig->nbd->ndisks].target = g_strdup(disk->dst);
        	/* 取出磁盘对应的entry,将其容量信息存放在cookie中 */
       	 	mig->nbd->disks[mig->nbd->ndisks].capacity = entry->capacity;
        	mig->nbd->ndisks++;
    	}

Dst Prepare Phase

  • 目的端的prepare阶段为了实现存储迁移,主要做两件事,一是启动qemu进程等待迁移数据的到来,二是启动nbd server,监听端口,等待源端drive mirror存储。
qemuDomainMigratePrepare3Params
	qemuMigrationDstPrepareDirect
		qemuMigrationDstPrepareAny
			/* 解析源端传入的cookie信息 */
			qemuMigrationCookieParse
			/* 目的端预先创建磁盘,如果目的端是网络设备,比如rbd,这里会跳过 */
			qemuMigrationDstPrecreateStorage
			    switch ((virStorageType)disk->src->type) {
			  	case VIR_STORAGE_TYPE_NETWORK:
        			VIR_DEBUG("Skipping creation of network disk '%s'",
                  				disk->dst);
        			return 0;
        	/* 启动目的端qemu进程 {qemu-kvm} -incoming defer.... */
			qemuProcessLaunch
			/* 通过向目的端qemu进程的monitor server下发qmp命令,启动nbd server监听存储迁移 */
			qemuMigrationDstStartNBDServer
/* 进入nbd server启动函数 */			
qemuMigrationDstStartNBDServer
	bool server_started = false;
	for (i = 0; i < vm->def->ndisks; i++) {
		/* 如果目的端没有启动nbd server,通过nbd-server-start qmp命令启动 */
		if (!server_started) {
            if (qemuMonitorNBDServerStart(priv->mon, &server, tls_alias) < 0)
                goto exit_monitor;
            server_started = true;
        }
        /* 对于要通过drive mirror迁移的所有目的端磁盘
         * 将其通过nbd-server-add qmp命令逐个暴露给nbd server
         * 这样源端就可以通过drive mirror迁移,nbd server中通过export name区分每个磁盘
         * */
      	if (qemuBlockExportAddNBD(vm, diskAlias, disk->src, diskAlias, true, NULL) < 0)
            goto exit_monitor;
  	}
/* 上述所有工作准备好之后,目的端下发migrate-incoming qmp命令开始接收源端的迁移数据 */
qemuMigrationDstRun

Src Perform Phase

  • 源端的perform阶段完成迁移的主要任务,首先在迁移内存之前,就会发起drive mirror命令迁移存储,让任务在qemu后台执行,然后是迁移内存,等待其结束后,检查迁移存储是否完成,完成后显示取消迁移存储的后台任务,最后将源端的磁盘从虚机中detach掉。
qemuDomainMigratePerform3Params
	qemuMigrationSrcPerform
		qemuMigrationSrcPerformPhase
			qemuMigrationSrcPerformNative
				qemuMigrationSrcRun
				/* 如果需要迁移存储,在迁移内存之后首先发起drive mirror,让其在后台异步执行 */
				if (migrate_flags & (QEMU_MONITOR_MIGRATE_NON_SHARED_DISK |
                         QEMU_MONITOR_MIGRATE_NON_SHARED_INC)) {
        			if (mig->nbd) {
        				qemuMigrationSrcNBDStorageCopy
        			}
        		}
        		/* 发起drive mirror后,开始迁移内存 */
        		qemuMonitorMigrateToFd
        		/* 内存迁移结束后,等待drive mirror任务结束
        		 * 然后将drive mirror的block job取消,不然drive mirror会一直进行
        		 * 一旦有新增数据就会mirror到目的端磁盘 */
        	   	if (mig->nbd)
        			qemuMigrationSrcNBDCopyCancel(driver, vm, true,
                                      QEMU_ASYNC_JOB_MIGRATION_OUT,
                                      dconn)                                     
/* 分析drive mirror流程 */    		
qemuMigrationSrcNBDStorageCopy
	/* 遍历每一个虚机的磁盘,对于需要进行拷贝的盘,执行drive mirror */
    for (i = 0; i < vm->def->ndisks; i++) {
        virDomainDiskDefPtr disk = vm->def->disks[i];
        if (qemuMigrationSrcNBDStorageCopyOne(driver, vm, disk, host, port,
                                              socket,
                                              mirror_speed, mirror_shallow,
                                              tlsAlias, flags) < 0)
            return -1;
    	}
    		/* 执行drive-mirror qmp命令,其目的端磁盘通过如下格式执行
    		 * nbd:{HOST}:{PORT}:exportname={NAME} 
    		 * 其中HOST是目的端的主机IP,PORT是目的端nbd server监听的端口
    		 * NAME是在dst prepared阶段通过nbd-server-add命令到处的磁盘的export name
    		 * */
    		qemuMigrationSrcNBDStorageCopyDriveMirror
    		/* 开始drive mirror之后,通过query-block-jobs命令查询job信息,确认任务正在执行 */
    		qemuMigrationSrcFetchMirrorStats
/* 如果drive mirror已经完成,需要通过命令显示取消drive mirror任务
 * 否则它会后台一直进行,分析其流程 */       		
qemuMigrationSrcNBDCopyCancel
	/* 对于虚机的每个需要迁移的磁盘,逐个发送block-job-cancel命令取消drive mirror任务*/
    for (i = 0; i < vm->def->ndisks; i++) {
        virDomainDiskDefPtr disk = vm->def->disks[i];
        rv = qemuMigrationSrcNBDCopyCancelOne(driver, vm, disk, job,
                                              check, asyncJob);
    }
    /* 完成drive mirror之后,将磁盘从源虚机detach掉
     * 为替换目的端磁盘做准备,通过blockdev-del命令实现 */
    for (i = 0; i < vm->def->ndisks; i++) {
        virDomainDiskDefPtr disk = vm->def->disks[i];
        qemuBlockStorageSourceDetachOneBlockdev(driver, vm, asyncJob,
                                                diskPriv->migrSource);
    }

Dst Finish Phase

  • 内存和磁盘迁移完成后,目的端在Finish阶段主要做这几件事,首先停止nbd server,然后保存虚机的xml,持久化到磁盘上,最后启动虚机。
qemuDomainMigrateFinish3Params
	qemuMigrationDstFinish
		/* 调用 nbd-server-stop qmp命令停止nbd server */
		qemuMigrationDstStopNBDServer
		/* 持久化虚机配置到磁盘 */
		qemuMigrationDstPersist
		/* 调用cont qmp命令启动vcpu */
		qemuProcessStartCPUs

Src Confirm Phase

  • 目的端启动虚机成功,源端将虚机暂停
qemuDomainMigrateConfirm3Params
	qemuMigrationSrcConfirm
		qemuMigrationSrcConfirmPhase
			qemuProcessStop

你可能感兴趣的:(虚拟化,存储虚拟化)