Skip to the content.

block

https://wiki.qemu.org/Features/LiveBlockMigration

忽然意识到,overlayfs 可以实现 snapshot

block/dirty-bitmap.c 和 migration/block-dirty-bitmap.c

我现在的感觉是,dirty bitmap 和 block 并不是互相替代的技术,就是存在 dirty bitmap 的信息需要被发送出去的。 如果去进一步的看 block/dirty-bitmap.c ,应该是可以验证这个想法的。

关键结构体:

接受端的:

那么这个 migration/block-dirty-bitmap.c 功能都是注入到哪些 level 的

本来以为是 qcow2 中做这个事情,实际上

非共享存储的热迁移 : codex

支持,但不再通过 migrate 命令内置的 block capability 迁移。

现在 QEMU 把两件事拆开了:

典型做法是 blockdev-mirror + NBD:

源端磁盘 | | blockdev-mirror v 目的端 NBD export | v 目的端磁盘

大致流程是:

  1. 在目的端创建目标磁盘。
  2. 目的端通过 NBD export 暴露目标 block node。
  3. 源端添加对应的 NBD block node。
  4. 源端运行 blockdev-mirror,一边运行 VM,一边复制磁盘。
  5. mirror 进入 READY 后,新产生的 guest 写入会继续同步到目的端。
  6. 启动普通 VM migration,迁移 RAM 和设备状态。
  7. switchover 时停止源 VM并完成最后写入同步。
  8. 目的端使用已经同步好的本地磁盘启动 VM。

这种设计比旧 block capability 更灵活:

几个容易混淆的地方:

所以更准确的说法是: QEMU 仍然支持在线复制和迁移存储, 但存储复制已从 VM migration stream 中拆出,改由 blockdev-mirror、NBD。

仓库中有完整的 QMP 示例:docs/interop/live-block-operations.rst。

nbd + blockdev-mirror 的关键 QMP 操作

目标端:

{ “execute”: “nbd-server-start”, “arguments”: { “addr”: { “type”: “unix”, “data”: { “path”: “/path/to/migration.nbd” } } } }

用新的接口导出目标 block node:

{ “execute”: “block-export-add”, “arguments”: { “type”: “nbd”, “id”: “export-boot1”, “node-name”: “boot1-format”, “name”: “boot1”, “writable”: true, “allow-inactive”: true } }

allow-inactive=true 对当前 QEMU 很重要:incoming QEMU 的磁盘尚未激活时,NBD mirror 仍需向其写入;

源端添加 NBD node:

{ “execute”: “blockdev-add”, “arguments”: { “driver”: “nbd”, “node-name”: “migration-target-boot1”, “server”: { “type”: “unix”, “path”: “/path/to/migration.nbd” }, “export”: “boot1” } }

开始完整同步:

{ “execute”: “blockdev-mirror”, “arguments”: { “job-id”: “mirror-boot1”, “device”: “boot1-format”, “target”: “migration-target-boot1”, “sync”: “full”, “on-source-error”: “report”, “on-target-error”: “report” } }

源码分析 : codex

block/dirty-bitmap.c 和 migration/block-dirty-bitmap.c 的关系是什么?

两者的关系可以概括为:

guest write/discard │ ▼ block/io.c: bdrv_set_dirty() │ ▼ block/dirty-bitmap.c BdrvDirtyBitmap / HBitmap │ ├── backup/mirror/block-copy 使用 │ └── migration/block-dirty-bitmap.c │ serialize ▼ migration stream │ deserialize ▼ 目标端 BdrvDirtyBitmap

block/dirty-bitmap.c

这是 block layer 的通用脏块位图实现。

核心对象是 block/dirty-bitmap.c:28:

struct BdrvDirtyBitmap {
    BlockDriverState *bs;
    HBitmap *bitmap;
    char *name;
    int64_t size;
    bool disabled;
    bool busy;
    bool persistent;
    bool inconsistent;
    BdrvDirtyBitmap *successor;
    ...
};

它挂在某个 BlockDriverState 上,用一个 HBitmap 表示哪些磁盘区间发生过变化。

假设:

那么 bitmap 不保存这 4 KiB 数据,只把覆盖该范围的 64 KiB granule 对应 bit 置为 1。

写请求怎样进入 bitmap

block I/O 完成时,write 和 discard 会调用:

bdrv_set_dirty(bs, offset, bytes);

调用位置在 block/io.c:2028,实现位于 block/dirty-bitmap.c:656。

bdrv_set_dirty() 遍历这个 block node 上所有启用的 bitmap:

QLIST_FOREACH(bitmap, &bs->dirty_bitmaps, list) { if (!bdrv_dirty_bitmap_enabled(bitmap)) { continue; } hbitmap_set(bitmap->bitmap, offset, bytes); }

这个文件提供的主要能力

相关序列化基础接口在 block/dirty-bitmap.c:599。

谁会使用它

它是一个通用基础设施:

其中匿名 bitmap:

bdrv_create_dirty_bitmap(bs, granularity, NULL, errp);

没有名字,只供 QEMU 内部使用,不会通过 QMP 暴露,也不会被 migration 迁移。

migration/block-dirty-bitmap.c

这个文件实现一种 migration section,名称为 dirty-bitmap:

register_savevm_live(“dirty-bitmap”, 0, 1, &savevm_dirty_bitmap_handlers, &dbm_state);

注册位置见 migration/block-dirty-bitmap.c:1253。

只有启用了 migration capability:

dirty-bitmaps=on

并且源端确实有具名 bitmap,它才会激活:

return migrate_dirty_bitmaps() && !s->no_bitmaps;

它迁移的内容

每个 bitmap 迁移这些信息:

协议格式写在文件开头,见 migration/block-dirty-bitmap.c:21。

它不发送:

因此目标端必须已经有对应的 block node,而且它代表的磁盘内容必须和源端处在一致的迁移时间点。否则 bitmap 即使成功迁过去,也没有实际意义。

发送端流程

1. 找到要迁移的 bitmap

init_dirty_bitmap_migration() 遍历:

实现见 migration/block-dirty-bitmap.c:601。

匿名 bitmap 会被跳过:

bitmap_name = bdrv_dirty_bitmap_name(bitmap); if (!bitmap_name) { continue; }

被选择的 bitmap 会:

2. 发送 START

dirty_bitmap_save_setup() 对每个 bitmap 发送:

node alias bitmap alias granularity enabled persistent

见 migration/block-dirty-bitmap.c:1220。

3. 发送 bitmap bits

send_bitmap_bits() 调用底层:

bdrv_dirty_bitmap_serialize_part(…)

把一段 HBitmap 转成 byte buffer,见 migration/block-dirty-bitmap.c:423。

这里的 CHUNK_SIZE = 1024 是“每个 bitmap 数据块最多约 1 KiB”,不是复制 1 KiB 磁盘内容。

如果整段 bitmap 都是零,会只发送 ZEROES 标志,不发送 buffer。

4. 发送 COMPLETE

所有 bits 发送完成后,对每个 bitmap 发送 COMPLETE,最后发送 EOS。

为什么叫 postcopy bitmap migration

它支持目标 VM 已经运行、bitmap bits 仍未完全传完的情况。

发送端的 save_live_iterate 只有在 VM 不再运行时才参与:

return dirty_bitmap_is_active(opaque) && !runstate_is_running();

见 migration/block-dirty-bitmap.c:1243。

因此:

难点在于:目标 VM 启动后又会产生新的磁盘写入,但旧 bitmap 还没接收完。这里就需要 successor。

目标端和 successor

目标端收到 START 后:

  1. 创建一个 disabled bitmap,接收源端传来的旧 bits。
  2. 如果源 bitmap 原本是 enabled,再为它创建一个 enabled successor。
  3. 目标 VM 启动后,新写入进入 successor。
  4. 源 bitmap bits 接收完成后,将 successor 合并进主 bitmap。

代码见 migration/block-dirty-bitmap.c:798。

逻辑上相当于:

源端迁来的历史 bitmap: 00100100 目标启动后的新写入: 00010001 ——– 最终目标 bitmap: 00110101

如果 bitmap 在目标 VM 启动前已经接收完,直接启用主 bitmap;如果还没接收完,则先启用 successor。处理位置是 migration/block-dirty-bitmap.c:891。

收到 COMPLETE 后,dirty_bitmap_load_complete() 调用 reclaim,将 parent 和 successor 合并,见 migration/block-dirty-bitmap.c:952。

AliasMapInnerNode 是什么

它只是为了实现两层名称映射:

source node name └── source bitmap name ↓ node alias └── bitmap alias ↓ target node name └── target bitmap name

结构:

typedef struct AliasMapInnerNode { char *string; GHashTable *subtree; } AliasMapInnerNode;

外层 hash table:

node name/alias -> AliasMapInnerNode

其中:

发送端构建 name -> alias,接收端构建 alias -> name,见 migration/block-dirty-bitmap.c:192。

这允许源、目标两端的 block node 名字不同:

source: node=drive0, bitmap=backup0 alias: node=disk-a, bitmap=incremental target: node=target-disk, bitmap=backup-chain

最关键的区别

文件 管什么 保存磁盘数据吗 bitmap 范围 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━ block/dirty-bitmap.c bitmap 数据结构、状态和操作 否 具名和匿名 ──────────────────────────────── ────────────────────────────────────────────── ──────────────── ───────────────────────── migration/block-dirty-bitmap.c 把 bitmap 本身通过 migration stream 搬到目标 否 只迁移具名 bitmap ──────────────────────────────── ────────────────────────────────────────────── ──────────────── ───────────────────────── block/mirror.c 根据内部 bitmap 实际复制磁盘数据 是 通常使用匿名内部 bitmap

所以,在 storage 热迁移中:

blockdev-mirror 迁移实际磁盘内容 dirty-bitmaps capability 迁移增量备份所依赖的 bitmap 状态 migrate 迁移 RAM/CPU/device state

三者互补,不是互相替代。

先看看核心文档

这份文档介绍了 QEMU 在虚拟机运行期间对磁盘镜像链执行的四类主要操作:stream、commit、mirror 和 backup。核心内容可以概括如下。

镜像链模型

文档使用下面的 QCOW2 backing chain 作为示例:

[A] <– [B] <– [C] <– [D] ↑ 运行中的 QEMU

因此,快照时间点和文件名并不直接对应:

四类操作对比

操作 数据移动方向 主要目的 是否切换运行磁盘 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ block-stream backing → overlay 拉平或缩短 backing chain 否 ──────────────────────────────── ───────────────────────── ────────────────────────────── ──────────────────────────── block-commit overlay → backing 将增量提交回底层镜像 active commit 完成时会切换 ──────────────────────────────── ───────────────────────── ────────────────────────────── ──────────────────────────── drive-mirror / blockdev-mirror 当前磁盘链 → 新目标 磁盘复制、存储迁移、持续同步 可选 ──────────────────────────────── ───────────────────────── ────────────────────────────── ──────────────────────────── blockdev-backup 当前磁盘状态 → 备份目标 创建时间点备份 否

1. block-stream

将 backing file 中当前镜像需要的数据复制到上层 overlay,是一种“向右合并”。

例如:

[A] <– [B] <– [C] <– [D]

可以变成:

[D] # 全部拉平 [A] <– [D] # 把 B、C 拉入 D [A] <– [C] <– [D] # 把 B 拉入 C

典型命令:

block-stream device=node-D job-id=job0 block-stream device=node-D base-node=node-A job-id=job0

特点:

2. block-commit

把 overlay 的数据写回 backing file,是 block-stream 的反方向。

例如把 [B] 提交到 [A]:

[A] <– [B] <– [C] <– [D] ↓ [A] <– [C] <– [D]

也可以把整个链提交回 [A]:

[A] <– [B] <– [C] <– [D] ↓ [A]

普通 commit 完成后直接发出 BLOCK_JOB_COMPLETED。

active commit(包括当前 [D])是两阶段操作:

  1. 先把 [B]、[C]、[D] 同步到 [A]。
  2. 收到 BLOCK_JOB_READY 后执行:

block-job-complete device=job0

然后让运行中的 QEMU 切换到 [A],最后收到 BLOCK_JOB_COMPLETED。

重要区别:

3. drive-mirror / blockdev-mirror

把运行中的磁盘链持续同步到另一个镜像:

[A] <– [B] <– [C] <– [D] ──mirror──> [E]

同步到最新状态后,任务进入 ready 状态并发出 BLOCK_JOB_READY。此时有两种收尾方式:

因此 mirror 常用于:

mirror 支持四种同步模式:

drive-mirror 与 blockdev-mirror 的主要差异是:

文档还演示了 drive-mirror + NBD 的无共享存储迁移流程:

  1. 目标端准备 backing chain 和目标 overlay。
  2. 目标 QEMU 启动 NBD server。
  3. 通过 NBD 导出目标 block node。
  4. 源 QEMU 使用 drive-mirror sync=top mode=existing 同步 active layer。
  5. 收到 BLOCK_JOB_READY 后停止 mirror。
  6. 停止 NBD server,并在目标端恢复虚拟机。

4. blockdev-backup

blockdev-backup 创建的是命令启动时刻的时间点备份:

[A] <– [B] ──backup──> [E]

即使备份过程中 guest 继续写磁盘,QEMU 也会通过 copy-before-write 等机制保证目标表示备份开始时的状态。

基本流程:

  1. 使用 qemu-img create 创建目标镜像。
  2. 使用 blockdev-add 将其加入 QEMU。
  3. 执行:

blockdev-backup device=node-B target=node-E sync=full job-id=job0

  1. 完成时收到 BLOCK_JOB_COMPLETED,不需要额外的 block-job-complete。

drive-backup 是旧接口:

mirror 和 backup 的时间点语义不同:

操作时需要特别注意

原文位置:docs/interop/live-block-operations.rst:9。

那么,这些功能就是这个文档中记录实现的吗?

snapshot_blkdev snapshot_blkdev_internal snapshot_delete_blkdev_internal

https://wiki.qemu.org/Features/SnapshottingImprovements#snapshot-create

codex 给出的回答:

不完全是。更准确地说:这三个是 HMP 命令的实现,底层调用 QMP/Block Layer API;live-block-operations.rst 只是介绍这些接口的使用方式,并不是它们的 实现依据或规范。

HMP 命令 底层 QMP 接口 文档是否涉及 类型 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━ snapshot_blkdev blockdev-snapshot-sync 是,作为构造 backing chain 的辅助操作 外部快照 ───────────────────────────────── ──────────────────────────────────────── ─────────────────────────────────────── ────────────── snapshot_blkdev_internal blockdev-snapshot-internal-sync 否 内部快照 ───────────────────────────────── ──────────────────────────────────────── ─────────────────────────────────────── ────────────── snapshot_delete_blkdev_internal blockdev-snapshot-delete-internal-sync 否 删除内部快照

snapshot_blkdev

它与文档里的 blockdev-snapshot-sync 是直接对应的。

调用链:

HMP snapshot_blkdev └─ hmp_snapshot_blkdev() └─ qmp_blockdev_snapshot_sync() └─ QMP transaction └─ external_snapshot_action()

HMP 包装代码位于 block/monitor/block-hmp-cmds.c:344,直接调用:

qmp_blockdev_snapshot_sync(device, NULL, filename, NULL, format, true, mode, &err);

底层 QMP 实现在 blockdev.c:1074。

其效果是创建外部 overlay:

操作前:

QEMU | [A]

操作后:

[A] <– [B] | QEMU

底层 external_snapshot_action() 会:

  1. 找到原 block node。
  2. drain I/O 并 flush 原镜像。
  3. 创建或打开新 overlay。
  4. 将原镜像设置成新 overlay 的 backing node。
  5. 让设备改用新 overlay。

具体处理见 blockdev.c:1367。

文档正是用 blockdev-snapshot-sync 创建 [A] <– [B] <– [C] <– [D] 示例链,见 docs/interop/live-block-operations.rst:171。

但它不是文档重点介绍的四类 block job:

block-stream block-commit drive/blockdev-mirror blockdev-backup

snapshot_blkdev 是同步修改 block graph 的快照操作,不会启动一个可由 query-block-jobs 观察的后台 BlockJob。

两个 internal 命令

这两个和文档中的 backing chain 操作不是同一种机制。

snapshot_blkdev_internal 的调用链是:

HMP snapshot_blkdev_internal └─ qmp_blockdev_snapshot_internal_sync() └─ internal_snapshot_action() └─ bdrv_snapshot_create() └─ 镜像格式驱动的 snapshot_create 回调

相关代码:

它不会创建新的 overlay 文件,而是在同一个镜像或存储对象内部记录快照:

外部快照:

[A] <– [B] 两个 block node/镜像

内部快照:

[A] ├─ internal snapshot 1 └─ 当前状态 同一个镜像/存储对象

只有实现了内部快照回调的格式才能使用,例如:

删除命令则直接:

snapshot_delete_blkdev_internal └─ qmp_blockdev_snapshot_delete_internal_sync() └─ 查找快照 └─ bdrv_snapshot_delete()

见 blockdev.c:1125。

所以最终结论是:

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。