第
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.5;
docs.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
Internals(dev/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 Reference 的
Minimum 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_size 与
rotational。文档给出的空间放大例子(机制推导,非本站实测):1
KiB RGW 对象在 min_alloc_size=4 KiB 时仍占 4
KiB;三副本下约 9 KiB 设备容量被「钉住」;EC
k=4,m=2 时每个 shard
各分配一格,放大更明显。对象越大,模运算余数带来的浪费比例越低。
权衡(与文档一致):
- 更小:降低小对象浪费、减小 COW/覆盖时重写跨度;metadata 与碎片增多。
- 更大:metadata
与碎片压力下降;小对象内部空洞变大。粗 IU 的新型 QLC
等可能要求创建时显式指定匹配 IU 的
_ssd值。
写路径选择规则(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 钉住崩溃一致性,再异步应用到裸盘。
步骤:
_do_write_data()构造 deferred 载荷,序列化为bluestore_deferred_transaction_t(目标偏移 + 数据)。- 同一 RocksDB
WriteBatch写入:PREFIX_DEFERRED(字面量L)键:deferred entry;PREFIX_OBJ(O)键:更新后的 onode(extent_map 已指向「写完成后应呈现」的逻辑状态)。
- RocksDB WAL commit(经 BlueFS)。此刻对 KV 面 已持久;目标裸盘偏移上的旧字节可能尚未被覆盖。
- 向 PG 侧推进
on_applied(具体 ack 时机仍受 PG/副本协议约束),并将 entry 放入deferred_queue。 - 后台
deferred_finisher批量 aio 写到裸盘目标偏移。 - 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。
步骤:
- Allocator 分配 pextent,构造新 blob;计算 checksum,写入 blob 元数据(尚未持久)。
BlockDevice::aio_write();等待完成(io_wait())。- RocksDB
WriteBatch提交新 onode(extent_map 指向新 blob);WAL commit。 - 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)时不能就地改:
- 分配新物理区间;
- 若只覆盖部分,可能先读旧数据再合并(RMW 式合并发生在 BlueStore 内,与 EC 的 RMW 不同层);
- 新数据走直写落到新区间;
- 更新本对象 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
配置(none、passive、aggressive、force)。
压缩时机:在直写路径的 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_bytes、bluestore.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 级)
- Ceph Squid,BlueStore Configuration
Reference(
bluestore_csum_type、bluestore_compression_mode、bluestore_min_alloc_size等参数说明),docs.ceph.com/en/squid/rados/configuration/bluestore-config-ref/。 - Ceph Squid,BlueStore Internals(Developer
Guide),
docs.ceph.com/en/squid/dev/bluestore/。
源码(A 级)
ceph/cephtag v19.2.5:src/os/bluestore/BlueStore.cc(_do_write_data()、_do_write()、_txc_add_transaction());src/os/bluestore/bluestore_types.h(bluestore_blob_t、bluestore_deferred_transaction_t)。
论文(A 级)
- Aghayev et al. (2019). File Systems Unfit as Distributed Storage Backends. SOSP ’19. 评估 BlueStore 写路径与 FileStore 的双写差异,讨论 deferred 路径在 SSD 环境下的行为。
站内
- 第 7 篇:BlueStore 架构 · 第 9 篇:BlueStore 读路径 · 系列目录
- RocksDB 内核系列(RocksDB WAL 与 WriteBatch 机制)
→ 系列目录 · 上一篇:BlueStore 架构 · 下一篇:BlueStore 读路径
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】BlueStore 读路径:onode、blob、缓存与 checksum 失败模式
跟踪 BlueStore 一次读操作从 onode 缓存查询、extent map 展开、blob 物理位置定位,到 BlockDevice aio 读取和 checksum 验证的完整路径;梳理缓存架构与静默数据损坏的检测边界。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】BlueStore 架构:BlockDevice、BlueFS、RocksDB 与 Allocator
拆解 BlueStore 四个核心组件的职责边界:BlockDevice 直控裸盘、BlueFS 为 RocksDB 提供最小文件系统、RocksDB 管理对象元数据、Allocator 追踪空闲空间;交代取代 FileStore 的工程动机与 SOSP'19 学术锚点。
【Ceph RADOS】排障坐标系:slow ops、peering 卡死、满盘、恢复拖死前台与 checksum 错误
按五轴映射 Ceph 常见故障症状:slow ops(慢操作)、peering 卡死、满盘/nearfull、recovery 拖死前台、checksum/inconsistent——每轴给出核对顺序与观测入口,不粘贴伪造集群输出。