块层缓存 (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 的性能特征而设计 - 它只分配擦除块大小的 bucket,并且它使用混合 btree/日志来跟踪缓存的范围(可以是单个扇区到 bucket 大小的任何位置)。 它的设计目标是不惜一切代价避免随机写入; 它会按顺序填充一个擦除块,然后在重复使用之前发出丢弃命令。

支持直写和回写缓存。 回写默认为关闭,但可以在运行时任意打开和关闭。 Bcache 竭尽全力保护您的数据 - 它可以可靠地处理非正常关机。(它甚至没有正常关机的概念; bcache 只有在写入完成并存储在稳定存储上后才会返回)。

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

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

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

入门: 您需要 bcache-tools 仓库中的 bcache util。 缓存设备和后备设备都必须在使用前格式化

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>

附加

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

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 相关,cd 进入该目录并停止缓存

    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 us(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

可以是 writethrough、writeback、writearound 或 none 之一。

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

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

no cache: 从未连接到缓存集合。

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

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

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

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

命中和未命中是根据 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

Bucket 的大小

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

计算了当从缓存中读取数据时,bucket 被重用并失效的实例 - 即,在读取完成后,指针已过时。 发生这种情况时,将从后备设备重新读取数据。

trigger_gc

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

Sysfs - 缓存设备

可在 /sys/block/<cdev>/bcache 中找到

block_size

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

btree_written

所有 btree 写入的总和,以(千/兆/吉)字节计

bucket_size

Bucket 的大小

cache_replacement_policy

可以是 lru、fifo 或 random 之一。

discard

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

freelist_percent

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

io_errors

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

metadata_written

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

nbuckets

此缓存中的总 bucket 数

priority_stats

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

written

已写入缓存的所有数据的总和; 与 btree_written 进行比较可以得出 bcache 中的写入膨胀量。