土法炼钢 · 系统与基础设施

【Ceph RADOS】BlueStore 写路径:min_alloc_size、deferred/WAL、checksum 与压缩

文章导航

分类入口
storagedistributed
标签入口
#ceph#bluestore#write-path#deferred-write#wal#checksum#compression#squid#v19.2.5

目录

第 7 篇划清了 BlockDevice、BlueFS、RocksDB、Allocator 四个组件的边界。本篇把这四个组件串起来,跟着一次对象写操作走完从 queue_transactions() 到 aio 完成的完整路径,重点是写路径上的两个分叉:deferred 路径和直写路径,以及 checksum 与压缩在哪一步介入。

本文是「Ceph / RADOS 存储内核」系列第 8 篇(共 18 篇)。→ 系列目录

篇目 核心内容
第 7 篇 · BlueStore 架构 BlockDevice / BlueFS / RocksDB / Allocator 组件职责
第 8 篇 · BlueStore 写路径 min_alloc_size、deferred/WAL、checksum、压缩
第 9 篇 · BlueStore 读路径 onode、blob、cache、checksum 失败模式

上一篇BlueStore 架构 · 下一篇BlueStore 读路径

版本锚定:Ceph Squid v19.2.5docs.ceph.com/en/squid/rados/configuration/bluestore-config-ref/;源码入口 src/os/bluestore/BlueStore.cc,函数 BlueStore::_do_write_data() @ v19.2.5。本篇无伪造性能数字。


一、写路径的起点:queue_transactions()

PrimaryLogPG 完成副本确认后,调用 ObjectStore::queue_transactions() 把一批操作提交给 BlueStore。BlueStore 将这批操作封装成一个 TransContext(txc),在 txc 的生命周期内完成分配、编码、I/O 提交、元数据落盘和回调通知。

单次写 txc 的核心路径如下:

flowchart LR
  A["queue_transactions()"] --> B["_do_write_data()"]
  B --> C{size vs min_alloc_size\n+ prefer_deferred 等}
  C -->|"deferred / WAL 覆盖"| D["kv commit 先\nPREFIX_DEFERRED L + onode"]
  C -->|"新分配大写 / COW"| E["裸盘 aio 先\n再 RocksDB commit onode"]
  D --> F["deferred_finisher\n后台 aio 落到目标 pextent"]
  E --> G["aio 完成 → kv commit"]
  F --> H["删除 L 键;txc 收尾"]
  G --> I["txc 完成,回调 PG"]
  H --> I

两条路径最终都要求:(1)用户可见数据最终在裸盘正确位置;(2)onode 的 extent_map 在 RocksDB 中与之一致。关键不变量是顺序相反的

路径 持久化顺序不变量 崩溃时「未 ack」含义
直写(新分配大块) 数据 aio 完成 → 再 RocksDB commit onode kv 未提交则无 onode 引用;裸盘孤儿区间 mount 时回收
deferred(小写/就地覆盖) RocksDB commit(L deferred + onode)→ 后台 aio 到目标偏移 kv 已提交则 mount 重放 deferred;幂等覆盖

官方开发者文档 BlueStore Internalsdev/bluestore/)把小写策略写成字母表(U/P/W/N/R+W/C):例如 U = 完整新 blob 直写后 kv commit;W = 先 kv commit 再异步 WAL overwrite;R+W = 先读齐 checksum chunk 再 WAL 覆盖。本篇用「直写 / deferred」概括生产排障最常碰到的二分;细策略以该表与 _do_write_data() 为准。


二、min_alloc_size:分叉点与空间语义

bluestore_min_alloc_size 是 Allocator 的最小分配粒度,也是写路径选择与空间放大的共同旋钮。官方说明见 Squid BlueStore Configuration ReferenceMinimum Allocation Size 一节(docs.ceph.com/en/squid/rados/configuration/bluestore-config-ref/)。

版本事实(不要背错默认值)

时期 HDD 默认 SSD/NVMe 默认
Mimic 及更早 64 KiB 16 KiB
Octopus (仍偏大) 4 KiB
Pacific 及以后(含 Squid 4 KiB 4 KiB
SOSP’19 论文叙述 64 KiB 16 KiB

该值在 OSD 创建时烘焙;事后改配置不会改变已有 OSD,升级发行版也不会自动改写。Reef/Squid 可用:

ceph osd metadata osd.N | egrep 'rotational|alloc'

核对 bluestore_min_alloc_sizerotational。文档给出的空间放大例子(机制推导,非本站实测):1 KiB RGW 对象在 min_alloc_size=4 KiB 时仍占 4 KiB;三副本下约 9 KiB 设备容量被「钉住」;EC k=4,m=2 时每个 shard 各分配一格,放大更明显。对象越大,模运算余数带来的浪费比例越低。

权衡(与文档一致):

写路径选择规则(BlueStore::_do_write_data() 内逻辑的概括;并受 bluestore_prefer_deferred_size 等影响):

写场景 路径 原因
新写,数据 ≥ min_alloc_size 且可整块分配 直写(对应文档策略族中的 U/N 等) 新 pextent;aio 后再 kv
新写,数据 < min_alloc_size deferred(W / R+W 族) 先把数据承诺写入 PREFIX_DEFERRED,再异步落到盘
覆盖写,非共享 blob 常走 deferred / 读齐 chunk 再覆盖 避免崩溃窗口留下半新半旧且与 csum 不一致
覆盖写,共享 blob(克隆) COW:新分配后直写 不得就地改共享物理区间

文档原话要点:小于 min_alloc_size 的写必须先经 BlueStore journal(deferred);更大的值减少描述盘布局的 metadata、降低碎片。具体分支以 v19.2.5 源码为准。


三、deferred 写路径(小写与覆盖写)

3.1 机制与不变量

deferred 路径的核心思路:把「即将覆盖到某 pextent 的字节」当作 RocksDB 事务的一部分提交,利用 KV 的原子 commit 钉住崩溃一致性,再异步应用到裸盘

步骤:

  1. _do_write_data() 构造 deferred 载荷,序列化为 bluestore_deferred_transaction_t(目标偏移 + 数据)。
  2. 同一 RocksDB WriteBatch 写入:
    • PREFIX_DEFERRED(字面量 L)键:deferred entry;
    • PREFIX_OBJO)键:更新后的 onode(extent_map 已指向「写完成后应呈现」的逻辑状态)。
  3. RocksDB WAL commit(经 BlueFS)。此刻对 KV 面 已持久;目标裸盘偏移上的旧字节可能尚未被覆盖。
  4. 向 PG 侧推进 on_applied(具体 ack 时机仍受 PG/副本协议约束),并将 entry 放入 deferred_queue
  5. 后台 deferred_finisher 批量 aio 写到裸盘目标偏移。
  6. aio 完成后删除对应 L 键(再次 KV commit)。

不变量(可检验):在步骤 3 成功之后、步骤 5 完成之前,权威数据在 deferred 记录里;读路径必须能看到与 onode 一致的逻辑内容(实现上通过 deferred 与缓存协同,细节以 _do_read() 为准)。不能假设「onode 已更新 ⇒ 裸盘目标偏移已是新数据」。

3.2 崩溃安全三段

崩溃点 盘上状态 恢复动作
步骤 3 前 无新 onode / 无 L 写如同未发生;未向客户端构成 commit 语义
步骤 3 后、5 前 onode + L 在,裸盘可能仍旧 mount 扫描 L,重放 aio(幂等)
步骤 5 后、6 前 裸盘已新,L 仍在 再次幂等覆盖;随后删 L

这与直写路径的「aio 在前、kv 在后」对称:deferred 用 KV 先提交 换取就地覆盖的安全窗口。SOSP’19 §4.2 将小写描述为先插入 RocksDB 作为 future I/O 的 promise,再异步落盘——与上表一致。

3.3 代价与节流

deferred 引入受控双写:数据进 RocksDB/BlueFS WAL,再由 finisher 写到数据盘。范围限于小写与需要安全覆盖的场景,不是 FileStore 式全量双写。

积压时前台会被拖住:关注 bluestore_throttle_deferred_bytes(deferred 字节上限触发提交节流,见 config-ref)以及 bluestore_max_deferred_txs / batch 参数。观测入口:ceph tell osd.N perf dump 中与 deferred 相关的计数;完整「queue 深度 → aio 完成」延迟直方图若缺失,属于开放问题(第八节)。


四、直写路径(大写与 COW)

4.1 机制与不变量

直写适用于新分配的大块(≥ min_alloc_size 对齐的新 blob 等):数据先到新 pextent,元数据后到 RocksDB

步骤:

  1. Allocator 分配 pextent,构造新 blob;计算 checksum,写入 blob 元数据(尚未持久)。
  2. BlockDevice::aio_write()等待完成io_wait())。
  3. RocksDB WriteBatch 提交新 onode(extent_map 指向新 blob);WAL commit。
  4. txc 完成,回调 PG。

不变量:客户端 commit 语义建立之前,要么(aio 未完成 / kv 未提交)写不可见,要么(kv 已提交)裸盘新区间已被 aio 写满且与 csum 一致。若 aio 完成、kv 提交前崩溃:无 onode 引用 → mount 回收孤儿区间;安全,因 on_commit 未发出。若 kv 已提交后崩溃:重放 RocksDB WAL 恢复 onode,裸盘数据已在位。

与 deferred 对比时只需记住一句话:直写用「盘先于 KV」避免双写;deferred 用「KV 先于盘」允许安全就地覆盖。

4.2 COW 写

覆盖共享 blob(引用计数 > 1)时不能就地改:

  1. 分配新物理区间;
  2. 若只覆盖部分,可能先读旧数据再合并(RMW 式合并发生在 BlueStore 内,与 EC 的 RMW 不同层);
  3. 新数据走直写落到新区间;
  4. 更新本对象 extent_map;原 shared blob 引用递减,为零则归还 Allocator(并更新 PREFIX_SHARED_BLOB)。

克隆快照语义:克隆方看不到之后的覆盖。


五、Checksum:计算时机与存储位置

BlueStore:写时计算 → 存进 blob 元数据(随 onode 进 RocksDB)→ 读时校验裸盘字节。相对 FileStore 依赖 XFS(通常无用户数据 checksum)这是端到端静默损坏检测的底座。

计算时机:在 aio 提交(直写)或构造 deferred 载荷(deferred)之前,按 csum_chunk_order 分块计算,写入 bluestore_blob_t::csum_data,与 onode 同一 KV 事务落盘。

算法bluestore_csum_type;亦可 per-pool csum_type,见 config-ref):

配置值 算法 备注
none 禁用 失去读时检测
crc32c CRC-32C(常见默认) 常有硬件加速
crc32c_16 / crc32c_8 截断 省元数据,误判率上升
xxhash32 / xxhash64 xxHash 非加密完整性

块大小由 blob 的 csum_chunk_order(及配置侧 bluestore_csum_block_size 一类参数)决定:更小 → 定位更细、元数据更大。

存储位置不变量csum_data元数据面;被校验的字节在数据面。读时只要 onode 在 cache,不必为「取 csum」再付一次随机读。压缩时 csum 覆盖的是压缩后字节(第六节)。

失败模式(返回 EIO、副本/EC 修复)见第 9 篇;本篇只钉写路径必须把「将要落盘的字节」与「写入 KV 的 csum」绑在同一事务语义里——deferred 则绑在 L+O 同批提交上。


六、压缩:写路径上的透明压缩

BlueStore 支持对象数据的透明压缩,通过 bluestore_compression_mode 配置(nonepassiveaggressiveforce)。

压缩时机:在直写路径的 aio 提交之前,对数据缓冲区执行压缩。压缩后若数据变小,分配更小的物理区间(节省空间);若压缩比太差(bluestore_compression_required_ratio 控制,默认 0.875,即压缩后至少缩小 12.5%),则放弃压缩,按原始大小分配。

压缩算法bluestore_compression_algorithm):

算法 特点
snappy 压缩快,解压更快,压缩率一般
zlib 压缩率好,CPU 开销高
lz4 速度与压缩率的折中,适合 NVMe 场景
zstd 高压缩率,支持多级别(level 1-22);适合冷数据

压缩后的 blob 在 bluestore_blob_t 的 flags 中标记 FLAG_COMPRESSED,并记录原始逻辑长度(logical_length)和压缩后物理长度(实际 pextent 总长度)。读时需先解压才能还原数据,因此压缩对读路径有 CPU 开销,对延迟敏感场景需要评估。

压缩与 checksum 的关系:checksum 在压缩后计算,即 csum_data 校验的是压缩数据,而非原始数据。这意味着解压后的数据完整性需要应用层(PG log 校验或 scrub)另外保证;BlueStore 层的 checksum 只保证读到的压缩块没有静默损坏。

压缩与 deferred 路径的关系:deferred 路径(小写)通常不开压缩,因为压缩本身需要读取完整块(至少 csum_block_size),而小写可能触发 RMW 式的读-改-写-压缩操作,代价过高。实际行为以 Squid 源码中 _do_write_data() 内的压缩启用条件为准。


七、争论:deferred 路径的抖动问题

deferred 写路径在社区有一个持续的争论点:deferred_finisher 的后台写与前台写之间的带宽竞争

具体场景:当工作负载大量产生小写(< min_alloc_size,如 4 KiB 随机写),deferred queue 快速积累;deferred_finisher 以批量 aio 的形式将数据从 WAL(DB SSD 上)复制到数据盘(HDD 或主 SSD),产生顺序写流量。这与前台客户端写同样需要的数据盘写带宽形成竞争,在 HDD 场景下表现为周期性写延迟抖动(latency spikes)。

SOSP’19 §4.2 / 评估章节在论文实验假设(含 SSD 数据盘等)下讨论 deferred 相对 FileStore 全量双写的改善;把「论文集群上的曲线」说成「任意 HDD 生产集群无抖动」不成立。HDD 上 WAL→数据盘的二次写与前台争带宽时,抖动更常见——这是机制外推,本站无伪造 IOPS。

工程响应:Ceph 通过 bluestore_deferred_batch_ops(每批最多 deferred ops 数)、bluestore_max_deferred_txs(queue 深度上限)等参数调节。mClock 调度器(见第 10 篇)可以限制 recovery/scrub 对带宽的抢占,但对 deferred_finisher 的带宽使用没有直接控制。这个工程间隙在 Squid 时代尚未有系统性解法,属于调优而非架构修复。


八、开放问题

1. deferred 路径的可观测性ceph tell osd.N perf dump 可以看到 bluestore.deferred_write_bytesbluestore.deferred_write_ops 等计数器,但无法直接看到 deferred queue 当前积压深度和 deferred_finisher 的吞吐。完整的 deferred 路径延迟分解(从 RocksDB commit 到 aio 完成)目前没有暴露为单独的 perf 计数器,排障时需要结合日志和 aio 统计推断。

2. 已部署 OSD 的 min_alloc_size 与文档默认不一致。Squid 文档默认 4 KiB,但 Mimic 时代创建的 OSD 可能仍是 64/16 KiB。混布会导致 ceph df raw/stored 比异常、balancer 行为怪异(config-ref 已警告)。可检验:ceph osd metadata 中 alloc 字段是否集群一致;不一致时只能销毁重建 OSD,不能热改。

3. 压缩与 checksum 的协议升级。当压缩算法或 checksum 算法配置变更时,已有 blob 维持原格式,新写的 blob 用新格式。这导致一个对象上可能同时存在不同算法的 blob,读路径需要按 blob 记录的算法字段分别处理。滚动升级期间的一致性管理(特别是 scrub 期间的跨版本 blob 校验)是一个运维难点。


九、参考资料

规范 / 官方文档(A 级)

源码(A 级)

论文(A 级)

站内


系列目录 · 上一篇:BlueStore 架构 · 下一篇:BlueStore 读路径

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .