块层缓存 (bcache)

假设你有一个大型的、速度较慢的 raid 6,以及一两个 ssd。如果能将它们用作缓存不是很好吗?因此有了 bcache。

bcache wiki 可以在以下网址找到

https://bcache.evilpiepirate.org

这是 bcache-tools 的 git 仓库

https://git.kernel.org/pub/scm/linux/kernel/git/colyli/bcache-tools.git/

最新的 bcache 内核代码可以在主线 Linux 内核中找到

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/

它的设计围绕着 SSD 的性能特点 - 它只以擦除块大小的桶进行分配,并且它使用混合的 btree/日志来跟踪缓存的范围(可以是从单个扇区到桶大小的任何地方)。它的设计目的是不惜一切代价避免随机写入;它按顺序填充一个擦除块,然后在重用它之前发出丢弃命令。

支持直写和回写缓存。回写默认为关闭,但可以在运行时任意打开和关闭。Bcache 竭尽全力保护你的数据 - 它可靠地处理不干净的关机。(它甚至没有干净关机的概念;bcache 只是在写入稳定存储之前不返回已完成的写入)。

回写缓存可以使用大部分缓存来缓冲写入 - 将脏数据写入后备设备始终是按顺序完成的,从索引的开始到结束进行扫描。

由于随机 IO 是 SSD 的强项,因此缓存大型顺序 IO 通常不会有太多好处。Bcache 检测顺序 IO 并跳过它;它还保留每个任务的 IO 大小的滚动平均值,只要平均值高于截止值,它就会跳过来自该任务的所有 IO - 而不是在每次寻道后缓存前 512k。因此,备份和大文件复制应完全绕过缓存。

如果闪存上发生数据 IO 错误,它将尝试通过从磁盘读取或使缓存条目失效来恢复。对于不可恢复的错误(元数据或脏数据),缓存会自动禁用;如果缓存中存在脏数据,它会首先禁用回写缓存,并等待所有脏数据刷新。

入门:你需要 bcache 工具,来自 bcache-tools 仓库。缓存设备和后备设备都必须在使用前格式化

bcache make -B /dev/sdb
bcache make -C /dev/sdc

bcache make 能够同时格式化多个设备 - 如果你同时格式化你的后备设备和缓存设备,你就不必手动附加

bcache make -B /dev/sda /dev/sdb -C /dev/sdc

如果你的 bcache-tools 未更新到最新版本,并且没有统一的 bcache 工具,你可以使用旧的 make-bcache 工具来使用相同的 -B 和 -C 参数格式化 bcache 设备。

bcache-tools 现在附带 udev 规则,并且内核立即知道 bcache 设备。如果没有 udev,你可以像这样手动注册设备

echo /dev/sdb > /sys/fs/bcache/register
echo /dev/sdc > /sys/fs/bcache/register

注册后备设备会使 bcache 设备显示在 /dev 中;你现在可以格式化并像往常一样使用它。但是,第一次使用新的 bcache 设备时,它将以直通模式运行,直到你将其附加到缓存。如果你考虑稍后使用 bcache,建议将所有速度较慢的设备设置为没有缓存的 bcache 后备设备,并且你可以选择稍后添加缓存设备。请参阅下面的 “附加” 部分。

这些设备显示为

/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 中的 sysfs 控制 bcache 设备。你还可以通过 /sys/fs//bcache/<cset-uuid>/ 控制它们。

缓存设备作为集合进行管理;每个集合不支持多个缓存,但将来可以镜像元数据和脏数据。你的新缓存集显示为 /sys/fs/bcache/<UUID>

附加

在注册缓存设备和后备设备后,后备设备必须附加到你的缓存集才能启用缓存。将后备设备附加到缓存集的方式如下,使用 /sys/fs/bcache 中的缓存集 UUID

echo <CSET-UUID> > /sys/block/bcache0/bcache/attach

这只需要做一次。下次你重新启动时,只需重新注册你所有的 bcache 设备。如果后备设备在某个地方的缓存中有数据,则在缓存出现之前不会创建 /dev/bcache<N> 设备 - 如果你启用了回写缓存,这一点尤为重要。

如果你启动时缓存设备消失了并且永远不会回来,你可以强制运行后备设备

echo 1 > /sys/block/sdb/bcache/running

(你需要使用 /sys/block/sdb(或你的后备设备称为的任何名称),而不是 /sys/block/bcache0,因为 bcache0 尚不存在。如果你使用的是分区,则 bcache 目录将在 /sys/block/sdb/sdb2/bcache 中)

如果后备设备将来出现,它仍将使用该缓存集,但所有缓存的数据都将失效。如果缓存中有脏数据,则不要期望文件系统是可恢复的 - 你将会有大量的文件系统损坏,尽管 ext4 的 fsck 确实创造了奇迹。

错误处理

Bcache 尝试透明地处理来自/到缓存设备的 IO 错误,而不会影响正常操作;如果它看到太多错误(阈值是可配置的,默认为 0),它会关闭缓存设备并将所有后备设备切换到直通模式。

  • 对于从缓存读取,如果它们出错,我们只需从后备设备重试读取。

  • 对于直写写入,如果写入缓存出错,我们只需切换到使缓存中该 lba 处的数据无效(即我们对绕过缓存的写入执行的操作相同)

  • 对于回写写入,我们目前将该错误传递回文件系统/用户空间。这可以改进 - 我们可以将其作为跳过缓存的写入重试,这样我们就无需使写入出错。

  • 当我们分离时,我们首先尝试刷新任何脏数据(如果我们以回写模式运行)。但是,如果它无法读取某些脏数据,它目前不会执行任何智能操作。

操作指南/菜谱

  1. 使用缺失的缓存设备启动 bcache

如果注册后备设备没有帮助,那说明它已经存在,你只需要强制它在没有缓存的情况下运行。

host:~# echo /dev/sdb1 > /sys/fs/bcache/register
[  119.844831] bcache: register_bcache() error opening /dev/sdb1: device already registered

接下来,尝试注册你的缓存设备(如果存在)。但是,如果它不存在,或者由于某些原因注册失败,你仍然可以在没有缓存的情况下启动你的 bcache,像这样。

host:/sys/block/sdb/sdb1/bcache# echo 1 > running

请注意,如果你在写回模式下运行,这可能会导致数据丢失。

  1. 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/register
  1. 损坏的 bcache 在设备注册时导致内核崩溃

这不应该发生。如果确实发生了,那么你发现了一个错误!请将其报告给 bcache 开发邮件列表:linux-bcache@vger.kernel.org

请务必提供尽可能多的信息,包括内核 dmesg 输出(如果可用),以便我们提供帮助。

  1. 在没有 bcache 的情况下恢复数据

如果内核中没有 bcache,后备设备上的文件系统仍然可以通过 8KiB 的偏移量访问。因此,可以通过使用 `--offset 8K` 创建的后备设备的 loopdev,或者使用 `bcache make` 格式化 bcache 时由 `--data-offset` 定义的任何值进行访问。

例如

losetup -o 8192 /dev/loop0 /dev/your_bcache_backing_dev

这应该在 /dev/loop0 中显示你未修改的后备设备数据。

如果你的缓存处于直写模式,那么你可以安全地丢弃缓存设备而不会丢失数据。

  1. 擦除缓存设备

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 的情况下重新启动后,你重新创建缓存并将其附加。

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

启动缺少缓存的后备设备

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-e247949913c1
  1. 移除或更换缓存设备

    host:/sys/block/sda/sda7/bcache# echo 1 > detach
    [  695.872542] bcache: cached_dev_detach_finish() Caching disabled for sda7
    
    host:~# wipefs -a /dev/nvme0n1p4
    wipefs: error: /dev/nvme0n1p4: probing initialization failed: Device or resource busy
    Ooops, it's disabled, but not unregistered, so it's still protected
    

我们需要取消注册它。

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 81
  1. dm-crypt 和 bcache

首先设置未加密的 bcache,然后在 /dev/bcache<N> 之上安装 dmcrypt。这比你同时对后备设备和缓存设备进行 dmcrypt 加密,然后再在其之上安装 bcache 的速度更快。[基准测试?]

  1. 停止/释放已注册的 bcache 以擦除和/或重新创建它

假设你需要释放所有 bcache 引用,以便你可以运行 fdisk 并重新注册更改后的分区表,如果其上还有任何活动的后备设备或缓存设备,则此操作将不起作用。

  1. 它是否存在于 /dev/bcache* 中?(有时它不会存在)

    如果是,那就很简单。

    host:/sys/block/bcache0/bcache# echo 1 > stop
    
  2. 但是,如果你的后备设备消失了,这将不起作用。

    host:/sys/block/bcache0# cd bcache
    bash: 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
    

    这会导致后备 bcache 从 /sys/fs/bcache 中删除,然后可以重复使用。对于 bcache 是较低设备的任何块设备堆叠,都是如此。

  3. 在其他情况下,你也可以在 /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 是相关的,进入该目录并停止缓存。

    host:/sys/fs/bcache/5bc072a8-ab17-446d-9744-e247949913c1# echo 1 > stop
    

    这将释放 bcache 引用,并允许你将分区重复用于其他目的。

性能故障排除

Bcache 有很多配置选项和可调参数。默认值旨在对典型的桌面和服务器工作负载是合理的,但它们不是你在进行基准测试时获得最佳可能结果所需的值。

  • 后备设备对齐

    bcache 中的默认元数据大小为 8k。如果你的后备设备是基于 RAID 的,请确保使用 `bcache make --data-offset` 将其对齐为条带宽度的倍数。如果你打算将来扩展磁盘阵列,请将一系列素数乘以你的 RAID 条带大小,以获得你想要的磁盘倍数。

    例如:如果你有一个 64k 条带大小,则以下偏移量将为许多常见的 RAID5 数据盘片计数提供对齐。

    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 ...
    
  • 写入性能不佳

    如果写入性能不符合你的预期,你可能希望以回写模式运行,这不是默认模式(不是因为缺乏成熟度,而是因为在回写模式下,如果你的 SSD 发生问题,你会丢失数据)。

    # echo writeback > /sys/block/bcache0/bcache/cache_mode
    
  • 性能不佳,或流量未按预期流向 SSD

    默认情况下,bcache 不会缓存所有内容。它会尝试跳过顺序 IO - 因为你真正想缓存的是随机 IO,并且如果你复制一个 10 GB 的文件,你可能不希望它将 10 GB 的随机访问数据推出你的缓存。

    但是,如果你想对缓存读取进行基准测试,并且你从 fio 写入一个 8 GB 的测试文件开始 - 所以你想禁用它。

    # echo 0 > /sys/block/bcache0/bcache/sequential_cutoff
    

    要将其设置回默认值 (4 MB),请执行以下操作

    # echo 4M > /sys/block/bcache0/bcache/sequential_cutoff
    
  • 流量仍然流向磁盘/仍然出现缓存未命中

    在现实世界中,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
    

    读取的默认值为 2000 微秒(2 毫秒),写入的默认值为 20000 微秒。

  • 仍然出现缓存未命中,对于相同的数据

    有时会使人们绊倒的最后一个问题实际上是一个旧错误,这是由于缓存未命中时缓存一致性的处理方式造成的。如果 btree 节点已满,缓存未命中将无法插入新数据的键,并且数据将不会写入缓存。

    实际上,这不是一个问题,因为一旦出现写入操作,就会导致 btree 节点被拆分,并且你几乎不需要写入流量来使其不显示得足够明显(特别是由于 bcache 的 btree 节点很大并且索引了设备的较大区域)。但是,当你进行基准测试时,如果你尝试通过读取大量数据来预热缓存,并且没有其他流量 - 这可能是一个问题。

    解决方案:通过执行写入来预热缓存,或使用测试分支(其中有一个针对此问题的修复程序)。

Sysfs - 后备设备

可在 /sys/block/<bdev>/bcache、/sys/block/bcache*/bcache 和(如果已附加)/sys/fs/bcache/<cset-uuid>/bdev* 中找到

attach

将缓存集的 UUID 回显到此文件以启用缓存。

cache_mode

可以是直写、回写、绕过或无中的一个。

clear_stats

写入此文件将重置运行总计统计信息(而不是每天/每小时/每 5 分钟衰减的版本)。

detach

写入此文件以从缓存集中分离。如果缓存中有脏数据,它将首先被刷新。

dirty_data

此后备设备在缓存中的脏数据量。与缓存集的版本不同,会持续更新,但可能会略有偏差。

label

底层设备的名称。

readahead

应执行的预读取大小。默认为 0。如果设置为例如 1M,它会将缓存未命中的读取向上舍入到该大小,但不会与现有的缓存条目重叠。

running

如果 bcache 正在运行(即 /dev/bcache 设备是否存在,它是否处于直通模式或缓存),则为 1。

sequential_cutoff

一旦通过此阈值,顺序 IO 将绕过缓存;跟踪最近的 128 个 IO,以便即使不是一次完成也可以检测到顺序 IO。

sequential_merge

如果非零,bcache 会保留提交的最后 128 个请求的列表,以便与所有新请求进行比较,以确定哪些新请求是先前请求的顺序延续,以便确定顺序截止。如果顺序截止值大于任何单个请求的最大可接受顺序大小,则这是必要的。

state

后备设备可以处于四种不同的状态之一

无缓存:从未附加到缓存集。

干净:缓存集的一部分,并且没有缓存的脏数据。

脏:缓存集的一部分,并且有缓存的脏数据。

不一致:当有脏数据被缓存但缓存集不可用时,后备设备被用户强制运行;后备设备上的任何数据都可能已损坏。

stop

写入此文件以关闭 bcache 设备并关闭后备设备。

writeback_delay

当脏数据写入缓存并且先前不包含任何数据时,在启动回写之前等待若干秒。默认为 30。

writeback_percent

如果非零,bcache 会尝试通过限制后台回写并使用 PD 控制器平稳地调整速率来保持大约此百分比的缓存为脏。

writeback_rate

以每秒扇区数为单位的速率 - 如果 writeback_percent 非零,则后台回写将限制为此速率。由 bcache 持续调整,但也可以由用户设置。

writeback_running

如果关闭,则完全不会发生脏数据的回写。脏数据仍将添加到缓存中,直到它几乎填满;仅用于基准测试。默认为开启。

Sysfs - 后备设备统计信息

运行总计的数字目录,以及过去一天、一小时和 5 分钟衰减的版本;它们也汇总在缓存集目录中。

bypassed

已绕过缓存的 IO 量(包括读取和写入)

cache_hits, cache_misses, cache_hit_ratio

每次 IO 在 bcache 中被视为单独的 IO 时都会计数命中和未命中;部分命中被视为未命中。

cache_bypass_hits, cache_bypass_misses

旨在跳过缓存的 IO 的命中和未命中仍然被计数,但在此处被分解出来。

cache_miss_collisions

计算了数据将要从缓存未命中插入到缓存中的实例数,但与写入竞争并且数据已经存在(通常为 0,因为缓存未命中的同步已被重写)

Sysfs - 缓存集

可在 /sys/fs/bcache/<cset-uuid> 中找到

average_key_size

btree 中每个键的平均数据量。

bdev<0..n>

每个附加后备设备的符号链接。

block_size

缓存设备的块大小。

btree_cache_size

btree 缓存当前使用的内存量

bucket_size

存储桶的大小

cache<0..n>

指向构成此缓存集的每个缓存设备的符号链接。

cache_available_percent

缓存设备中不包含脏数据的百分比,有可能用于回写。这并不意味着此空间不用于存储干净的缓存数据;未使用的统计数据(在 priority_stats 中)通常要低得多。

clear_stats

清除与此缓存关联的统计信息

dirty_data

缓存中脏数据的数量(在垃圾回收运行时更新)。

flash_vol_create

向此文件回显大小(以人类可读的单位,k/M/G)会创建一个由缓存集支持的精简配置卷。

io_error_halflife, io_error_limit

这些确定我们在禁用缓存之前接受多少错误。每个错误都会按半衰期(以 # ios 为单位)衰减。如果衰减计数达到 io_error_limit,则会写出脏数据并禁用缓存。

journal_delay_ms

日志写入将最多延迟这么多毫秒,除非较早发生缓存刷新。默认为 100。

root_usage_percent

根 btree 节点的使用百分比。如果此值过高,则节点将拆分,从而增加树的深度。

stop

写入此文件以关闭缓存集 - 等待直到所有连接的后备设备都已关闭。

tree_depth

btree 的深度(单个节点 btree 的深度为 0)。

unregister

分离所有后备设备并关闭缓存设备;如果存在脏数据,则会禁用回写缓存并等待其刷新。

Sysfs - 缓存集内部

此目录还公开了许多内部操作的计时,并为平均持续时间、平均频率、上次发生时间和最大持续时间提供了单独的文件:垃圾回收、btree 读取、btree 节点排序和 btree 分裂。

active_journal_entries

比索引更新的日志条目的数量。

btree_nodes

btree 中的总节点数。

btree_used_percent

btree 的平均使用比例。

bset_tree_stats

有关辅助搜索树的统计信息

btree_cache_max_chain

btree 节点缓存的哈希表中最长的链

cache_read_races

计算从缓存读取数据时,存储桶被重用和失效的次数 - 即读取完成后指针过时的情况。发生这种情况时,将从后备设备重新读取数据。

trigger_gc

写入此文件会强制运行垃圾回收。

Sysfs - 缓存设备

在 /sys/block/<cdev>/bcache 中可用

block_size

最小写入粒度 - 应与硬件扇区大小匹配。

btree_written

所有 btree 写入的总和,单位为(千/兆/吉)字节

bucket_size

存储桶的大小

cache_replacement_policy

lru、fifo 或 random 之一。

discard

布尔值;如果启用,则在重用每个存储桶之前,将向其发出 discard/TRIM 命令。默认为关闭,因为 SATA TRIM 是一个非排队命令(因此速度较慢)。

freelist_percent

空闲列表的大小占 nbuckets 的百分比。可以写入此值以增加空闲列表上保留的存储桶数量,这使您可以在运行时人为地减小缓存的大小。主要用于测试目的(即测试不同大小的缓存如何影响您的命中率),但由于存储桶在移动到空闲列表时会被丢弃,因此通过有效地为其提供更多保留空间,它也会使 SSD 的垃圾回收更容易。

io_errors

发生的错误数,按 io_error_halflife 衰减。

metadata_written

所有非数据写入的总和(btree 写入和所有其他元数据)。

nbuckets

此缓存中的总存储桶数

priority_stats

有关缓存中数据最近访问时间的统计信息。这可以揭示您的工作集大小。Unused 是不包含任何数据的缓存百分比。Metadata 是 bcache 的元数据开销。Average 是缓存存储桶的平均优先级。Next 是具有每个优先级阈值的分位数列表。

written

已写入缓存的所有数据的总和;与 btree_written 的比较得出 bcache 中写入放大的量。