Linux 内核 bcache:用 SSD 加速慢速块设备的架构、实操与 sysfs 完整参考
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文基于 Linux 内核仓库中的 Documentation/admin-guide/bcache.rst 整理而成,系统讲解内核块层缓存子系统 bcache 的设计思想、设备创建与挂载流程、attach/detach 生命周期、错误处理机制,以及覆盖 backing device / cache set / cache device 三层的完整 sysfs 接口。读完后你将掌握从零部署 bcache 的全部命令、常见故障的排查路径,以及drivers/md/bcache源码中各关键参数(如sequential_cutoff、拥塞阈值、journal 延迟)的默认值与实现位置。
一、设计哲学:围绕 SSD 性能特征构建
设想你有一套又大又慢的 RAID 6 阵列,外加一两个(甚至三个)SSD——如果能把 SSD 用作缓存就好了,bcache 正是为此而生。
根据官方文档,其核心设计决策如下:
- 按擦除块(erase block)大小的 bucket 分配空间:bcache 只以 erase block 大小的桶为单位分配空间,并用混合 btree/日志(journal)来跟踪被缓存的 extent——extent 大小可以从单扇区一直大到整个桶。
- 不惜一切代价避免随机写:SSD 对随机 IO 擅长,但元数据层面的随机写会加速磨损,因此 bcache 的索引结构与 GC 策略都以顺序写为前提。
- 同时支持 writethrough 与 writeback 两种缓存模式:writeback 默认关闭,但可以在运行时任意开关。bcache 在数据保护上非常保守——它能可靠处理非正常关机,甚至没有“正常关机”的概念:写请求只有落盘到稳定存储后才会返回完成,脏数据始终有完整日志保护。
- writeback 模式下缓存几乎可以全部用于缓冲写:脏数据回写到 backing device 时永远按顺序进行,从索引头扫描到索引尾,因此不会把顺序流量压回慢速盘。
还有一个反直觉的优化:顺序大 IO 通常不值得缓存。bcache 会检测顺序 IO 并直接跳过;它还为每个 task 维护 IO 大小的滑动平均值,只要平均值超过阈值就跳过该 task 的全部 IO——而不是在每次 seek 之后仍去缓存最初 512k 的数据。因此备份和大批量文件拷贝会完全绕过缓存。
当闪存上发生数据 IO 错误时,bcache 会尝试从 backing device 读回数据或使相应缓存条目失效来恢复;对于不可恢复错误(元数据或脏数据损坏),缓存会被自动禁用——如果缓存里存在脏数据,会先关闭 writeback 并等待全部脏数据刷出。
二、快速上手:格式化、注册与使用
你需要 bcache-tools 仓库中的用户态工具。缓存设备和 backing 设备都必须先格式化后才能使用:
bcache make -B /dev/sdb bcache make -C /dev/sdcbcache make支持一次性格式化多个设备——把 backing 设备和缓存设备同时格式化,就无需手动 attach:
bcache make -B /dev/sda /dev/sdb -C /dev/sdc如果你的 bcache-tools 版本较旧、还没有统一的bcache命令,可以用遗留的make-bcache工具,-B/-C参数用法相同。
bcache-tools 现在随附 udev 规则,内核可以立即识别 bcache 设备。没有 udev 时可以手动注册:
echo /dev/sdb > /sys/fs/bcache/register echo /dev/sdc > /sys/fs/bcache/register注册 backing 设备后,bcache 设备就会出现在/dev下,你可以像普通块设备一样格式化并使用它。但首次使用一个新的 bcache 设备时,在 attach 到某个缓存之前它运行在 passthrough(直通)模式。文档建议:如果打算将来使用 bcache,可以先把所有慢速设备都格式化为 backing 设备(暂不接缓存),之后再按需添加缓存设备(见下文“Attaching”一节)。
设备呈现形式为:
/dev/bcache<N>在有 udev 的系统中还会出现符号链接:
/dev/bcache/by-uuid/<uuid> /dev/bcache/by-label/<label>开始使用:
mkfs.ext4 /dev/bcache0 mount /dev/bcache0 /mnt设备控制入口有两处:/sys/block/bcache<N>/bcache,以及/sys/fs/bcache/<cset-uuid>/。缓存设备按 cache set(缓存集)管理;目前一个 set 尚不支持多个缓存设备,但设计上为将来做元数据与脏数据的镜像留了余地。新建的 cache set 会出现在/sys/fs/bcache/<UUID>。
三、Attaching:把 backing 设备接入缓存集
缓存设备与 backing 设备都注册后,必须把 backing 设备 attach 到 cache set 才能启用缓存,写入 cache set 的 UUID(可在/sys/fs/bcache下找到):
echo <CSET-UUID> > /sys/block/bcache0/bcache/attach这一步只需要做一次。之后每次重启只需重新注册所有 bcache 设备即可。注意:如果 backing 设备上某处缓存里有数据,那么在缓存设备出现之前/dev/bcache<N>根本不会被创建——开 writeback 时这一点尤其重要。
如果开机时缓存设备丢失且永远不会回来,可以强制让 backing 设备独立运行:
echo 1 > /sys/block/sdb/bcache/running这里必须写/sys/block/sdb/bcache/running(或你的 backing 设备实际名字),而不是/sys/block/bcache0,因为此时 bcache0 尚不存在。若使用的是分区,bcache 目录位于/sys/block/sdb/sdb2/bcache。
backing 设备以后缓存重新出现时仍会使用该 cache set,但所有已缓存数据都会被作废。如果当时缓存里有脏数据,请做好文件系统大面积损坏的准备——文档直言 ext4 的 fsck 也只能算“创造奇迹”级别地抢救。
四、错误处理(Error Handling)
bcache 尝试透明地处理与缓存设备之间的 IO 错误,不影响正常运行;若错误过多(阈值可配置,默认 0,即一个错误都接受不了),就关闭缓存设备,并把所有 backing 设备切回 passthrough 模式。具体策略分四种情况:
| 场景 | 处理方式 |
|---|---|
| 从缓存读出错 | 直接从 backing 设备重试读取 |
| writethrough 写出错 | 切换为在缓存中作废该 LBA 处的数据(与绕过缓存的写等效) |
| writeback 写出错 | 目前把错误上抛给文件系统/用户态(文档指出可改进为重试为绕过缓存的写) |
| detach 时 | 先尝试刷出脏数据(writeback 模式下);若部分脏数据读回失败,目前没有智能处理 |
源码印证:在 drivers/md/bcache/sysfs.c 中,io_error_limit/io_error_halflife以读写属性暴露于 cache set 和 backing device 两层(如sysfs.c中的rw_attribute(io_error_limit)、rw_attribute(io_error_halflife)),与文档描述的“错误计数按半衰期衰减、达到 limit 即禁用缓存”的机制一一对应。
五、实操手册(Howto / Cookbook)
A. 缓存设备缺失时启动 bcache
如果注册 backing 设备后发现报device already registered:
host:~# echo /dev/sdb1 > /sys/fs/bcache/register [ 119.844831] bcache: register_bcache() error opening /dev/sdb1: device already registered说明它已在册。此时若缓存设备不在场或注册失败,可以强制无缓存运行:
host:/sys/block/sdb/sdb1/bcache# echo 1 > running注意:如果之前处于 writeback 模式,这可能导致数据丢失。
B. bcache 找不到自己的缓存
典型日志:
host:/sys/block/md5/bcache# echo 0226553a-37cf-41d5-b3ce-8b1e944543a8 > attach [ 1933.455082] bcache: bch_cached_dev_attach() Couldn't find uuid for md5 in set [ 1933.478179] bcache: __cached_dev_store() Can't attach 0226553a-37cf-41d5-b3ce-8b1e944543a8 [ 1933.478179] : cache set not found原因是缓存设备开机时没被注册,或掉线后重新插入。重新注册即可:
host:/sys/block/md5/bcache# echo /dev/sdh2 > /sys/fs/bcache/registerC. 损坏的 bcache 在注册时导致内核崩溃
这种情况不应该发生,一旦发生说明发现了 bug,请向 bcache 开发列表(linux-bcache@vger.kernel.org)报告,并尽量附上内核 dmesg 输出等尽可能多的信息。
D. 没有 bcache 时恢复数据
若内核中没有 bcache 支持,backing 设备上的文件系统仍可通过8KiB 偏移直接访问。用 loop 设备加--offset 8K挂载,或改用当初bcache make --data-offset指定的值:
losetup -o 8192 /dev/loop0 /dev/your_bcache_backing_dev这样/dev/loop0就呈现了未经修改的 backing 数据。如果缓存处于 writethrough 模式,你可以安全地丢弃缓存设备而不丢数据。
E. 擦除缓存设备
host:~# wipefs -a /dev/sdh2 16 bytes were erased at offset 0x1018 (bcache) they were: c6 85 73 f6 4e 1a 45 ca 82 65 f5 7f 48 ba 6d 81重启使 bcache 生效后,重建缓存并 attach:
host:~# bcache make -C /dev/sdh2 UUID: 7be7e175-8f4c-4f99-94b2-9c904d227045 Set UUID: 5bc072a8-ab17-446d-9744-e247949913c1 version: 0 nbuckets: 106874 block_size: 1 bucket_size: 1024 nr_in_set: 1 nr_this_dev: 0 first_bucket: 1 [ 650.511912] bcache: run_cache_set() invalidating existing data [ 650.549228] bcache: register_cache() registered cache device sdh2随后启动缺失缓存的 backing 设备并 attach 新缓存:
host:/sys/block/md5/bcache# echo 1 > running host:/sys/block/md5/bcache# echo 5bc072a8-ab17-446d-9744-e247949913c1 > attach [ 865.276616] bcache: bch_cached_dev_attach() Caching md5 as bcache0 on set 5bc072a8-ab17-446d-9744-e247949913c1F. 移除或更换缓存设备
host:/sys/block/sda/sda7/bcache# echo 1 > detach [ 695.872542] bcache: cached_dev_detach_finish() Caching disabled for sda7此时直接 wipefs 会失败:
host:~# wipefs -a /dev/nvme0n1p4 wipefs: error: /dev/nvme0n1p4: probing initialization failed: Device or resource busy因为设备只是被禁用、尚未注销,仍受保护。先查缓存 set 中对应的符号链接确认路径,然后stop:
host:/sys/fs/bcache/b7ba27a1-2398-4649-8ae3-0959f57ba128# ls -l cache0 lrwxrwxrwx 1 root root 0 Feb 25 18:33 cache0 -> ../../../devices/pci0000:00/0000:00:1d.0/0000:70:00.0/nvme/nvme0/nvme0n1/nvme0n1p4/bcache/ host:/sys/fs/bcache/b7ba27a1-2398-4649-8ae3-0959f57ba128# echo 1 > stop kernel: [ 917.041908] bcache: cache_set_free() Cache set b7ba27a1-2398-4649-8ae3-0959f57ba128 unregistered之后即可安全擦除:
host:~# wipefs -a /dev/nvme0n1p4 /dev/nvme0n1p4: 16 bytes were erased at offset 0x00001018 (bcache): c6 85 73 f6 4e 1a 45 ca 82 65 f5 7f 48 ba 6d 81G. dm-crypt 与 bcache 的组合
先以非加密方式建好 bcache,再在/dev/bcache<N>之上安装 dm-crypt。这比“先对 backing 和缓存设备分别加密、再叠加 bcache”更快(文档标注了此处可补充基准测试)。
H. 释放/停止已注册的 bcache 以便重新格式化
当你需要 fdisk 重建分区表并重新注册时,设备上不能残留任何活跃的 backing 或 caching 设备。
- 若
/dev/bcache*存在,直接:
host:/sys/block/bcache0/bcache# echo 1 > stop- 若 backing 设备已消失,
/sys/block/bcache0/bcache不存在(cd bcache报 No such file or directory),可能需要移除引用该 bcache 的 dmcrypt 上层设备来释放它:
host:~# dmsetup remove oldds1 bcache: bcache_device_free() bcache0 stopped bcache: cache_set_free() Cache set 5bc072a8-ab17-446d-9744-e247949913c1 unregistered这会令 backing bcache 从/sys/fs/bcache移除、可以复用。对任何以 bcache 为底层设备的块设备堆叠结构都适用。
- 其他情况可查
/sys/fs/bcache/:
host:/sys/fs/bcache# ls -l */{cache?,bdev?} lrwxrwxrwx 1 root root 0 Mar 5 09:39 0226553a-37cf-41d5-b3ce-8b1e944543a8/bdev1 -> ../../../devices/virtual/block/dm-1/bcache/ lrwxrwxrwx 1 root root 0 Mar 5 09:39 0226553a-37cf-41d5-b3ce-8b1e944543a8/cache0 -> ../../../devices/virtual/block/dm-4/bcache/ lrwxrwxrwx 1 root root 0 Mar 5 09:39 5bc072a8-ab17-446d-9744-e247949913c1/cache0 -> ../../../devices/pci0000:00/0000:00:01.0/0000:01:00.0/ata10/host9/target9:0:0/9:0:0:0/block/sdl/sdl2/bcache/根据符号链接确定相关 UUID,进入对应目录后echo 1 > stop,即可释放 bcache 引用、把分区用于其他目的。
六、性能调优与故障排查
bcache 的配置项和可调参数很多。默认值面向典型桌面/服务器负载是合理的,但不是跑分最优值。
1. Backing 设备对齐
bcache 默认元数据区大小为 8k。若 backing 设备是 RAID,务必用bcache make --data-offset按条带宽度(stride width)的倍数对齐。若计划未来扩容磁盘阵列,可用一串质数乘以 RAID 条带大小来挑选磁盘倍数。例如条带为 64k 时:
64k * 2*2*2*3*3*5*7 bytes = 161280k只浪费约 157.5MB,就能让 RAID 5 卷扩展到以下数据盘数量而无需重新对齐:3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 15, 18, 20, 21 …
2. 写性能不及预期
多半是你本想跑 writeback 模式,而它并非默认(不是成熟度问题,而是因为 writeback 模式下 SSD 一旦出事会丢数据):
# echo writeback > /sys/block/bcache0/bcache/cache_mode3. 流量没有按预期走向 SSD
默认情况下 bcache 不缓存一切——它会跳过顺序 IO,因为真正值得缓存的是随机 IO;拷贝 10GB 大文件时,你也不希望那 10GB 把真正高频随机访问的数据挤出缓存。若要压测“从缓存读”,且用 fio 先写 8GB 测试文件,需要关掉顺序跳过:
# echo 0 > /sys/block/bcache0/bcache/sequential_cutoff恢复默认值(4MB):
# echo 4M > /sys/block/bcache0/bcache/sequential_cutoff源码印证:默认值 4MB 在 drivers/md/bcache/super.c 中初始化为dc->sequential_cutoff = 4 << 20;判定逻辑位于 drivers/md/bcache/request.c,当某 task 近期 IO 的平均扇区数达到sequential_cutoff且未被拥塞控制拦截时,该 IO 绕过缓存。
4. 流量仍落在机械盘 / 仍有 cache miss
现实中 SSD 未必总是快过磁盘——低速 SSD、一颗 SSD 挂多块盘、或以顺序 IO 为主时尤其如此。要避免 SSD 成为全局瓶颈,bcache 会跟踪到缓存设备的时延,一旦超过阈值就逐步节流流量(手段是调低顺序旁路)。需要时可以置 0 禁用:
# echo 0 > /sys/fs/bcache/<cache set>/congested_read_threshold_us # echo 0 > /sys/fs/bcache/<cache set>/congested_write_threshold_us默认读阈值 2000us(2 毫秒)、写阈值 20000us。源码印证:drivers/md/bcache/super.c 中c->congested_read_threshold_us = 2000、c->congested_write_threshold_us = 20000;时延判定在 drivers/md/bcache/io.c 按读写分别选用对应阈值;拥塞控制入口见 drivers/md/bcache/request.c(两个阈值同时为 0 时直接跳过拥塞控制)。
5. 同一数据反复 cache miss
一个曾经困扰人的老问题源于 cache miss 的缓存一致性处理方式:若 btree 节点已满,miss 读入的新数据无法插入索引键,也就不会写入缓存。实际上几乎无感——任何一次写都会触发节点分裂,而 bcache 的 btree 节点很大、索引覆盖整块设备的大区域,极少量写流量就足以消除该现象。但纯读压测“预热缓存”且没有其他流量时就会踩坑。解决办法:用写来预热,或使用 testing 分支(其中包含修复)。
七、sysfs 接口完整参考
7.1 Backing 设备(/sys/block/ /bcache,亦见 /sys/block/bcache*/bcache 及(已 attach 时)/sys/fs/bcache/ /bdev*)
| 文件 | 说明 |
|---|---|
attach | 写入一个 cache set 的 UUID 以启用缓存 |
cache_mode | 取值writethrough、writeback、writearound或none |
clear_stats | 写入即重置运行累计统计(不重置 day/hour/5min 衰减版本) |
detach | 写入即从 cache set 分离;若缓存中有脏数据会先刷出 |
dirty_data | 该 backing 设备在缓存中的脏数据量。比 cache set 层更新更及时,但可能略有偏差 |
label | 底层设备名 |
readahead | 预读大小,默认 0。设为如 1M 时,cache miss 的读会被向上取整到该大小,但不会与已有缓存条目重叠 |
running | bcache 是否在运行(即 /dev/bcache 设备是否存在,无论是 passthrough 还是缓存模式) |
sequential_cutoff | 顺序 IO 超过该阈值后绕过缓存;跟踪最近 128 个 IO,因此顺序 IO 即使不连续发出也能被识别 |
sequential_merge | 非零时,bcache 保留最近 128 个已提交请求的列表,与新请求比对以判断其是否为之前请求的顺序延续,用于判定 sequential cutoff。当 sequential cutoff 大于任何单个请求可接受的最大顺序大小时此项是必需的 |
state | 四态之一:no cache(从未 attach 过缓存集);clean(属于缓存集且无脏数据);dirty(属于缓存集且有脏数据);inconsistent(用户强制运行了存在缓存脏数据但缓存集不可用的 backing 设备,其上的数据很可能已损坏) |
stop | 写入即关停 bcache 设备并关闭 backing 设备 |
writeback_delay | 脏数据首次写入缓存(此前无脏数据)后,等待若干秒才开始回写,默认 30 |
writeback_percent | 非零时,bcache 通过节流后台回写并配合 PD 控制器平滑调速,让缓存脏数据占比维持在该百分比附近 |
writeback_rate | 单位扇区/秒的回写速率——writeback_percent 非零时后台回写被节流到此速率。bcache 会持续调整,用户也可手动设置 |
writeback_running | 关闭时脏数据完全不回写,仍会持续写入缓存直到接近写满;仅用于压测。默认开启 |
7.2 Backing 设备统计
每个统计项都有“运行累计”版本,以及过去一天、一小时、五分钟三个衰减版本,同时也在 cache set 目录中做聚合。
| 文件 | 说明 |
|---|---|
bypassed | 绕过缓存的 IO 量(读+写) |
cache_hits/cache_misses/cache_hit_ratio | 按 bcache 看到的每个 IO 单独计数;部分命中按 miss 计 |
cache_bypass_hits/cache_bypass_misses | 意图绕过缓存的 IO 的命中/未中仍会计入,但在此单独列出 |
cache_miss_collisions | 统计从 miss 向缓存插入数据时与写竞争、发现数据已存在的次数(自 miss 同步逻辑重写以来通常恒为 0) |
7.3 Cache set(/sys/fs/bcache/ )
| 文件 | 说明 |
|---|---|
average_key_size | btree 中每个 key 对应的平均数据量 |
bdev<0..n> | 指向每个已 attach backing 设备的符号链接 |
block_size | 缓存设备的块大小 |
btree_cache_size | btree 缓存当前占用的内存量 |
bucket_size | 桶大小 |
cache<0..n> | 指向组成该缓存集的每个缓存设备的符号链接 |
cache_available_percent | 缓存设备中不含脏数据、理论上可用于 writeback 的百分比。这不意味着该空间没有存放干净缓存数据;priority_stats中真正的 unused 统计通常低得多 |
clear_stats | 清除该缓存关联的统计 |
dirty_data | 缓存中的脏数据量(在 GC 运行时更新) |
flash_vol_create | 写入人类可读大小(k/M/G)即创建一个由该 cache set 支撑的薄供给卷 |
io_error_halflife/io_error_limit | 决定禁用缓存前容忍多少错误。每个错误按半衰期(以 IO 次数计)衰减;衰减计数达到io_error_limit时,脏数据被写出、缓存被禁用 |
journal_delay_ms | journal 写最多延迟该毫秒数,除非 sooner 发生 cache flush。默认 100 |
root_usage_percent | btree 根节点使用率;过高时节点分裂、树深增加 |
stop | 写入即关停 cache set——等待所有已 attach 的 backing 设备关停 |
tree_depth | btree 深度(单节点 btree 深度为 0) |
unregister | 分离所有 backing 设备并关闭缓存设备;若有脏数据会先禁用 writeback 并等待其刷出 |
源码印证:journal_delay_ms的默认值 100 在 drivers/md/bcache/journal.c 中初始化,其延迟窗口在 drivers/md/bcache/journal.c 处以msecs_to_jiffies(c->journal_delay_ms)参与 journal 合并判定。
7.4 Cache set 内部状态
该目录还暴露若干内部操作的耗时统计,GC、btree 读、btree 节点排序、btree 分裂各自有平均耗时、平均频率、最近一次、最大耗时四个独立文件。
| 文件 | 说明 |
|---|---|
active_journal_entries | 比索引更新鲜的 journal 条目数 |
btree_nodes | btree 节点总数 |
btree_used_percent | btree 平均使用比例 |
bset_tree_stats | 辅助搜索树的统计 |
btree_cache_max_chain | btree 节点缓存哈希表中最长链长度 |
cache_read_races | 统计从缓存读数据期间桶被复用失效——即读完成后指针已过期——的次数;发生时数据从 backing 设备重读 |
trigger_gc | 写入即强制触发 GC |
7.5 缓存设备(/sys/block/ /bcache)
| 文件 | 说明 |
|---|---|
block_size | 写的最小粒度,应与硬件扇区大小一致 |
btree_written | 全部 btree 写量之和(k/M/G 字节) |
bucket_size | 桶大小 |
cache_replacement_policy | 取值lru、fifo或random |
freelist_percent | freelist 相对 nbuckets 的百分比。可写入以增大保留在 freelist 上的桶数,从而在运行时人为缩小缓存可用空间,主要用于测试(如试验不同缓存大小对命中率的影响) |
io_errors | 已发生错误数,按io_error_halflife衰减 |
metadata_written | 所有非数据写(btree 写及其他元数据)之和 |
nbuckets | 该缓存的桶总数 |
priority_stats | 缓存中数据最近被访问情况的统计,可揭示工作集大小。Unused是不含任何数据的缓存百分比,Metadata是 bcache 元数据开销,Average是桶的平均优先级,其后是按优先级阈值排列的分位数列表 |
written | 写入缓存的数据总量;与btree_written对比即可看出 bcache 的写放大 |
八、文档与源码对照速查
bcache 的完整内核实现位于 drivers/md/bcache/,目录清单 中的文件与本文各主题对应关系:
| 文档主题 | 主要源码 |
|---|---|
| 设备格式化、超级块、默认参数 | drivers/md/bcache/super.c、drivers/md/bcache/bcache_ondisk.h |
| IO 路径、顺序检测与 bypass | drivers/md/bcache/request.c、drivers/md/bcache/io.c |
| 写放大与元数据跟踪 | drivers/md/bcache/btree.c、drivers/md/bcache/bset.c |
| writeback 机制 | drivers/md/bcache/writeback.c |
| journal 脏数据保护 | drivers/md/bcache/journal.c |
| 本文所有 sysfs 属性的注册与读写实现 | drivers/md/bcache/sysfs.c |
| 统计计数(hits/misses/bypassed 等) | drivers/md/bcache/stats.c |
需要强调的是本文的前提限制:上述内容以当前 Linux 内核仓库中的文档与drivers/md/bcache实现为准。多缓存设备镜像、writeback 写错误重试等能力在文档中明确标注为“尚未支持/待改进”,请勿当作现有能力使用;所有bcache make等用户态命令依赖发行版提供的 bcache-tools 包,其具体行为以所用版本为准。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考