块层缓存 (bcache)¶
假设你有一个大型的、速度较慢的 raid 6,以及一两个 ssd。如果能将它们用作缓存不是很好吗?因此有了 bcache。
- bcache wiki 可以在以下网址找到
- 这是 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 处的数据无效(即我们对绕过缓存的写入执行的操作相同)
对于回写写入,我们目前将该错误传递回文件系统/用户空间。这可以改进 - 我们可以将其作为跳过缓存的写入重试,这样我们就无需使写入出错。
当我们分离时,我们首先尝试刷新任何脏数据(如果我们以回写模式运行)。但是,如果它无法读取某些脏数据,它目前不会执行任何智能操作。
操作指南/菜谱¶
使用缺失的缓存设备启动 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
请注意,如果你在写回模式下运行,这可能会导致数据丢失。
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
损坏的 bcache 在设备注册时导致内核崩溃
这不应该发生。如果确实发生了,那么你发现了一个错误!请将其报告给 bcache 开发邮件列表:linux-bcache@vger.kernel.org
请务必提供尽可能多的信息,包括内核 dmesg 输出(如果可用),以便我们提供帮助。
在没有 bcache 的情况下恢复数据
如果内核中没有 bcache,后备设备上的文件系统仍然可以通过 8KiB 的偏移量访问。因此,可以通过使用 `--offset 8K` 创建的后备设备的 loopdev,或者使用 `bcache make` 格式化 bcache 时由 `--data-offset` 定义的任何值进行访问。
例如
losetup -o 8192 /dev/loop0 /dev/your_bcache_backing_dev
这应该在 /dev/loop0 中显示你未修改的后备设备数据。
如果你的缓存处于直写模式,那么你可以安全地丢弃缓存设备而不会丢失数据。
擦除缓存设备
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
移除或更换缓存设备
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
dm-crypt 和 bcache
首先设置未加密的 bcache,然后在 /dev/bcache<N> 之上安装 dmcrypt。这比你同时对后备设备和缓存设备进行 dmcrypt 加密,然后再在其之上安装 bcache 的速度更快。[基准测试?]
停止/释放已注册的 bcache 以擦除和/或重新创建它
假设你需要释放所有 bcache 引用,以便你可以运行 fdisk 并重新注册更改后的分区表,如果其上还有任何活动的后备设备或缓存设备,则此操作将不起作用。
它是否存在于 /dev/bcache* 中?(有时它不会存在)
如果是,那就很简单。
host:/sys/block/bcache0/bcache# echo 1 > stop
但是,如果你的后备设备消失了,这将不起作用。
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 是较低设备的任何块设备堆叠,都是如此。
在其他情况下,你也可以在 /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 中写入放大的量。