首页
/ Linux dm-vdo(Virtual Data Optimizer)深入指南:块级去重、压缩与精简配置的实现、参数与运维全解

Linux dm-vdo(Virtual Data Optimizer)深入指南:块级去重、压缩与精简配置的实现、参数与运维全解

2026-09-07 14:04:58作者:蔡丛锟

本文以 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 必须理解的机制):

  1. 多数情况下,vdo 目标在下一次启动时会自动从崩溃中恢复;
  2. 若遇到不可恢复错误(正常运行中或崩溃恢复过程中),目标会进入只读模式(read-only mode),或以只读模式启动;
  3. 由于只读模式意味着可能存在数据丢失,必须执行显式动作才能将其带回可写状态:运行 vdoforcerebuild 后,vdo 目标下次启动时会重建(rebuild)其元数据
  4. 重建后可能会有部分数据丢失,但重建出的元数据在内部是自洽的,目标将重新可写。

这一语义与内核驱动中 repair 逻辑相对应——从源码结构看,drivers/md/dm-vdo/ 目录下存在 repair.cadmin-state.cstatus-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.cphysical-zone.c 分别管理逻辑/物理分区,dedupe.cindexer/ 子目录承载哈希比对与去重索引(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 行起分派到各结构,statsconfig 消息分别在第 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 去重索引当前状态:closedclosingerrorofflineonlineopeningunknown
compression state 压缩当前状态:offlineonline
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,控制驱动日志的详略程度,默认值为 6LOGLEVEL_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 时必须了解其行为与其他存储目标的不同之处:

  1. 覆写已有块不保证成功。 由于底层存储可能被多重引用(multiply referenced),覆写一个已有块通常要求 vdo 有一个空闲块可用(copy-on-write 语义);
  2. discard 是回收空间的途径但不保证回收。 块不再使用时,对相应块发送 discard 请求可让 vdo 释放对这些块的引用;对精简配置的 vdo 而言,discard 未用块是防止目标耗尽空间的必要操作。但由于重复块共享,对某个逻辑块的 discard 不保证能回收空间;
  3. 崩溃韧性依赖下层 flush 实现。 假设底层存储正确实现了 flush 请求,vdo 对崩溃是韧性(resilient)的;但崩溃后未 flush 的写可能持久化、也可能没有;
  4. 每次写都伴随大量处理,但高度可并行。 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/KconfigMakefile 控制构建)。从源码结构看,各核心文件与文档所述功能的对应关系如下:

文档概念 源码文件
表行解析、消息分发、log_level 模块参数、状态输出 dm-vdo-target.c
块映射(block map)与缓存 block-map.c
slab 数据池 slab-depot.c
恢复日志 recovery-journal.c
压缩打包(packer) packer.c
去重与 UDS 索引 dedupe.cindexer/
data_vio / vio I/O 对象池 data-vio.cvio.c
逻辑/物理线程分区 logical-zone.cphysical-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 后才能恢复写入。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389