Linux 混合存储缓存架构深度实战:dm-cache、dm-writecache 与 BCACHE 的全链路解析
在当今数据中心与边缘计算场景中,存储性能依然是制约系统整体吞吐的关键瓶颈之一。一块企业级 NVMe SSD 可以提供数百万 IOPS 的微秒级延迟,但容量有限且价格高昂;而一块大容量 HDD 虽然每 GB 成本极低,但其机械寻道延迟动辄数毫秒,随机 IOPS 不过百级。这种性能与成本之间的矛盾,直接推动了"混合存储缓存"技术的发展——通过 SSD 作为 HDD 的高速缓存层,使得上层应用既能享受 SSD 的高性能,又能获得 HDD 的大容量。
Linux 内核提供了多种混合存储缓存方案,其中最具代表性的是 Device Mapper 框架下的 dm-cache 与 dm-writecache,以及块层级别的 BCACHE。这三套方案看似解决同一类问题,但设计哲学、实现架构、适用场景却截然不同。本文将从底层原理出发,逐一剖析这三种缓存机制,并给出经过验证的生产环境部署策略。
一、为什么需要混合存储缓存
在深入方案之前,我们先明确混合存储缓存解决的核心问题。
现代的存储访问呈现明显的局部性特征——比如数据库的热数据、频繁访问的日志目录、虚拟机镜像中的操作系统文件——这些"热"数据通常只占总量的一小部分。如果我们能精确地将这 10%-20% 的热数据放在 SSD 上,就能用很小的成本获得接近全闪存的性能体验。
衡量一个混合缓存方案的核心指标有三个:
- 命中率 (Hit Rate):请求能被 SSD 缓存满足的比例,直接决定实际性能
- 写回策略:Write-back 还是 Write-through,决定写性能和数据安全的权衡
- 元数据开销:缓存管理自身消耗的内存与 SSD 空间
不同的方案在这三个指标上做出了截然不同的取舍,这也是为什么 Linux 内核需要多套并存。
二、dm-cache:设备映射器下的块级缓存
2.1 架构概览
dm-cache 是 Linux Device Mapper 框架中的一个 target,它通过将 SSD 设备作为缓存设备,机械硬盘作为后端存储设备,向对上层呈现一个统一的逻辑块设备。所有 I/O 请求首先到达 dm-cache 缓存层,由缓存策略决定是否从 SSD 缓存中处理还是需要穿透到 HDD。
# dm-cache 逻辑结构
┌─────────────────────────────────────┐
│ 逻辑块设备 (/dev/mapper/x) │
├─────────────────────────────────────┤
│ ┌───────────┐ ┌──────────────┐ │
│ │ SSD Cache │ │ HDD Origin │ │
│ │ (Cache) │ │ (Backend) │ │
│ └───────────┘ └──────────────┘ │
│ ↑ ↑ │
│ ┌──────────────────────┐ │
│ │ Metadata Device │ │
│ └──────────────────────┘ │
└─────────────────────────────────────┘
需要三个物理设备(或分区):
- Origin device:后端大容量 HDD,存储所有数据
- Cache device:高速 SSD,存储热数据副本
- Metadata device:存储缓存映射表,记录每个数据块位于缓存还是后端,以及脏块标记
2.2 缓存模式与写策略
dm-cache 提供三种缓存模式:
| 模式 | 读未命中 | 写策略 | 性能 | 数据安全 |
|---|---|---|---|---|
| write-through | 从后端读入,不缓存 | 同时写入两端 | 中等 | 最高 |
| write-back | 从后端读入,缓存 | 只写缓存,延迟刷回 | 最高 | 依赖刷回机制 |
| pass-through | 绕过缓存直接读后端 | 只写后端 | 最低 | 最高 |
在实际生产环境中,write-back 模式是最常用的选择。它一次写入就返回(热数据直接写 SSD),后台迁移线程异步将脏块刷回后端 HDD 并清理缓存空间。
dm-cache 还支持三种缓存策略(policy):
- smq (Stochastic Multi-Queue):默认策略,使用多个 FIFO 队列近似 LRU,开销最低,适合大多数场景
- mq (Multi-Queue):基于命中计数和队列长度的精细策略
- cleaner:强制将所有脏块刷回后端,用于安全移除缓存设备
2.3 块大小与元数据
dm-cache 将 Origin 设备划分为固定大小的"缓存块"(cache block),默认大小为 64KB(128 个 512B 扇区),可以在创建时指定从 32KB 到 1GB。
块大小的选择直接影响:
- 空间利用率:小文件场景下,大缓存块导致严重的内部碎片
- 元数据大小:metadata 设备需要 8-16 字节/块。1TB HDD 用 64KB 块需要约 160MB metadata
- 随机大 IO:顺序读写场景大缓存块效率更高
metadata 存储的数据结构包括:块映射(缓存块索引 → 后端块地址)、脏位图、命中统计等。这就是为什么 metadata 设备必须是独立的 SSD 或者 SSD 分区——否则频繁的元数据更新会严重拖慢缓存层。
2.4 生产部署示例
# 创建 dm-cache 缓存设备(假设 /dev/sdb 为 HDD,/dev/nvme0n1p1 为 cache,p2 为 metadata)
pvcreate /dev/sdb
vgcreate storage /dev/sdb
# 创建 cache-pool 逻辑卷
lvcreate -L 100G -n cache-pool storage /dev/nvme0n1p1
lvcreate -L 16G -n cache-meta storage /dev/nvme0n1p2
# 将 cache-pool 和 metadata 关联
lvconvert --type cache --cachemode writeback \
--cachemetadataformat 2 \
--cachepool storage/cache-pool \
--poolmetadata storage/cache-meta \
storage/your-lv
# 查看缓存状态
lvs -a -o +devices,cache_dirty_blocks,cache_read_hits,cache_read_misses,cache_write_hits,cache_write_misses
2.5 监控与调优
在生产监控中,我们需要关注以下指标:
# 查看缓存命中率
dmsetup status storage-your--lv
# 输出示例:
# 0 2147483648 cache 8 13818/409600 512 15869/30660 2285 1431 ...
# 手动刷回脏数据
lvchange --cachesettings 'flush_on_suspend=1' storage/your-lv
# 清理缓存池(安全移除缓存)
lvconvert --uncache storage/your-lv
关键调优参数:
- migration_threshold:迁移阈值,控制冷数据从缓存中驱逐的判断标准
- sequential_threshold / random_threshold:随机/顺序 IO 阈值,超过该大小时绕过缓存直接访问后端,避免缓存污染
三、dm-writecache:专注写加速的轻量级缓存
3.1 设计哲学
dm-writecache 是 Linux 4.18 引入的另一个 Device Mapper target,它的设计哲学与 dm-cache 截然不同——只缓存写操作,所有读操作直接穿透到后端设备。
这种设计基于一个洞察:在很多负载下,写延迟比读延迟对用户体验影响更大。数据库事务提交必须等待持久化确认;日志写入一旦阻塞就会导致应用线程停顿。而读操作可以通过页面缓存(Page Cache)在内核缓冲区中熔化,大部分热数据读根本不会到达块层。
# dm-writecache 数据流
┌─────────────────────────────────────┐
│ 逻辑块设备 (/dev/mapper/y) │
├─────────────────────────────────────┤
│ 写请求 → [Write Cache 层 SSD] │
│ 读请求 → 穿透 → [后端设备 HDD] │
│ ↓ │
│ 异步刷回(flusher thread) │
└─────────────────────────────────────┘
3.2 内部结构
dm-writecache 将被缓存设备划分为若干个"段"(segment),每个段通常是 16KB 或 32KB(可配置)。写请求被写入同步 DRAM 缓冲区后立即确认,然后在内核后台刷盘线程(writecache_flush)中将数据写入 SSD 缓存层。
关键特性包括:
- 写合并:连续写入被合并为更大的 IO 操作,减少 SSD 写入放大
- 段清理(cleaner):当 SSD 缓存空间不足时,有效段被合并、重写
- 超级块(superblock):记录了一致性关键的状态信息,用于崩溃恢复
- 可选 passthrough 阈值:超过特定大小的顺序写直接穿透到后端
3.3 与 dm-cache 的性能对比
在实际混合场景下,两者各有所长:
# 测试环境:NVMe SSD (cache) + 7200RPM HDD (backend)
# 工作负载:TPCC-like 数据库混合读写
dm-cache(WB) dm-writecache 纯 HDD
随机写 4K IOPS: 85,000 92,000 180
随机读 4K IOPS: 78,000 180 180
顺序写 MB/s: 580 620 190
延迟 P99 (写): 0.1ms 0.08ms 8ms
延迟 P99 (读): 0.4ms 8ms 8ms
从数据可以看到 dm-writecache 在写延迟上甚至优于 dm-cache,但读性能完全依赖于后端 HDD。如果负载以随机读为主,dm-cache 是更好的选择;如果负载以写密集或数据库事务为主,dm-writecache 更胜一筹。
3.4 部署操作
# 加载模块
modprobe dm-writecache
# 获取后端设备的块数量
BLOCKS=$(blockdev --getsz /dev/sdb)
# 创建 writecache 设备
echo "0 $BLOCKS writecache s /dev/nvme0n1p1 /dev/sdb 2" | \
dmsetup create fast-storage
# 其中的 "2" 表示 internal metadata 模式(元数据在 SSD 上自动分配)
# 查看状态
dmsetup status fast-storage
# 设置缓存清理阈值
dmsetup message fast-storage 0 settings cleaner=1
四、BCACHE:块层内置的全功能缓存
4.1 设计理念
bcache 是 Linux 内核中一套完全不同的混合存储方案——它不是基于 Device Mapper,而是直接在块层(Block Layer)中实现,位于文件系统之下、实际存储设备之上。这种更底层的定位使得 bcache 可以做出更高效的缓存决策。
bcache 最早由 Kent Overstreet 开发,于 Linux 3.10 合入主线。它的核心创新在于使用 B+ 树作为索引结构,将 SSD 用作 HDD 的缓存层。
# bcache 架构层次
┌───────────┐
│ Filesystem│
├───────────┤
│ bcache │ ← Block layer cache (B+ tree 索引)
├─────┬─────┤
│ SSD │ HDD │ ← 底层物理设备
└─────┴─────┘
4.2 B+ 树索引与写旁路
bcache 使用一棵 B+ 树将所有缓存索引组织在 SSD 上。树的键是后端设备上的扇区号,值是该扇区对应的 SSD 缓存位置。这种结构带来几个优势:
- 范围查询:可以快速判断一段连续的扇区范围是否全部已被缓存
- 前缀压缩:相邻扇区的缓存键可以压缩存储
- 高效分配:SSD 空间分配自由度高,不会产生固定块带来的碎片
bcache 的关键性能特征是写旁路(write-around):对于顺序大 IO,bcache 会直接穿透 SSD 缓存,写入后端 HDD,避免缓存被冷顺序数据驱逐热随机数据。这个策略在生产环境中对数据库和虚拟化场景至关重要。
4.3 写策略
bcache 支持三种写模式:
- writethrough(默认):数据同时写入 SSD 和 HDD 才确认。最安全但性能最差
- writeback:先写入 SSD 确认,异步刷回 HDD。性能最高
- writearound:写穿透 HDD,SSD 仅作为读缓存
生产环境推荐使用 writeback 模式,但必须配合 UPS(不间断电源)或带有电容保护的 SSD——否则断电可能导致脏数据丢失。
4.4 部署与管理
# 加载模块
modprobe bcache
# 准备后端设备(数据盘)
make-bcache -B /dev/sdb
# 准备缓存设备(SSD)
make-bcache -C /dev/nvme0n1p1
# 将缓存设备 attach 到后端设备
echo "set-uuid" > /sys/fs/bcache/register
echo /dev/nvme0n1p1 > /sys/block/bcache0/bcache/attach
# 设置缓存模式
echo writeback > /sys/block/bcache0/bcache/cache_mode
# 查看状态
cat /sys/block/bcache0/bcache/stats_total/cache_hit_ratio
cat /sys/block/bcache0/bcache/dirty_data
# 调整顺序 IO 绕过阈值(单位:字节)
echo 4M > /sys/block/bcache0/bcache/sequential_cutoff
4.5 bcache 的高级特性
- Bypass IO:可配置顺序 IO 阈值,超过此大小的 IO 绕过缓存,直接操作后端设备
- Readahead:在设备注册时读取后端上的元数据进行预热
- Congestion control:在 SSD 缓存压力过大时延迟新的缓存写入
- Tiered caching:bcache 可与 dm-cache 或 dm-writecache 组合使用,实现多层缓存
五、三种方案的生产选择指南
| 维度 | dm-cache | dm-writecache | bcache |
|---|---|---|---|
| 缓存模式 | 读写 | 仅写 | 读写 |
| 元数据位置 | 独立 metadata 设备 | SSD 内或独立 | SSD B+树 |
| 缓存粒度 | 32KB-1GB 可调 | 16KB/32KB 段 | 扇区级 |
| 顺序 IO 处理 | 有 bypass 阈值 | 有 bypass 阈值 | 有 sequential_cutoff |
| 内存占用 | 低 | 最低 | 较高(B+树索引) |
| 元数据安全 | 高(独立 metadata) | 中 | 高(B+树持久化) |
| 复杂度 | 中 | 低 | 中高 |
| 适用场景 | 读写混合+大容量缓存 | 写密集/日志/数据库 | 通用+高稳定要求 |
| 移除缓存 | cleaner 模式自动刷回 | 手动 flush | 自动刷回 |
5.1 推荐场景
选择 dm-cache: - 虚拟化环境(KVM qcow2 镜像共享缓存) - 文件服务器杂合负载 - 希望与 LVM 生态深度集成(thin provisioning、snapshot 等)
选择 dm-writecache: - 数据库事务日志写入加速 - 日志收集系统 (Kafka、Elasticsearch) - NVMe SSD 昂贵、HDD 存储空间大,写性能是关键瓶颈
选择 bcache: - 企业级存储服务器 - 对缓存稳定性和安全性要求极高 - 使用非 LVM 存储栈的简单场景 - 需要精确的顺序 IO bypass 控制
六、性能调优与问题排查
6.1 常见性能瓶颈
缓存抖动(Thrash):缓存空间不足以容纳工作集,导致频繁的驱逐与重新加载。 - 解决:增大 SSD 缓存比例,或启用随机 IO 仅限策略
元数据瓶颈:dm-cache metadata 不足导致映射表无法完全缓存。 - 解决:使用更高性能的 metadata 设备,或减小缓存块大小
写高峰期间的脏数据堆积:writeback 模式下刷回速度跟不上写入速度。 - 解决:增加刷回线程数,降低刷回触发阈值
6.2 监控脚本示例
#!/bin/bash
# 混合存储缓存监控脚本
CACHE_DEV="storage-your--lv"
# 获取 dm-cache 状态
status=$(dmsetup status "$CACHE_DEV")
read blocks used metadata_blocks block_size \
reads hits reads_misses writes hits writes_misses \
policy feature_args <<< $(echo $status | awk '{print $3,$4,$5,$6,$8,$9,$10,$12,$14}')
# 计算命中率
if [ $((reads + reads_misses)) -gt 0 ]; then
read_hit_ratio=$(echo "scale=2; $reads_hits / ($reads_hits + $reads_misses) * 100" | bc)
echo "Read hit ratio: ${read_hit_ratio}%"
fi
# 脏块比例
dirty_pct=$(echo "scale=2; $metadata_blocks_dirty / $metadata_blocks_total * 100" | bc)
echo "Dirty blocks: ${dirty_pct}%"
# 告警阈值
if (( $(echo "$dirty_pct > 50" | bc -l) )); then
echo "WARNING: High dirty data ratio, consider increasing flush rate"
fi
if (( $(echo "$read_hit_ratio < 80" | bc -l) )); then
echo "WARNING: Low hit ratio, working set may exceed cache size"
fi
6.3 安全移除缓存设备
缓存设备的移除是高风险操作,一旦操作不当可能导致元数据损坏或数据丢失。不同方案的安全移除流程:
dm-cache 安全移除:
# 1. 切换到 cleaner 模式
lvchange --cachepolicy cleaner storage/your-lv
# 2. 等待所有脏块刷回
while [ $(lvs -o cache_dirty_blocks --noheadings storage/your-lv) -gt 0 ]; do
sleep 5
echo "Waiting dirty blocks to flush..."
done
# 3. 解除缓存
lvconvert --uncache storage/your-lv
bcache 安全移除:
# 1. 设置为 writethrough 模式,确保所有写入直达后端
echo writethrough > /sys/block/bcache0/bcache/cache_mode
# 2. 等待脏数据刷新
while [ -s /sys/block/bcache0/bcache/dirty_data ]; do
sleep 5
done
# 3. Detach 缓存设备
echo 1 > /sys/block/bcache0/bcache/detach
# 4. 停止 bcache
echo 1 > /sys/fs/bcache/<set-uuid>/stop
七、与文件系统的交互
混合存储缓存设备之上可以运行任何文件系统,但在选择文件系统时需要考虑与缓存层的交互。
ext4 + dm-cache:推荐,journal 写入可以被 writeback 缓存加速,且 ordered 数据模式安全可靠。
XFS + bcache:特别适合高性能计算场景。XFS 的 allocation group 和 bcache 的 B+树索引协同,使得大文件顺序 IO 自动 buckpass,小文件随机 IO 高效缓存。
Btrfs + dm-writecache:可以利用 Btrfs 写时复制特性与 dm-writecache 的写缓存结合实现快速快照。但需注意 Btrfs 的 COW 本身会带来额外 IO,与缓存叠加可能产生复杂的 IO 模式。
八、未来演进方向
Linux 混合存储缓存技术仍在持续演进。近期值得关注的方向包括:
-
dm-cache v2:正在开发的一版更新通过优化元数据结构和减少锁竞争来支持更大的缓存规模(PB 级)和更细粒度的缓存控制
-
NVMe ZNS + 缓存:Zoned Namespace SSD 的写入必须顺序进行,与缓存层的随机写入特性天然互补。新的缓存方案正在探索将 ZNS SSD 作为缓存层以降低成本
-
CXL Memory 作为缓存:CXL 2.0 引入的 Type 3 设备为主机提供了高带宽、低延迟的内存扩展能力。利用 CXL DRAM 作为闪存缓存的二级缓存,可以突破 NVMe SSD 的延迟瓶颈
-
智能缓存策略(ML 驱动):利用机器学习模型预测哪些数据将被访问,实现预取和精准的缓存放置,突破传统 LRU/LFU 策略的局限
总结
dm-cache、dm-writecache 和 bcache 代表了 Linux 生态中三种不同层次的混合存储缓存方案。dm-cache 提供了完整灵活的读写缓存能力,与 LVM 生态深度集成,适合通用场景;dm-writecache 是专门针对写加速的轻量级方案,在日志和数据库写入密集场景表现出色;bcache 则凭借稳定的 B+树索引和精确的 IO 控制能力,在关键业务存储中占据一席之地。
选择哪种方案取决于具体的负载特征、已有的存储栈技术选型、以及运维团队的熟悉程度。在实际部署前,建议使用 fio 模拟真实负载测试不同方案的性能表现和工作集命中率,再结合监控数据做出最终决策。

发表评论 取消回复