war3 replay overlay 这词有点意思。如果你玩过魔兽争霸3的录像,应该记得 replay overlay 是在回放画面上叠的那层操作面板、资源和建筑信息,纯渲染层的东西。但做虚拟化和容器的人一听 overlay,脑子里蹦出来的是另一套东西:qcow2 外部快照生成的那个快照文件,以及它背后那个被当作底座的 backing file。标题里那句“存往后的数据”和“对基础镜像做外部快照,生成的快照文件被称为 overlay,基础镜像成为 backing file”,说的正是后者。这篇文章把 backing file 和 overlay 这套机制拆开讲清楚,再给一套可以直接上手的操作流程,适合刚接触 QEMU/KVM 镜像管理、被快照链绕晕的运维和虚拟化初学者。
1. war3 replay overlay 是另一码事:先把概念边界划清楚
1.1 游戏回放里的“叠加层”和存储里的“叠加层”
war3 replay overlay 里的 overlay,指的是录像播放时叠加在 3D 画面上的 2D 面板层。它不改变游戏回放的数据,只是在显示层面盖上去一层信息。老玩家应该记得当年装各种 replay 插件,就是为了让录像界面多显示 APM、出兵时间、经济曲线这些“叠”在画面上的东西。
存储领域的 overlay 完全不同。一个 qcow2 格式的 overlay 文件,是实际承载磁盘写入数据的实体文件,不是显示层。它是通过 backing file 机制跟基础镜像建立关联的:基础镜像作为只读底座,overlay 负责把后续写入的数据全部接住。很多人第一次看到这两个词时产生混淆,就是因为它们都叫 overlay,但一个活在渲染管线里,一个活在存储栈里。
这也是我写这篇的原因之一。搜索引擎把 war3 replay overlay 和麒麟操作系统基础镜像下载这类词关联在一起,根源就在这儿:两边都用了 overlay 这个词,但语境天差地别。先把概念边界划清楚,后面所有操作才不会跑偏。
1.2 backing file 和 overlay 的本质关系:底账与流水账
用个生活化类比。基础镜像是一本已经写满内容的底账,记录的是系统初始状态。backing file 就是这本底账。overlay 则是一本全新的空白流水账。你开一台新虚拟机,所有新增数据不往底账上写,全记在流水账上。
查询账目时先翻流水账,流水账没有的记录再去翻底账。底账始终保持原样,所以你可以随时丢弃这本流水账,重新换一本新的,账目就回到了底账的初始状态——这就是系统回滚的本质。
标题里那句“存往后的数据”,我理解的就是这个意思:把后续产生的增量数据写到后面的 overlay 层里去,而不是改动作为基础镜像的 backing file。基础镜像就在那躺着,一动不动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. “存往后的数据”是怎么实现的:qcow2 写时重定向机制拆解
2.1 qcow2 文件不是简单的一块磁盘镜像
要理解 overlay 为什么能“存往后”,得先知道 qcow2 文件内部长什么样。qcow2 不是 raw 那种线性的“从头到尾一个字节一个字节映射”的格式,它有一套自己的元数据体系。
文件开头是固定的 header,里面放着 magic 号(0x514649FB,也就是 “QFI\xFB”)、版本号、虚拟磁盘大小、cluster_bits 这些关键信息。header 之后跟着 L1 表、L2 表、refcount 表和一堆 data cluster。虚拟磁盘被划分成固定大小的 cluster,一般默认 64KB。guest 看到的每一个虚拟地址,都会先映射到 L1 表,再由 L1 表指向某个 L2 表,L2 表里的 entry 记录这个虚拟块实际落在文件的哪个 data cluster 偏移量上。
这套两级映射就是 qcow2 的“目录系统”。没有数据分配的块,L2 entry 是空的,读出来就是零。
2.2 写路径:guest 一写入,数据就落到 overlay 的新簇里
新建一个 overlay 时,它只是个几 KB 的小文件,虚拟大小却跟基础镜像一样大。那个“存往后”的魔力就发生在写入那一瞬间。
当 guest 向虚拟磁盘某个地址写入数据时,QEMU 先根据地址算出对应的 cluster 位置,去查 overlay 的 L2 表。如果这个 cluster 在 overlay 里已经有分配记录,那就直接覆盖现有 cluster。如果没有分配记录——也就是这个块自 overlay 创建以来从没被写过——QEMU 会做两件事:在 overlay 文件里新分配一个 data cluster,把 guest 传来的数据写进去,然后在 L2 表里把虚拟地址指向这个新 cluster。
注意,这里不会先去基础镜像里把旧数据拷贝过来。这就是所谓的“写时分配 + 重定向”:数据只写到 overlay,基础镜像对应位置的旧数据对 guest 不再可见,因为后续读取会优先命中 overlay 的新映射。
2.3 读路径:overlay 优先,backing file 兜底
读路径简单但关键。guest 读一个虚拟地址,QEMU 先查 overlay 的 L2 映射。
如果映射存在,直接读 overlay 的 cluster,这条路径对基础的旧数据完全无感。如果映射不存在,说明这个块在 overlay 里从未被写过,QEMU 就沿着 backing chain 往下一层找,也就是去 backing file 里读对应位置。如果 backing file 也没有,再继续往下,直到基础镜像都没有,那就返回全零。
这就是为什么 overlay 刚建立时,你往里面读数据,看到的能跟基础镜像一模一样,但 overlay 文件本身几乎没有占用空间。所有“读老数据”的请求都被下行到 backing file 处理了,“写新数据”的请求则永远留在 overlay 这一层。一收一支,正好把增量数据隔离开来。
2.4 用 qemu-img info 观察层与层的关系
实操中最常用来观察这套机制的命令是 qemu-img info。对 overlay 执行之后,输出会多出两行:
code复制$ qemu-img info overlay1.qcow2
image: overlay1.qcow2
file format: qcow2
virtual size: 20 GiB (21474836480 bytes)
disk size: 2.1 MiB
backing file: base.qcow2
backing file format: qcow2
virtual size 显示的是完整虚拟磁盘大小,disk size 才是 overlay 实际占用的磁盘空间。backing file 这行,就是标题里那个“基础镜像成为 b...”的完整版——基础镜像成为了 backing file。
backing file 的路径是固化在 overlay 头部字段里的。这个细节很要命:你把基础镜像和 overlay 放到不同目录,或者改了基础镜像的文件名,再启动虚拟机就会报 “Could not open backing file”。后面会专门讲这个坑。
3. 外部快照实操:把基础镜像变成 backing file,生成 overlay 快照文件
3.1 内部快照与外部快照:一张表看清差异
标题里明确提到“外部快照”,那就要先搞清楚它和内部快照的区别。内部快照是把快照数据存在同一个 qcow2 文件内部。外部快照则是新建一个独立文件作为 overlay,让原镜像变成 backing file。
我做了个对照表,两者差异一目了然:
| 对比维度 | 内部快照 | 外部快照 |
|---|---|---|
| 快照存放位置 | 单个 qcow2 文件内部 | 独立的新 qcow2 文件(overlay) |
| 原镜像角色 | 仍是主镜像 | 变成 backing file |
| 创建命令 | qemu-img snapshot -c | qemu-img create -b |
| 回滚方式 | qemu-img snapshot -a | 删除 overlay / 重建新 overlay |
| 是否支持在线快照 | libvirt 支持,但元数据膨胀明显 | 支持,配合 qemu monitor / libvirt |
| 多次快照 | 单文件内多个快照点 | 形成快照链,逐层叠加 |
| 主要风险 | 元数据膨胀、文件损坏面大 | 文件依赖关系复杂,路径要管好 |
内部快照看起来省事,所有东西塞一个文件里,但它有个致命问题:改一个文件等于改所有快照。而且反复创建删除内部快照,qcow2 文件里的 refcount 和 L2 表会不断重组,文件越来越大,性能越来越差。
外部快照则把每次快照都切成独立文件,逻辑更清晰,删除也安全。你想恢复到哪个时间点,只要把对应的 overlay 当作启动层就好。
3.2 一条命令生成 overlay:完整实操流程
下面这套流程我在测试环境跑过很多遍,可以直接抄。
第一步:准备一个基础镜像。假设你已经有一个 base.qcow2,比如某个 Linux 发行版的云镜像。先看一下它的格式和大小,确认无误再做下一步。
code复制$ qemu-img info base.qcow2
image: base.qcow2
file format: qcow2
virtual size: 20 GiB
disk size: 3.2 GiB
第二步:创建 overlay 快照文件。命令就是外部快照的核心:
code复制$ qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay1.qcow2
这里 -b 指定 backing file,-F 指定 backing file 的格式。执行完你会得到一个只有几百 KB 的 qcow2 文件,但它的虚拟大小跟 base 一样。
第三步:验证 overlay 和 base 的关联关系。
code复制$ qemu-img info overlay1.qcow2 --backing-chain
--backing-chain 会把整条链都列出来,从 overlay 一路列到最底层的 base。这一步确认无误后,overlay 就能直接拿去启动虚拟机了。
第四步:启动虚拟机,开始往 overlay 里写数据。
code复制$ qemu-system-x86_64 -drive file=overlay1.qcow2,if=virtio -m 2048 -boot c
装软件、写文件、跑测试,随便折腾。然后你再执行一次 qemu-img info,会发现 overlay 的 disk size 涨上去了,而 base 的 disk size 纹丝不动。这就是“存往后”的直观证据。
3.3 回滚到“昨天晚上”的操作
外部快照最爽的应用场景是回滚。假设你在 overlay1 上装了一堆软件,把系统搞坏了,想回到刚创建 overlay1 时的干净状态,其实不需要做任何恢复操作。
只要关掉虚拟机,删掉 overlay1.qcow2,重新执行一次 qemu-img create -b base.qcow2 生成一个新的 overlay2.qcow2,再用 overlay2 启动,系统就回到 base 的初始状态了。这条链路就是这么干净。
生产环境里我一般会保留多个 overlay 做“时间点”:overlay1 是周一的状态,overlay2 是周三,overlay3 是周末。每个都是基于同一个 base 派生出来的独立分支,互不干扰。想回哪个时间点,就启动对应的 overlay。
3.4 -F 参数为什么不能省
很多 qcow2 外部快照报错,都是栽在 -F 参数上。如果省略 -F,QEMU 会尝试自动探测 backing file 的格式。自动探测大多数时候没问题,但遇到 backing file 是 raw 格式、或者文件头恰好有类似 qcow2 魔数的情况,就会探测错误。
一旦探测错了,overlay 以为自己继承的是一个 qcow2 的 base,实际 base 却是 raw,读写时解析 L1/L2 表就会错乱,轻则数据读不出来,重则写坏虚拟磁盘。所以我的习惯是 -F 永远显式写清楚,不要偷懒。
4. 快照链越滚越长:commit、rebase 和回滚决策的取舍
4.1 线性链和快照树
外部快照用得多了,就会出现链式结构:base <- overlay1 <- overlay2 <- overlay3。每个 overlay 的 backing file 是上一个 overlay,最底下的才是原始基础镜像。
这条链越长,读性能越差。因为读一个冷数据块要沿着链逐层往下找,每层都要查一次 L2 表。写性能的影响相对小,数据总是往最上层写,但链太长后,某些场景下的 I/O 延迟会明显增加。
还有个容易忽略的点是“共享分支”。多个 overlay 可以同时指向同一个 backing file,这就形成了树形结构。比如 base 作为模板,同时派生了 work1、work2、work3,三个测试环境各自独立。这个结构没问题,但一旦你想往 base 里合并数据,三个分支都会受影响,这点在 4.2 里会细说。
4.2 commit:向下合并的代价
qemu-img commit 的作用是把 overlay 里记录的数据写回它的 backing file。我见过不少人以为 commit 是“把 base 合并进 overlay”,方向正好搞反。commit 的方向永远是向下的:overlay 的数据融入 backing file。
code复制$ qemu-img commit overlay1.qcow2
执行完,overlay1 里的数据和 base 合并,base 变大,overlay1 可以删除。但这里有个严重的前提问题:如果你把 commit 用在了共享 base 上,比如 work1 分支 commit 进了公共 base,work2 和 work3 的数据参照就乱了。
为什么?因为 work1 的 overlay 里记录的是“base 原始状态之上我只改了这些”,如果你把 work1 的改动写回 base,base 就变了。work2 读工作时,它的 overlay 里没改过的块会 fall through 到新的 base 上,读到的却是 work1 改动后的数据。这就是共享污染。
所以我的建议是:不要往公共 base 上 commit。非要 commit,也是把某条测试分支单独抽出来,commit 到这条分支自己的“私有 base”上。
4.3 rebase:频繁更换底座的操作
rebase 是外部快照链管理里另一个高频命令。它的作用是把 overlay 的 backing file 换掉。
code复制$ qemu-img rebase -b newbase.qcow2 -F qcow2 overlay1.qcow2
不带 -u 参数执行 rebase 时,QEMU 会比较 overlay 所有“未修改块”在旧 base 和新 base 之间的差异,并同步到 overlay 里。带 -u 参数表示 unsafe 模式,只修改 backing file 路径,不搬运数据。unsafe 模式很危险,如果新旧 base 的对应位置内容不一致,读出来的数据就是错的,只适合你确定新旧 base 内容完全一致时使用。
rebase 最典型的场景是升级。比如你想把整条快照链的底座从旧系统镜像换成打了安全补丁的新系统镜像,直接 rebase 整条链,就能让所有 overlay 都基于新底座工作。
4.4 什么时候 commit,什么时候直接重建 overlay
处理快照链之前,先想清楚你的诉求:
| 场景 | 推荐操作 | 理由 |
|---|---|---|
| 测试环境需要反复回滚 | 保留 base,不断新建 overlay | 成本最低,回滚就是重建文件 |
| overlay 已累积大量数据,想固化 | commit 到私有 base | 减少链深,提升读性能 |
| 想换底座但仍保留 overlay 增量 | rebase | 避免重新安装配置 |
| 上层数据已无保留价值 | 删掉对应 overlay 文件 | 操作最直接 |
| 需要迁移镜像到新环境 | qemu-img convert | 输出一个无依赖的单文件 |
这里有个实践经验:commit 时要预留足够的临时磁盘空间。commit 不是原地修改,它是把 overlay 数据读到临时区域再写回 base,过程中 base 和临时文件都会增大。之前我因为没盯 df -h,commit 到一半磁盘满了,结果 base 处于半更新状态,整个分支都废了,只能回滚重建,教训很深刻。
5. 在国产化环境里跑这套方案:麒麟下的实测与注意点
5.1 麒麟环境里 qemu-img 的版本差异
我在麒麟系的 Linux 环境里跑过这套外部快照流程,整体畅通,但有几个细节需要说明。麒麟系统默认仓库里的 QEMU 相关工具版本不一定是最新的,qemu-img info 的输出字段顺序、--backing-chain 的支持程度,跟上游版本会有细微差异。
如果你的系统里 qemu-img 版本偏老,遇到信息输出不完整或者参数不识别,优先检查版本:
code复制$ qemu-img --version
版本太旧的话,我建议从发行版的新版本源里安装更新的 QEMU 包,或者编译安装较新版本。外部快照本身是 QEMU 很成熟的功能,新老版本在底层逻辑上没有太大差异,但工具版本影响脚本兼容性,值得提前确认。
5.2 下载“基础镜像”时怎么确认它真的是 qcow2
麒麟操作系统基础镜像下载这词最近搜得人挺多,我顺手提醒一句:下载回来的镜像文件,名字可能叫 .qcow2,也可能叫 .raw,还可能是个压缩包里面套着镜像。建议先解压(如果是压缩包),再把解压后的文件丢给 qemu-img info 看一眼,确认 format 一行写的是 qcow2 还是 raw。
这一步必须做。有人直接用 raw 格式的文件当作 qcow2 base 创建 overlay,结果虚拟机能正常启动,但写入一段时间后 I/O 报错。qemu-img info 几十毫秒的确认动作,能省一晚上的排查时间。
5.3 文件权限、SELinux 和 sparse 文件的隐形坑
麒麟环境默认可能启用 SELinux 或 AppArmor。如果 overlay 和 base 放在 /var/lib/libvirt/images 这类受管目录下,要注意 qemu 进程的 SELinux context 是否正确。常见的报错是启动虚拟机时提示 “Permission denied” 但文件权限明明没问题——这种基本都是 SELinux 的 type 不匹配。
镜像复制和备份也有讲究。qcow2 是 sparse 文件,文件实际占用远小于 apparent size。你用 cp 复制时最好加 --sparse=always 参数,用 tar 打包时要加 -S 参数,否则复制出来的文件可能被“撑实”,把几 GB 的虚拟磁盘变成几十 GB 的实际占用,白白撑爆磁盘。我写过一个小函数,现在创建快照都用它:
bash复制snapshot_new() {
if [ -z "$1" ]; then
echo "用法: snapshot_new 基础镜像文件"
return 1
fi
BASE="$1"
TS=$(date +%Y%m%d_%H%M%S)
OVERLAY="${BASE%.qcow2}_${TS}.qcow2"
qemu-img create -f qcow2 -b "$BASE" -F qcow2 "$OVERLAY"
echo "已生成快照: $OVERLAY"
}
这个函数生成带时间戳的 overlay 文件名,避免覆盖,也方便一眼看出快照创建时间。
5.4 路径依赖:移动镜像文件前必须检查 backing file 指向
最后再强调一个最隐蔽的坑。qcow2 overlay 头部里存的 backing file 路径,既有可能是绝对路径,也有可能是相对路径。你把整个目录从一台机器拷贝到另一台机器,如果 base 的路径变了,overlay 打不开。
解决办法是移动前先看信息:
code复制$ qemu-img info overlay1.qcow2 | grep backing
backing file: /old/path/base.qcow2
如果显示的是绝对路径且已经失效,用 rebase 重新指定新路径即可:
code复制$ qemu-img rebase -b /new/path/base.qcow2 -F qcow2 -u overlay1.qcow2
这里用 -u 是因为镜像文件没做数据搬运,只是修正路径引用。前提是新 base 和旧 base 内容完全一致,否则还是老老实实去掉 -u 做完整 rebase。
我在实际项目里的体会是,外部快照这套机制,最有价值的地方不是“备份”,而是让你对数据和镜像的关系有了新的掌控感。基础镜像可以一直保持只读,所有可变状态都收在薄薄一层 overlay 里,出问题就删掉重建,成本极低。war3 的 replay overlay 当年挡我视线,存储层的 overlay 倒是越用越顺手。
