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

【Ceph RADOS】BlueStore 读路径:onode、blob、缓存与 checksum 失败模式

文章导航

分类入口
storagedistributed
标签入口
#ceph#bluestore#read-path#onode#blob#cache#checksum#data-corruption#squid#v19.2.5

目录

写路径(第 8 篇)解决了「数据如何落盘」;读路径要回答的是:如何从 onode → extent_map → blob → 裸盘字节拼回正确内容,并在静默损坏时尽早失败。BlueStore 必须同时读 RocksDB(元数据)与 BlockDevice(数据),再用多层缓存挡住重复代价。本篇按调用链展开,并把 checksum 失败与 2Q 抗扫描污染写成可证伪命题,而不是「读比写简单」的印象句。

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

篇目 核心内容
第 8 篇 · BlueStore 写路径 min_alloc_size、deferred/WAL、checksum、压缩
第 9 篇 · BlueStore 读路径 onode、blob、缓存架构、checksum 失败模式
第 10 篇 · Recovery / Backfill / Scrub reservation、async recovery、mClock 边界

上一篇BlueStore 写路径 · 下一篇Recovery / Backfill / Scrub

版本锚定:Ceph Squid v19.2.5docs.ceph.com/en/squid/rados/configuration/bluestore-config-ref/;源码 src/os/bluestore/BlueStore.cc,函数 BlueStore::_do_read() @ v19.2.5


一、读路径总览

一次 RADOS 读操作进入 BlueStore 后,经历以下几个阶段:

flowchart LR
  A["read(oid, offset, length)"] --> B["onode 查找\n(cache → RocksDB)"]
  B --> C["extent_map 展开\n→ blob 列表"]
  C --> D["blob 解引用\n→ pextent 列表"]
  D --> E["BlockDevice aio_read\n→ bufferlist"]
  E --> F{压缩?}
  F -->|是| G["解压缩"]
  F -->|否| H["checksum 验证"]
  G --> H
  H --> I{校验通过?}
  I -->|通过| J["返回数据"]
  I -->|失败| K["EIO + 报错日志"]

整个过程的性能关键点在于两个缓存命中:onode 缓存(避免 RocksDB 读取)和数据缓存(避免 aio 读取)。未命中时,分别触发 RocksDB Get 和 BlockDevice aio_read。排障时先分清「慢在元数据面还是数据面」,再决定是调 bluestore_cache_meta_ratio、rocksdb 参数,还是盘与 scrub 窗口——混为一谈会调错旋钮。这一分法贯穿本篇后续各节。


二、onode 查找

2.1 onode 缓存

BlueStore 在内存中维护 onode 缓存BlueStore::OnodeCache / 相关 Cache 分片),按 collection(PG)分片。查找顺序:

  1. 由 collection + ghobject_t 构造与 PREFIX_OBJ 一致的查找键;
  2. 分片 LRU(或当前实现所用策略)命中 → OnodeRef
  3. 未命中 → RocksDB Get,反序列化 bluestore_onode_t,插入缓存。

容量由有效 bluestore_cache_size* 与比例瓜分。Squid config-ref 给出数据缓存份额公式:

[ = (1 - - ) ]

onode/meta 与 RocksDB block cache(kv)各吃掉一部分;开启 bluestore_cache_autotune 时还会向 osd_memory_target 靠拢(best-effort,不低于 osd_memory_cache_min)。调比例前先确认 autotune 是否在改你的「手调」假设。

2.2 RocksDB 读路径

未命中时:MemTable → RocksDB block cache → BlueFS 上的 SST。两级缓存分工:

omap 大的工作负载(如 RGW bucket index)压力在 PREFIX_OMAPM/m/p…),与 onode 前缀不同;compaction 抖动会同时抬高「找 onode」和「读 omap」的尾延迟。


三、extent_map 展开与 blob 解引用

读路径在拿到 onode 之后,不直接「按文件偏移读盘」,而是:

逻辑 [offset, offset+length)
  → ExtentMap 区间查询
  → 每个 Extent:blob + blob_offset
  → bluestore_blob_t::extents(pextent 列表)
  → 一次或多次 BlockDevice::aio_read
  → (可选)解压 → checksum → 裁剪拼接

3.1 extent_map

ExtentMap 是逻辑偏移上的区间映射(实现为 interval_map 一类结构)。每个 Extent 含:

跨多个 extent 的读会拆成多个 blob 读;_do_read() 可合并相邻物理 I/O,但无法合并真正分散的 pextent——长期部分覆盖会造成对象碎片化,HDD 上表现为单次逻辑读触发多次随机 aio(第七节)。

3.2 blob → pextent

bluestore_blob_textents 是裸盘 (disk_offset, disk_length) 列表;分配时找不到连续大块就会打散。压缩 blob 的 pextent 描述的是压缩后字节;shared blob 的引用在 SharedBlob / PREFIX_SHARED_BLOBX),读不改变引用计数。

若写路径刚提交的 deferred 尚未应用到裸盘,读路径仍须返回与 onode 一致的逻辑数据(与第 8 篇 deferred 不变量衔接);实现细节以 v19.2.5 _do_read() 为准,排障时不要假设「看盘偏移 = 看逻辑内容」。


四、数据缓存:2Q 与扫描污染

BlueStore 数据缓存BufferCache / BufferSpace)缓存从裸盘读出、供上层使用的数据块;淘汰算法是 2Q,而 onode 侧常用 LRU。这不是审美差异,而是针对「前台热点 + 后台全量扫描」混合负载。

4.1 算法锚点(Johnson & Shasha, VLDB 1994)

2Q(《2Q: A Low Overhead High Performance Buffer Management Replacement Algorithm》,Proceedings of the 20th VLDB Conference, 1994)把缓冲分成:

论文动机(§1–2):经典 LRU 在顺序扫描时会被一次性触碰的冷页淹没,把热点挤出——即 scan pollution。2Q 用「只进过一次 A1 的页」与「复访页」分离,使扫描流量主要伤害冷区。

4.2 为何对准 BlueStore

Ceph OSD 上 deep scrub、backfill、recovery 会顺序摸大量对象;若数据缓存用纯 LRU,前台热对象的缓存命中率会在 scrub 窗口塌陷。2Q 把首次顺序触碰留在冷区,降低该类污染——这是机制对齐:Johnson & Shasha 的假设(存在扫描型访问)在 OSD 上可检验(对比 scrub 时段 cache_hit 类计数是否相对 LRU 假想更稳;本站未做替换算法 A/B,不编造命中率百分点)。

数据区容量见上文公式中的 data_fraction。典型数量级随介质与 osd_memory_target 变化,以 ceph config get osd bluestore_cache_size / autotune 实际为准。

4.3 缓存 miss

miss → BlockDevice::aio_read()。延迟量级随介质变化(HDD 毫秒级随机读、NVMe 微秒级),此处只作数量级提示,不是基准测试结论。观测:ceph tell osd.N perf dump 中与 bluestore cache 相关的 hit/miss 计数。


五、Checksum 失败模式

数据从裸盘读出后,按 blob 记录的算法与 csum_chunk_order 分块计算,与 csum_data 比对。压缩路径:先对压缩字节验 csum,再解压(第 8 篇)。

5.1 正常路径

对每个块大小 (s = 2^{}):

\[\text{csum\_computed}[i] = H\big(\text{data}[i \cdot s \,:\, (i+1) \cdot s]\big)\]

其中 (H) 为 blob 上记录的算法(如 crc32c)。任一 (i) 不等即失败。

5.2 失败后的分层处理

行为 可观测
BlueStore _verify_csum() 失败 → 向上返回 EIO;打错误日志(含对象、偏移、期望/实际 csum) OSD log
PrimaryLogPG(副本池) 尝试从其他 acting OSD 读;成功则服务读并走修复/标记损坏对象 ceph health:damaged objects
ECBackend(EC 池) 用其余分片重建(第 11 篇);写回坏 shard PG inconsistent / repair
客户端 若冗余不足以修复 读失败 EIO

不可做的假设:checksum 失败 ≠ 自动选「最新版本」覆盖全集群;deep scrub 发现不一致后,官方路径常需显式 ceph pg repair,以避免把坏副本扩散(第 10 篇)。

5.3 检测边界(可证伪命题)

默认 osd_deep_scrub_interval 约 7 天:冷数据的发现窗口上界与此同量级(具体以集群配置为准)。


六、压缩数据的读路径

blob 标记了 FLAG_COMPRESSED 时,从裸盘读出的是压缩数据,需要先解压再验证(注意:checksum 验证的是压缩数据,即读出→验 checksum→解压,而不是读出→解压→验 checksum)。

解压完成后,数据缓存存储的是解压后的数据(而非压缩数据),这样第二次读同一 blob 时不需要重复解压。这是一个工程取舍:缓存解压数据消耗更多内存,但减少 CPU 开销;对于 CPU 紧缺的 HDD OSD 场景,缓存解压数据更合理;对于内存紧缺的高密度 OSD,反而希望存压缩数据节省内存。Squid 中 BlueStore 的数据缓存默认存解压后数据,此行为以 v19.2.5 源码为准。


七、读性能的瓶颈分析

读延迟按机制优先级大致如下(均为因果链,不是排名榜):

  1. onode 缓存命中率:未命中则 RocksDB Get;compaction 忙碌时尾部拉长。
  2. 数据缓存命中率:未命中则 aio_read;介质决定数量级。
  3. RocksDB compaction 干扰:SST 合并与 onode/omap 读争 BlueFS 带宽——与第 8 篇写路径是同一控制面缺口在读侧的投影。
  4. extent 碎片化:一次逻辑读拆成多次随机 aio;_do_read() 只能合并相邻请求。

可检验做法:在 compaction 计数上升窗口与 deep scrub 窗口分别盯客户端读 P99,看哪一个窗口更「像」元数据面瓶颈、哪一个更像数据面带宽瓶颈。不要把两种毛刺用同一句「OSD 慢了」糊过去。


八、争论:BlueStore 缓存 vs 内核页缓存

FileStore 依赖内核页缓存;BlueStore 绕过 VFS,只用自管 onode cache + 2Q 数据 cache。争论可以压成可检验命题:

主线选择是强化 BlueStore 侧预读与缓存,而不是退回页缓存。Crimson/Seastore 把该分叉推得更远:I/O 完全在 Seastar 框架内,不依赖内核缓存层(第 18 篇)。对排障而言,更有用的检验是:scrub 窗口内 bluestore cache hit 是否塌陷、以及塌陷是否与客户端读 P99 同向——这比「该不该用 page cache」口号更接近生产。


九、开放问题

1. RocksDB compaction 与读路径 P99 的相关性。在小 HDD 集群中,RocksDB compaction 产生的大量顺序读(读 SSTable)与 onode Get(也是 RocksDB 读)竞争 BlueFS 的 I/O 通道,导致 onode 查找延迟的 P99 突然上升,进而传导到客户端读 P99。这个传导链路目前没有单独的 perf counter 反映,只能通过 histogramsceph tell osd.N perf dump 的组合推断。问题的可检验形式:rocksdb_num_files_at_level* 指标持续增长期间客户端读 P99 是否同步上升。

2. 静默数据损坏(silent data corruption)与 deep scrub 的时间窗口。默认 7 天的 deep scrub 周期意味着一块全新写入但从未被读取的冷数据,理论上可以在损坏 7 天内未被发现。在单副本 EC 场景(k+2 等 EC 配置)中,如果主分片损坏而备用分片也损坏(两盘同时静默故障),scrub 前没有任何机制感知。问题的解法方向:增加 CRC 校验频率(减小 osd_deep_scrub_interval,但增加 I/O 开销),或引入后台数据验证守护线程(尚无主线实现)。

3. 压缩数据缓存的内存效率。数据缓存默认存解压后字节时,高压缩比对象会按逻辑大小吃内存,而不是按盘上物理占用。内存紧的高密度 OSD 上,这会表现为「缓存配得不小,却盖不住热工作集」。可检验:对比压缩池与未压缩池在相同 bluestore_cache_size 下的 hit 率与 RSS;若压缩池 hit 率尚可但 RSS 已顶住 osd_memory_target,说明缓存住的是放大后的逻辑视图。是否改为缓存压缩字节,属于实现策略取舍,Squid 默认行为以源码为准;调优前先确认这一点,避免按物理占用去估算缓存覆盖率。


十、参考资料

规范 / 官方文档(A 级)

源码(A 级)

论文(A 级)

站内


系列目录 · 上一篇:BlueStore 写路径 · 下一篇:Recovery / Backfill / Scrub

读完这篇,下一步读什么

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


By .