Linux dm-vdo(Virtual Data Optimizer)深入指南:块级去重、压缩与精简配置的实现、参数与运维全解
本文以 Linux 内核仓库中的 Documentation/admin-guide/device-mapper/vdo.rst 为主体,完整讲解 dm-vdo 设备映射目标(device mapper target)的定位、格式化与只读恢复机制、table line 全参数说明、热修改规则、消息接口、状态输出、内存占用模型与调优方法,并结合 drivers/md/dm-vdo/ 内核驱动源码印证其实现结构。读完本文,你应能独立完成一个 vdo 卷的创建、扩容、参数调整与故障恢复,并理解各配置项在内核中的实际作用。
dm-vdo 是什么
dm-vdo(virtual data optimizer,虚拟数据优化器)是一个设备映射目标,为存储栈提供**块级去重(block-level deduplication)、压缩(compression)与精简配置(thin provisioning)**三项能力。作为 dm 目标,它可以在任意文件系统之下透明地叠加这些特性,与具体文件系统解耦。文档明确给出两点边界:
- vdo 目标不提供数据损坏防护(no protection against data corruption),它依赖其下层存储的完整性(integrity)保护来保证数据可信;
- 官方强烈建议使用 LVM 来管理 vdo 卷(参见
lvmvdo(7)手册页)。
vdo 的完整设计(去重索引 UDS、slab 数据池、块映射、恢复日志、写路径 13 步流程、崩溃恢复与只读重建)在同目录的 vdo 设计文档中有专门阐述,本文的源码佐证部分会以该文档为背景。
用户态工具链:格式化与只读恢复
vdo 卷的格式化必须使用用户态工具 vdoformat(位于 dm-vdo 官方用户态代码仓库)。此外还有两类关键工具:
vdoforcerebuild:用于让处于只读模式的 vdo 卷退出只读状态;- 元数据检查工具:用于检查 vdo 目标的磁盘元数据,除 dm-vdo 开发者外很少需要。
崩溃恢复语义(这是运维 vdo 必须理解的机制):
- 多数情况下,vdo 目标在下一次启动时会自动从崩溃中恢复;
- 若遇到不可恢复错误(正常运行中或崩溃恢复过程中),目标会进入只读模式(read-only mode),或以只读模式启动;
- 由于只读模式意味着可能存在数据丢失,必须执行显式动作才能将其带回可写状态:运行
vdoforcerebuild后,vdo 目标下次启动时会重建(rebuild)其元数据; - 重建后可能会有部分数据丢失,但重建出的元数据在内部是自洽的,目标将重新可写。
这一语义与内核驱动中 repair 逻辑相对应——从源码结构看,drivers/md/dm-vdo/ 目录下存在 repair.c、admin-state.c、status-codes.c 等文件,分别对应修复流程、卷状态机(normal / recovering / read-only)与状态码定义;vdo 设计文档在 “Read-only Rebuild” 一节进一步说明:只读重建时引用计数不从 slab 日志重建,而是清零整个块映射后依据块映射重新推导,从而保证块映射与引用计数互相一致。
元数据空间要求
每个 vdo 卷至少预留 3GB 空间存放元数据,具体大小还取决于其配置。规划容量时,必须确认去重与压缩节省的空间不会被元数据开销抵消。
官方提供了 vdoestimator 估算工具(位于独立的 vdoestimator 仓库),可以对特定数据集计算可节省空间的估计值。结合 vdo 设计文档的数据,该目标可实现最高 254:1 的去重比(一个 4K 块最多被 254 个逻辑副本共享)、14:1 的压缩比,全零块完全不占存储;去重索引默认可容纳 6400 万条记录、对应约 256GB 的去重窗口(deduplication window)。因此对可压缩/可去重率低的裸数据,vdo 的净收益可能为负,务必先估算。
Table line 格式与必选参数
通过 dmsetup 加载 vdo 目标时,表行(table line)格式为:
<offset> <logical device size> vdo V4 <storage device>
<storage device size> <minimum I/O size> <block map cache size>
<block map era length> [optional arguments]
其中 V4 为当前元数据格式版本。必选参数说明如下:
| 参数 | 说明 |
|---|---|
offset |
vdo 卷逻辑空间起始处的偏移量,以扇区(sector)计 |
logical device size |
vdo 卷将要服务的设备大小,以扇区计。必须与 vdo 卷当前的逻辑大小一致 |
storage device |
存放该 vdo 卷数据与元数据的底层设备 |
storage device size |
存放 vdo 卷的设备大小,以 4096 字节块数计。必须与 vdo 卷当前大小一致 |
minimum I/O size |
vdo 卷接受的最小 I/O 大小,以字节计。合法值只有 512 或 4096,推荐 4096 |
block map cache size |
块映射缓存大小,以 4096 字节块数计。最小且推荐值为 32768 块;若 logical 线程数非零,缓存大小必须至少为每 logical 线程 4096 块 |
block map era length |
块映射缓存把已修改的块映射页写出的速度。较小的 era length 倾向于缩短重建(rebuild)时间,代价是正常运行时块映射写增多。最大且推荐值为 16380,最小值为 1 |
可选参数:线程与杂项
可选参数以 <key> <value> 对的形式出现在表行末尾。
线程类参数
vdo 将不同类别的工作分派给不同的线程组,各组线程数可独立配置。约束规则:若 <hash>、<logical>、<physical> 全为 0,则三类工作由单一线程处理;若其中任一非零,则三者都必须非零。
| 参数 | 说明 |
|---|---|
ack |
用于完成(complete)bio 的线程数。由于完成 bio 会调用 vdo 卷之外的任意完成函数,此类线程让 vdo 在 bio 完成缓慢时仍可持续处理请求。默认 1 |
bio |
用于向下层存储提交(issue)bio 的线程数。此类线程让 vdo 在 bio 提交缓慢时仍可持续处理请求。默认 4 |
bioRotationInterval |
每个 bio 线程切换到下一个线程前,向其入队的 bio 数量。取值必须大于 0 且不超过 1024,默认 64 |
cpu |
用于 CPU 密集型工作(如哈希与压缩)的线程数。默认 1 |
hash |
用于基于数据块哈希值进行数据比对去重管理的线程数。默认 0 |
logical |
用于基于传入 bio 逻辑地址管理缓存与锁的线程数。默认 0,最大 60 |
physical |
用于管理底层存储设备的线程数。格式化时选定 slab 大小;vdo 存储设备必须足够大,至少每个 physical 线程有 1 个 slab。默认 0,最大 16 |
这些线程类别与内核驱动的源码文件一一对应:从源码结构看,io-submitter.c 对应 bio 提交线程、completion.c 对应 ack 完成线程、logical-zone.c 与 physical-zone.c 分别管理逻辑/物理分区,dedupe.c 及 indexer/ 子目录承载哈希比对与去重索引(UDS)。每个分区(zone)绑定一个工作队列(见 funnel-workqueue.c),工作队列隐式持有该分区数据结构的“锁”,这正是 vdo 近乎无锁并发的实现基础(详见 vdo 设计文档的 “Zones and Threading”)。
杂项参数
maxDiscard:接受的最大 discard bio 大小,以 4096 字节块计。对 vdo 卷的 I/O 请求通常被拆分为 4096 字节块处理,一次最多处理 2048 个块;但 discard 请求可被自动合并拆分为单个 bio 中最多<maxDiscard>个 4096 字节块,且同时进行的 discard 请求上限为 1500。增大该值可能提升整体性能,代价是单个 discard 请求延迟升高。默认值与最小值为 1,最大值为 UINT_MAX / 4096。deduplication:是否启用去重。默认on,可接受on/off。compression:是否启用压缩。默认off,可接受on/off。
运行中修改设备(热修改)
一张修改后的表可以加载到运行中、未挂起的 vdo 卷上;修改将在设备下次 resume 时生效。可修改的参数为:<logical device size>、<physical device size>、<maxDiscard>、<compression>、<deduplication>。
容量修改的约束:
- 逻辑大小或物理大小变化成功后,vdo 会持久化新值,并在之后的每次启动时要求一致;
- 这两个参数只增不减;
- 逻辑大小不得超过 4 PB;
- 物理大小若增长,每次至少增加 32832 个 4096 字节块,且不得超过底层存储设备大小;
- 格式化时选定的 slab 大小决定了物理大小上限:物理大小永远不能增长到提供超过 8192 个 slab 的规模,且每次增长必须至少新增 1 个 slab。
完整操作示例(继承自官方文档)
1. 启动一个已格式化的 vdo 卷:1GB 逻辑空间 + 1GB 物理空间,存于有超过 1GB 空间的 /dev/dm-1:
dmsetup create vdo0 --table \
"0 2097152 vdo V4 /dev/dm-1 262144 4096 32768 16380"
2. 将逻辑大小扩到 4GB:
dmsetup reload vdo0 --table \
"0 8388608 vdo V4 /dev/dm-1 262144 4096 32768 16380"
dmsetup resume vdo0
3. 将物理大小扩到 2GB:
dmsetup reload vdo0 --table \
"0 8388608 vdo V4 /dev/dm-1 524288 4096 32768 16380"
dmsetup resume vdo0
4. 物理大小再增 1GB,并同时调大 maxDiscard:
dmsetup reload vdo0 --table \
"0 10485760 vdo V4 /dev/dm-1 786432 4096 32768 16380 maxDiscard 8"
dmsetup resume vdo0
5. 停止 vdo 卷:
dmsetup remove vdo0
6. 再次启动 vdo 卷。 注意:逻辑与物理设备大小必须与之前保持一致,但其他参数可以改变:
dmsetup create vdo1 --table \
"0 10485760 vdo V4 /dev/dm-1 786432 512 65550 5000 hash 1 logical 3 physical 2"
这条示例同时展示了最小 I/O 改为 512、块映射缓存改为 65550 块、era length 改为 5000、以及启用 hash 1 logical 3 physical 2 三线程组配置的完整组合写法。
消息接口(Messages)
所有 vdo 设备接受如下形式的消息:
dmsetup message <target-name> 0 <message-name> <message-parameters>
| 消息 | 说明 |
|---|---|
stats |
输出 vdo 统计信息当前视图,主要供用户态 vdostats 程序解析输出缓冲区 |
config |
输出有用的 vdo 配置信息,主要供希望重建一个相似 VDO 卷的用户了解当初的创建配置 |
dump |
将多个内部结构转储到系统日志。并非总是安全的,只应用于调试挂死的 vdo |
dump-on-shutdown |
下次 vdo 关闭时执行一次默认 dump |
dump 消息的可选参数(指定要转储的结构):
| 取值 | 含义 |
|---|---|
viopool |
传入 bio 所在的 I/O 请求池 |
pools |
viopool 的同义词 |
vdo |
管理磁盘数据的大部分结构 |
queues |
每个 vdo 线程的基本信息 |
threads |
queues 的同义词 |
default |
等价于 queues vdo |
all |
以上全部 |
内核侧的消息分发在 dm-vdo-target.c 中实现:dump 消息的处理自第 1153 行起分派到各结构,stats 与 config 消息分别在第 1201、1204 行按 argc == 1 的精确匹配处理;统计消息的实际格式化由 message-stats.c 承担。
Status 状态输出
vdo 的状态行格式为:
<device> <operating mode> <in recovery> <index state>
<compression state> <physical blocks used> <total physical blocks>
| 字段 | 取值与含义 |
|---|---|
device |
vdo 卷名称 |
operating mode |
当前运行模式:normal(正常)、recovering(检测到元数据问题并正在尝试修复)、read-only(发生错误,仅支持读操作、拒绝写) |
in recovery |
是否正处于恢复模式:recovering 或 -(未在恢复) |
index state |
去重索引当前状态:closed、closing、error、offline、online、opening、unknown |
compression state |
压缩当前状态:offline 或 online |
used physical blocks |
vdo 卷正在使用的物理块数量 |
total physical blocks |
vdo 卷可用的物理块总量;该值与 used 值之差即卷写满前的剩余块数 |
内存需求模型
vdo 目标的内存由固定部分与随规模扩展的部分组成:
- 固定 38 MB RAM,另加以下随目标规模线性扩展的部分:
- 块映射缓存每配置 1 MB 需 1.15 MB RAM(块映射缓存的内存需求最低约 150 MB);
- 逻辑空间每 1 TB 需 1.6 MB RAM;
- 卷管理的物理存储每 1 TB 需 268 MB RAM。
- 去重索引另需随去重窗口大小扩展的内存:稠密(dense)索引每 1 TB 窗口需 1 GB RAM;稀疏(sparse)索引每 10 TB 窗口需 1 GB RAM。索引配置在格式化(format)时设定,之后不可修改。
这与 vdo 设计文档中的说明一致:默认索引(约 256GB 去重窗口)可通过增大索引内存(窗口等比放大、存储与内存同步翻倍)或启用稀疏索引(窗口扩大 10 倍、存储扩大 10 倍、内存不变,可检出标准索引 97%–99% 的去重量)两种方式扩展去重窗口,且都只能在创建时确定。
模块参数
vdo 驱动提供一个数值型模块参数 log_level,控制驱动日志的详略程度,默认值为 6(LOGLEVEL_INFO 及更严重的消息)。
源码印证:
- dm-vdo-target.c 第 3056–3057 行注册该参数:
module_param_named(log_level, vdo_log_level, uint, 0644); MODULE_PARM_DESC(log_level, "Log level for log messages");; - logger.c 第 19 行定义全局变量
int vdo_log_level = VDO_LOG_DEFAULT;,与文档所述默认级别对应。
运行行为:vdo 与其他存储目标的关键差异
使用 dm-vdo 时必须了解其行为与其他存储目标的不同之处:
- 覆写已有块不保证成功。 由于底层存储可能被多重引用(multiply referenced),覆写一个已有块通常要求 vdo 有一个空闲块可用(copy-on-write 语义);
- discard 是回收空间的途径但不保证回收。 块不再使用时,对相应块发送 discard 请求可让 vdo 释放对这些块的引用;对精简配置的 vdo 而言,discard 未用块是防止目标耗尽空间的必要操作。但由于重复块共享,对某个逻辑块的 discard 不保证能回收空间;
- 崩溃韧性依赖下层 flush 实现。 假设底层存储正确实现了 flush 请求,vdo 对崩溃是韧性(resilient)的;但崩溃后未 flush 的写可能持久化、也可能没有;
- 每次写都伴随大量处理,但高度可并行。 vdo 在高 I/O 深度下获得更好的吞吐,可并行支持最多 2048 个请求——这与 vdo 设计文档所述“固定 2048 个 data_vio 池”一致:data-vio.c 管理的 data_vio 池大小即为 2048,既限制了崩溃恢复所需的回溯工作量,也是并发上限的来源。
调优实践
vdo 的选项很多,缺乏对负载的完整了解很难做出最优选择。且多数配置选项必须在目标启动时设定,不能在不彻底关闭目标的情况下更改——目标处于活动状态时无法变更配置。理想做法是在生产部署前用模拟负载做调优。
1. 块映射缓存大小(最重要的调整项)
为服务任意逻辑地址的请求,vdo 必须加载持有相关映射的块映射部分,这些映射被缓存;当工作集装不进缓存时性能受损。默认情况下,vdo 分配 128 MB 元数据缓存内存,足以高效访问同一时刻 100 GB 的逻辑空间;更大的工作集应按比例放大。
2. logical 与 physical 线程数
logical 线程各控制块映射的一段不相交区域,增加 logical 线程可提高并行度与吞吐;physical 线程各控制数据块的一段不相交区域,同理可提升吞吐。但线程过多会浪费资源并增加争用。注意表行参数约束:logical 线程非零时,块映射缓存必须至少每线程 4096 块。
3. bio 提交线程数
bio 提交线程控制发送 I/O 到下层存储的并行度:线程越少,I/O 请求重排优化性能的机会越大,但每个请求提交前的等待时间也越长;线程越多则相反。默认 4,可用 bioRotationInterval(默认 64,范围 1–1024)控制各 bio 线程间的轮转粒度。
4. bio 确认(ack)线程
用于结束 I/O 请求。之所以放在专用线程,是因为执行 bio 回调所需的工作量是 vdo 自身无法控制的。通常 1 个线程足够,但当 bio 的回调 CPU 开销较重时,增加线程可能有益。
5. CPU 线程
用于哈希与压缩;在启用压缩的负载下,更多 CPU 线程可能带来更高吞吐。
6. hash 线程
用于按哈希对活动请求排序并判定是否应去重;其最耗 CPU 的动作是比对 4096 字节数据块。多数情况下 1 个 hash 线程足够。
内核驱动源码结构印证
内核侧实现位于 drivers/md/dm-vdo/(Kconfig 与 Makefile 控制构建)。从源码结构看,各核心文件与文档所述功能的对应关系如下:
| 文档概念 | 源码文件 |
|---|---|
表行解析、消息分发、log_level 模块参数、状态输出 |
dm-vdo-target.c |
| 块映射(block map)与缓存 | block-map.c |
| slab 数据池 | slab-depot.c |
| 恢复日志 | recovery-journal.c |
| 压缩打包(packer) | packer.c |
| 去重与 UDS 索引 | dedupe.c、indexer/ |
| data_vio / vio I/O 对象池 | data-vio.c、vio.c |
| 逻辑/物理线程分区 | logical-zone.c、physical-zone.c |
| 崩溃修复(rebuild) | repair.c |
| 统计消息 | message-stats.c |
| 日志级别 | logger.c |
此外,该目录下的 murmurhash3.c 表明数据块哈希采用的是非加密的 MurmurHash3,这正对应 vdo 设计文档中“因未使用加密哈希,理论上可构造恶意哈希冲突负载,故 vdo 将索引位置视为提示(hint)并在共享前读取验证数据”的设计说明。
小结
dm-vdo 通过“去重索引 + 引用计数块映射 + slab 数据池 + 恢复日志”的组合,在块设备上实现了去重、压缩与精简配置。本文继承的 vdo.rst 给出了完整的运维契约:table line 的 7 个必选参数与 10 个可选参数、只增不减的扩容规则、5 个可热修改参数、4 类消息、状态行 7 个字段、内存公式与 log_level 模块参数;而 drivers/md/dm-vdo/ 源码则印证了这些行为在内核中的落点。实际部署时的三条硬纪律:格式化前先估算元数据与索引内存开销、精简配置卷必须配合文件系统 discard、只读模式必须显式 vdoforcerebuild 后才能恢复写入。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00