基本工具
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设备关联,这样可以将镜像文件作为本地块设备进行读写。下面分别介绍这两种用法:
通过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
将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接口,实现磁盘的迁移,包括源端和目的端的所有动作。
流程图
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