首页
/ Linux dm-cache 设备映射缓存目标:架构、工作模式与配置实操详解

Linux dm-cache 设备映射缓存目标:架构、工作模式与配置实操详解

2026-09-07 10:20:45作者:邬祺芯Juliet

dm-cache 是 Linux Device Mapper 框架下的一个块设备缓存目标(cache target),由 Joe Thornber、Heinz Mauelshagen 与 Mike Snitzer 编写。它以 SSD 等小容量高速设备为缓存,动态地把慢速大容量块设备(如机械硬盘)上的热点数据迁移到缓存设备上,从而在不改动上层文件系统与应用的条件下提升整体 IO 性能。本文基于内核源码树中的官方文档 Documentation/admin-guide/device-mapper/cache.rst 展开,并结合内核源码 drivers/md/dm-cache-target.cdrivers/md/dm-cache-metadata.c 及配套的 cache-policies.rst,系统讲解 dm-cache 的三设备模型、三种工作模式、固定块大小约束、替换策略、target 构造参数、状态输出格式、控制消息,并给出可直接落地的 dmsetup 配置示例。读完本文你将掌握如何正确选择缓存模式与块大小、如何通过 DM table 构建缓存设备、如何解读缓存运行状态,以及如何通过消息机制调整迁移阈值或做缓存块失效(invalidation)。

一、dm-cache 简介与适用场景

dm-cache 的目标是提升一个慢速块设备的性能:把一部分数据动态迁移到更小、更快的设备上(例如把机械盘上的热数据放到 SSD)。作为一个 device-mapper target,它可以被插入到 dm 栈的不同层次中,例如位于 thin-provisioning pool 的数据设备之上,为精简供给卷提供缓存加速。

文档同时指出一个客观的定位说明:与虚拟内存系统结合得更紧密的缓存方案理论上会获得更好的性能,dm-cache 是一个通用型设备映射层方案。也就是说,dm-cache 的价值更多体现在灵活性与普适性上,而不在"算法效率极致"这一维度上,这有助于读者对它的能力边界建立准确预期。

与缓存目标绑定非常紧密的两个子系统:

  • 元数据格式复用:该 target 复用了精简供给(thin-provisioning)所使用的元数据库。相关背景可参考 Documentation/admin-guide/device-mapper/persistent-data.rst
  • 策略插件机制:"哪些数据需要迁移、何时迁移"的决策被交给可插拔的策略模块(policy)。内核已提供若干策略,社区也可以为特定 IO 场景(例如虚拟机镜像服务器)贡献新的策略。

二、核心术语:迁移 / 提升 / 降级

文档定义了三组基本概念,理解它们是读懂后续所有设计的前提:

  • 迁移(Migration):把一个逻辑块的主副本(primary copy)从一个设备移动到另一个设备。
  • 提升(Promotion):从慢速设备(origin)迁移到快速设备(cache)。
  • 降级(Demotion):从快速设备(cache)迁移到慢速设备(origin)。

需要特别强调的是:origin 设备上始终保留逻辑块的一个副本。这个副本可能已经过期(out of date),也可能与 cache 设备上的副本保持同步——具体行为取决于所采用的策略与工作模式。这是 dm-cache"缓存并非移动数据"语义的关键:origin 永远是数据的权威锚点之一。

三、设计细节

3.1 三个子设备

dm-cache target 由三个块设备构造而成:

  1. origin 设备:大而慢的数据源设备。
  2. cache 设备:小而快的缓存数据设备。
  3. metadata 设备:小容量元数据设备,记录哪些块在缓存中、哪些块是脏的(dirty),以及供策略对象使用的额外提示(hints)。

把元数据单独放一个设备而非塞进 cache 设备,是为了让卷管理器(volume manager)能对它做差异化配置——例如做成镜像以获得额外健壮性。注意约束:一块 metadata 设备只能被一个 cache 设备使用

从源码结构看,三个设备在 target 的构造参数解析中分别对应独立的解析函数:parse_metadata_devparse_cache_devparse_origin_dev(见 drivers/md/dm-cache-target.c)。其中元数据设备大小超过阈值时内核仅给出警告("excess space will not be used"),多余的容量不会被使用。

3.2 固定块大小及其权衡

origin 设备被划分为固定大小的块。该块大小在首次创建 cache 时配置(之后不能修改),文档给出的经验值是 256KB~1024KB。合法取值范围与约束为:

  • 最小值:64 sectors(32KB)
  • 最大值:2097152 sectors(1GB)
  • 必须是 64 sectors(32KB)的整数倍

源码中这一校验由宏与 parse_block_size() 共同落实(drivers/md/dm-cache-target.cdrivers/md/dm-cache-target.c):

#define DATA_DEV_BLOCK_SIZE_MIN_SECTORS (32 * 1024 >> SECTOR_SHIFT)  /* 64 sectors = 32KB */
#define DATA_DEV_BLOCK_SIZE_MAX_SECTORS (1024 * 1024 * 1024 >> SECTOR_SHIFT) /* 2097152 sectors = 1GB */
if (kstrtoul(dm_shift_arg(as), 10, &block_size) || !block_size ||
    block_size < DATA_DEV_BLOCK_SIZE_MIN_SECTORS ||
    block_size > DATA_DEV_BLOCK_SIZE_MAX_SECTORS ||
    block_size & (DATA_DEV_BLOCK_SIZE_MIN_SECTORS - 1)) {
    *error = "Invalid data block size";
    return -EINVAL;
}
if (block_size > ca->cache_sectors) {
    *error = "Data block size is larger than the cache device";
    return -EINVAL;
}

可见内核除了校验上下限与对齐要求之外,还额外拒绝"块大小大于整个 cache 设备"的非法配置。

固定块大小显著简化了 target 实现,但本质上是一种折中:

  • 块过大:即使只有块内一小部分被频繁访问,整块也会被提升进缓存,浪费宝贵的缓存空间;
  • 块过小:会放大元数据开销(无论是核心态内存还是磁盘上的元数据)。

因此块大小的选择需要在"缓存空间利用率"与"元数据开销"之间寻找平衡。

3.3 三种缓存工作模式

dm-cache 支持三种工作模式:writebackwritethroughpassthrough

模式 行为 适用场景
writeback(默认) 对已缓存块的写只落到 cache 设备,并在元数据中把该块标记为脏(dirty) 追求最佳写性能,能容忍异常掉电时丢失少量最近写入数据
writethrough 对已缓存块的写必须同时写入 origin 与 cache 设备后才算完成,干净块保持干净 对数据可靠性要求较高,写缓存块不会产生与 origin 不一致的内容
passthrough 所有读都由 origin 提供(读全部未命中缓存),所有写都转发到 origin;对已缓存块的写命中会使对应 cache 块失效(invalidate) 缓存内容与 origin 的相干性(coherency)无法确认的场合,如回滚底层存储快照后激活缓存

passthrough 模式有三条重要性质需要记牢:

  1. 进入 passthrough 前缓存必须是干净的(clean),否则内核会拒绝:源码中的报错为 "%s: cannot enter passthrough mode unless all blocks are clean"(见 drivers/md/dm-cache-target.c)。
  2. passthrough 允许在一个不需要担心相干性的前提下激活缓存设备:已经存在的相干性会被维持,但由于写操作不断发生,缓存会逐渐"冷却"。
  3. 后续如果缓存的相干性得到确认(或通过 invalidate_cblocks 消息主动建立),可以在缓存仍温热(still warm)的状态下把它切回 writethrough / writeback 模式;否则可先丢弃缓存内容再切换到目标模式。

此外内核在异常关闭后恢复时也有兜底保护:源码中 "%s: unable to resume into passthrough mode after unclean shutdown" 表明非正常关机后不允许直接以 passthrough 模式恢复(见 drivers/md/dm-cache-target.c)。

为了清理 cache 并做好退役准备,内核提供了一个简单的 cleaner 策略:它会将缓存中所有脏块写回(write back),常用于退役一块缓存收缩缓存设备。收缩(shrink)cache 快设备时,要求被移除区域内所有 cache 块都必须是干净的;若被移除区域仍含脏块,resize 会失败。因此在把缓存快设备体积缩小之前,务必先让缓存彻底变干净——这在 writeback 模式下尤为关键(writethrough 与 passthrough 模式本来就维持缓存干净,不存在该风险)。文档同时预告了未来的能力:支持按阈值部分清理缓存(partial clean),以便在 resize 期间保持缓存温热并继续运行于 writeback 模式。

3.4 迁移节流(migration throttling)

在 origin 与 cache 设备之间迁移数据会消耗带宽。用户可以通过设置节流阈值,防止某一时刻发生过量的迁移。当前实现尚未考虑同时进行的普通 IO 流量对设备的占用(文档注明这是未来需要改进的方向,避免在 IO 高峰时段执行迁移)。

现阶段通过消息 migration_threshold <#sectors> 设置"单次允许迁移的最大扇区数":

从源码看,节流逻辑还结合了设备的空闲(idle)判定:当 IO 空闲且当前待迁移量不超过阈值时才执行迁移(见 drivers/md/dm-cache-target.c)。

3.5 磁盘元数据的落盘时机

  • 磁盘元数据在每个 FLUSH 或 FUA 的 bio 写入时提交(commit);
  • 如果一直没有这类请求,则每秒钟自动提交一次。

这意味着 cache 的行为类似一块带易失性写缓存的物理磁盘:一旦断电,可能丢失少量最近的写入。但无论发生何种崩溃,元数据本身应当始终是自洽的(consistent)。

另一个关键设计是脏标记是"提示"而非严格实时状态:cache 块的 dirty 状态变化过于频繁,不适合每次都在线更新。常规操作下,dirty 状态会在 dm 设备被 suspend 时写盘;如果系统崩溃,重启后所有 cache 块会一律被假定为脏。这一保守策略牺牲了崩溃后的小部分热数据信息,换来了元数据一致性的简单与可靠。

策略插件还可以为每个 cache 块存放一小段提示数据(per-block policy hints),长度由策略自定但应尽量小。与 dirty 标记类似,这段数据在崩溃后会丢失,因此策略必须永远能退回到一个安全的默认值。注意:策略提示只影响性能,不影响正确性("Policy hints affect performance, not correctness")。

3.6 策略消息与丢弃状态位图

不同策略有不同的专属可调参数,dm 通过统一的 device-mapper 消息机制来读写它们(文档指出 sysfs 接口也可行,但目前走消息通道)。消息的通用语义见后文"控制消息"一节,各策略专属参数详见 Documentation/admin-guide/device-mapper/cache-policies.rst

在迁移时,如果已知某个块已被 discard,就可以避免拷贝数据。典型场景是 mkfs 丢弃整个块设备。为此 dm-cache 保存了一张追踪块 discard 状态的位图(bitset)。由于需要记录整块 origin 设备的 discard 状态(对比 dirty 位图只需要覆盖更小的 cache 设备),该位图允许使用与 cache 块不同的块大小。

四、Target 接口:构造、状态与消息

4.1 构造函数(CTR)

构造 cache target 的完整命令行语法为:

cache <metadata dev> <cache dev> <origin dev> <block size>
      <#feature args> [<feature arg>]*
      <policy> <#policy args> [policy args]*

参数说明:

参数 含义
metadata dev 保存持久元数据的快设备
cache dev 保存缓存数据块的快设备
origin dev 保存原始数据块的慢设备
block size 缓存单元大小,单位 sector
#feature args 传入的 feature 参数个数
feature args writethroughpassthrough(默认是 writeback)
policy 要使用的替换策略名
#policy args 传给策略的 key/value 对参数个数(须为偶数)
policy args 传给策略的 key/value 对,例如 sequential_threshold 1024

从源码构造函数的注释可以确认 target 参数解析的完整顺序与三个设备的打开方式(drivers/md/dm-cache-target.c)。默认特征在 init_features() 中初始化(drivers/md/dm-cache-target.c):工作模式默认 writeback(CM_IO_WRITEBACK)、元数据版本 1、discard 下发默认开启(discard_passdown = true)。

可选 feature 参数:

feature 参数 说明
writethrough 透写式缓存:禁止 cache 块内容与 origin 块内容不一致。不指定时(默认行为)为性能考虑会在稍后把 cache 块内容写回 origin,因此两者内容可能不同
passthrough 降级模式,适用于各种缓存相干性场景(例如回滚底层存储的快照)。读写总是去 origin;若写命中某个已被缓存的 origin 块,则对应的 cache 块被失效。启用 passthrough 前缓存必须是干净的
metadata2 使用第 2 版元数据:把 dirty 位存放在一颗独立的 btree 中,从而提升 cache 关闭(shutdown)的速度
no_discard_passdown 禁止把 discard 从 cache 下传到 origin 的数据设备

补充两点与"default"策略相关的官方提醒:

  • 名为 default 的策略总是被注册,它是"当前我们认为综合表现最好的策略"的别名;
  • 由于不同内核版本之间 default 策略可能变化,如果你依赖某个具体策略的特性,务必在构造参数中按名称显式指定该策略

在当前的源码树中,default 策略实际指向 smq 策略(详见后文第五节),历史遗留的 mq 名字也已成为 smq 的别名。

4.2 Status 输出

查询 cache 设备状态时(例如 dmsetup status),输出格式为:

<metadata block size> <#used metadata blocks>/<#total metadata blocks>
<cache block size> <#used cache blocks>/<#total cache blocks>
<#read hits> <#read misses> <#write hits> <#write misses>
<#demotions> <#promotions> <#dirty> <#features> <features>*
<#core args> <core args>* <policy name> <#policy args> <policy args>*
<cache metadata mode>

各字段含义:

字段 含义
metadata block size 每块元数据块的固定大小,单位 sector
#used metadata blocks 已使用的元数据块数
#total metadata blocks 元数据块总数
cache block size cache 设备的可配置块大小,单位 sector
#used cache blocks 常驻缓存中的块数
#total cache blocks cache 块总数
#read hits READ bio 被映射到 cache 的次数
#read misses READ bio 被映射到 origin 的次数
#write hits WRITE bio 被映射到 cache 的次数
#write misses WRITE bio 被映射到 origin 的次数
#demotions 块被移出 cache 的次数
#promotions 块被移入 cache 的次数
#dirty 缓存中与 origin 不一致的块数
#feature args 其后 feature args 的个数
feature args writethrough(可选;writeback 为默认不打印)
#core args 核心参数个数(必须为偶数)
core args 用于调节 core 的 key/value 对,例如 migration_threshold
policy name 策略名称
#policy args 策略参数个数(必须为偶数)
policy args 策略 key/value 对,例如 sequential_threshold
cache metadata mode ro 表示只读,rw 表示读写

关于状态中两个异常指示:

  • 若遇到严重到连只读模式都不安全的故障,将不再允许任何 IO,状态输出仅含字符串 Fail,此时应使用用户态恢复工具处理;
  • 当某次元数据操作失败、导致元数据 superblock 中的 needs_check 标志被置位时,状态末尾会出现 needs_check,否则为 -。出现 needs_check 时,元数据设备必须先被停用(deactivate)并做检查/修复,cache 才能重新完全正常工作。

内核侧对应实现可见 drivers/md/dm-cache-metadata.cdm_cache_metadata_set_needs_check / dm_cache_metadata_needs_check)。另外从 status 输出代码还能看到一条辅助信息:cache 特征与版本会以 writethrough=, passthrough=, metadata2= 等形式附加打印(drivers/md/dm-cache-target.cdrivers/md/dm-cache-target.c),在需要诊断缓存模式时可多加留意。

4.3 控制消息(Messages)

不同策略有各自专属的可调参数,dm-cache 用统一的 device-mapper 消息机制读写它们。消息格式为:

<key> <value>

例如:

dmsetup message my_cache 0 sequential_threshold 1024

另一个核心消息是 invalidate_cblocks(缓存块失效)。失效(invalidation)指不写回、直接从缓存中移除一个条目。其格式为:

invalidate_cblocks [<cblock>|<cblock begin>-<cblock end>]*

规则要点:

  • 每个 cblock 区间采用**"末端不包含"**(one past the end)语义,例如 5-10 表示从 5 到 9 的区间;
  • 每个 cblock 必须以十进制书写(文档指出,未来可能需要支持十六进制区间变体,以便更高效地对大规模缓存做失效);
  • 使用 invalidate_cblocks 时缓存必须处于 passthrough 模式——内核对此有显式校验:"%s: cache has to be in passthrough mode for invalidation"(见 drivers/md/dm-cache-target.c)。

示例:

dmsetup message my_cache 0 invalidate_cblocks 2345 3456-4567 5678-6789

这条命令会依次失效 cblock 2345、区间 3456~4566、区间 5678~6788。消息分发入口可见 drivers/md/dm-cache-target.c

五、内核自带的替换策略:mq / smq / cleaner

策略的编写与实现细节、以及缓存策略消息的完整说明,统一收录于官方配套文档 Documentation/admin-guide/device-mapper/cache-policies.rst。文档同时给出了对策略作者的引导:尽量把事务性(transactionality)排除在策略之外——core 会小心地不去询问任何正在迁移中的块;每个被 target 映射的 bio 都会被交给策略,策略可以简单返回 HIT/MISS,或发起一次迁移;由于是按 bio 而非 request 映射,策略容易被大量小 bio 迷惑,因此 core 会周期性向策略发送 tick,建议策略在每个 tick 内对同一块的统计更新不超过一次。

5.1 multiqueue(mq):已被 smq 取代

mq 策略当前仅是 smq 的别名。其历史调优参数(sequential_thresholdrandom_thresholdread_promote_adjustmentwrite_promote_adjustmentdiscard_promote_adjustment)仍被接受,但不再有任何实际效果

5.2 Stochastic multiqueue(smq):默认策略

smq 策略是当前的默认策略,核心实现位于 drivers/md/dm-cache-policy-smq.c。它针对 mq 的若干问题做了改进:

  • 更低的内存占用:mq 在 64 位机器上每 cache 块约用 88 字节;smq 用 28 位索引而非指针实现数据结构、不保存显式命中计数、用"热点队列"(hotspot queue)取代预缓存(pre-cache,热点条目仅占四分之一槽位),综合下来每 cache 块约 25 字节
  • 层级均衡(level balancing):mq 依据命中计数(约 ln(hit count))把条目放入多层队列的不同层级,导致底层条目过多而失衡;smq 不维护命中计数,而是把命中的条目与上一层最近最少使用(LRU)的条目对调,通过随机过程形成整体顺序,从而可控制每个层级的条目数量,做出更合理的提升/降级决策。
  • 强适应性(adaptability):mq 靠命中计数决定提升,新块需超过缓存内最低命中计数,导致工作负载变化时适应很慢;smq 不维护命中计数,并持续跟踪热点队列的表现——热点队列表现不佳时,它会更快地在层级间移动条目,从而迅速适应新的 IO 模式。

用户只需重载使用 cache target 的 DM table,即可从 mq 平滑切换到 smq。副作用有两点:其一,mq 的全部策略提示会被丢弃;其二,在 smq 重新计算 origin 设备上应被缓存的热点之前,缓存性能可能略有下降。

5.3 cleaner:退役专用

cleaner 策略把缓存中所有脏块写回,用于将一块缓存退役(decommission)。它也正是上一节提到的"收缩/退役缓存前清空脏块"的执行者。

六、实战示例:用 dmsetup 创建 dm-cache

原文档给出的权威示例(命令较长时用 \ 续行):

示例 1:writeback 模式 + default 策略

dmsetup create my_cache --table '0 41943040 cache /dev/mapper/metadata \
        /dev/mapper/ssd /dev/mapper/origin 512 1 writeback default 0'

解读:

  • 0 41943040:起始扇区 0,总长度 41943040 sectors(即 20GB)的映射区段;
  • cache:target 类型;
  • /dev/mapper/metadata:元数据设备;
  • /dev/mapper/ssd:缓存(快)设备;
  • /dev/mapper/origin:原始(慢)设备;
  • 512:块大小 512 sectors(256KB),落在文档推荐的 256KB~1024KB 区间;
  • 1:1 个 feature 参数;
  • writeback:写回模式;
  • default:默认策略(当前指向 smq);
  • 0:0 个策略参数。

示例 2:writeback 模式 + mq(现为 smq 别名)+ 策略参数

dmsetup create my_cache --table '0 41943040 cache /dev/mapper/metadata \
        /dev/mapper/ssd /dev/mapper/origin 1024 1 writeback \
        mq 4 sequential_threshold 1024 random_threshold 8'

解读:

  • 块大小为 1024 sectors(512KB);
  • 策略名为 mq(等价于 smq),后面跟着 4 个策略参数:
    • sequential_threshold 1024
    • random_threshold 8

配套文档 cache-policies.rst 中还给出了一个 128GB 规模的等效示例(0 268435456 cache /dev/sdb /dev/sdc /dev/sdd 512 0 mq 4 sequential_threshold 1024 random_threshold 8),可作为大容量场景的参考模板。

把缓存运行中下发策略消息的写法汇总如下:

dmsetup message <mapped device> 0 sequential_threshold 1024
dmsetup message <mapped device> 0 random_threshold 8
dmsetup message my_cache 0 migration_threshold 4096
dmsetup message my_cache 0 invalidate_cblocks 2345 3456-4567 5678-6789

其中 migration_threshold 是 cache target core 直接支持的核心参数(源码注释明确标注该 key 由 core 处理,见 drivers/md/dm-cache-target.c),而 sequential_thresholdrandom_threshold 等则转发给策略处理。原文档还提到该功能有一组独立的自动化测试套件(device-mapper test suite,由维护者作为独立工程维护),可作为验证行为与回归测试的参考。

七、实操要点速查

  1. 选择缓存模式:默认 writeback 追求写性能但掉电有丢数据窗口;writethrough 保证数据先落 origin;passthrough 只在缓存内容相干性存疑(如快照回滚后)时用于安全激活与失效重建,进入前缓存必须干净。
  2. 选择块大小:256KB~1024KB 是文档推荐区间;下限 32KB、上限 1GB,且必须是 64 sectors 的整数倍;块过大浪费缓存、过小放大元数据开销。
  3. 元数据设备:独立且小;只能被一个 cache 使用;容量超限部分不会被使用;生产环境建议对元数据设备做镜像以增强健壮性。
  4. 退役/收缩缓存:务必先用 cleaner 策略写回全部脏块;writeback 模式下缩小快设备前不清理会直接导致 resize 失败。
  5. 显式指定策略:依赖具体策略特性时不要使用 default 别名,直接写 smq(或已等价的 mq)。
  6. 异常处理:status 中出现 Failneeds_check 时,停止常规 IO 并按文档要求停用元数据设备、做检查修复后再恢复。

八、进一步阅读

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

项目优选

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