前五篇把集群的每一层分别拆开:MON 维护 epoch、OSD 收
Op、CRUSH 算位置、PG
管一致性。本篇把它们串成一条线:一次
rados put 从 librados API
调用到收到写成功回复,经过哪些步骤,每一步的失败模式是什么,以及什么情况下客户端读到的是旧数据(stale
read)。
本文是「Ceph RADOS 存储内核」系列第 6 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · RADOS 全景 缺口、五轴坐标系、18 篇路线 第 2 篇 · MON/MGR 与 Map quorum、OSDMap/PGMap、epoch 传播 第 3 篇 · OSD 与 Messenger daemon、msgr2、Op 进入 OSD 第 4 篇 · CRUSH 加深 rule/bucket/weight 机制钉 第 5 篇 · PG 与 Peering PrimaryLogPG、PG log、状态机 第 6 篇 · 客户端写路径 librados → 副本应答、stale read 边界 第 7 篇 · BlueStore 架构 BlockDevice / BlueFS / RocksDB / Allocator
版本锚定:Ceph Squid v19.2.5。源码路径以
ceph/cephtagv19.2.5为准。
一、librados 与 Objecter:客户端路由层
librados 的角色
librados 是 RADOS 的 C/C++
客户端库,对外暴露 rados_t(cluster handle)和
rados_ioctx_t(pool 上下文)两层
API。在内部,librados 把所有操作委托给
Objecter(src/osdc/Objecter.h @
v19.2.5)——这个类也被 cephfs(MDS 的
objecter)、rbd(librbd)和 rgw
共用。
Objecter 的职责:
- 维护 OSDMap 缓存:从 MON 订阅 OSDMap,本地缓存当前 epoch。
- 计算对象目标:给定 pool + object name,通过 CRUSH 计算出 Primary OSD。
- 发送并重试 Op:把操作封装成
MOSDOp,发送到 Primary;收到 epoch 不匹配的错误时自动刷新 map 并重试。 - 管理 Op 生命周期:超时、重连、Messenger 层断连后的重发。
对象寻址步骤
flowchart LR
A["object name hash"] --> B["hash % pg_num = pgid"]
B --> C["OSDMap: CRUSH(pgid) = acting set"]
C --> D["acting[0] = Primary OSD"]
D --> E["send MOSDOp to Primary"]
关键函数:Objecter::_calc_target()(src/osdc/Objecter.cc
@ v19.2.5):
- 取对象名的
hash(
ceph_str_hash_rjenkins(),与 CRUSH 使用同一哈希族)。 hash_to_pg()把 hash 映射到 pg_t(考虑pg_num和pgp_num)。- 用 OSDMap 的
pg_to_acting_osds()接口通过 CRUSH 计算 acting set。 acting[0]即为 Primary;若 Primary 在 osd_primary_affinity 影响下有偏移,则查acting_primary字段。
二、MOSDOp 的构造与字段语义
MOSDOp(src/messages/MOSDOp.h @
v19.2.5)是客户端写请求的消息类型,主要字段:
| 字段 | 含义 |
|---|---|
client_inc |
客户端重连计数器,用于 Primary 去重(一次连接断开重连后递增) |
map_epoch |
客户端发送时的 OSDMap epoch |
flags |
RADOS Op
标志位(CEPH_OSD_FLAG_WRITE、CEPH_OSD_FLAG_ACK、CEPH_OSD_FLAG_ONDISK
等) |
oid |
目标对象名(含 namespace、key) |
pg |
目标 PG(spg_t) |
snapc |
Snapshot context(写 clone 时携带) |
ops |
OSDOp[] 向量,每个 OSDOp
含操作码(CEPH_OSD_OP_WRITE、CEPH_OSD_OP_READ
等)、offset、length |
reqid |
请求唯一
ID(osd_reqid_t = {client_addr, client_inc, tid}),用于幂等去重 |
flags 中 CEPH_OSD_FLAG_ONDISK
决定客户端等待 Primary ack 时,是等「写到副本内存」(ack on
journal/WAL 到达)还是「写到副本磁盘」。Squid
默认行为是等全副本
journal(WAL)持久化后才回复客户端,而非等
fsync 完成——这是 BlueStore deferred write 能在不降低
durability 保证的前提下提升吞吐的基础。
三、Primary 的
do_op() 执行
从 第
3 篇 可知,MOSDOp 经
ShardedOpWQ 进入
PrimaryLogPG::do_request() /
do_op()(PrimaryLogPG.cc @
v19.2.5)。简化步骤:
- 检查 pool、snap、对象是否属于本 PG;PG
必须可服务(否则进
waiting_for_active等队列)。 - 加载
ObjectContext(onode 等元数据)。 - 处理 snapshot/clone(细节见 第 12 篇)。
prepare_transaction()生成ObjectStore::Transaction(数据写 + PG log 追加等)。issue_repop()向 acting 副本发MOSDRepOp。- 收齐策略要求的副本应答后,提交并回复客户端。
一次写的 txn 典型包含数据写、可选 snap 元数据、以及把
pg_log_entry_t 记入元数据存储(BlueStore 上经
RocksDB/BlueFS 路径)。本地分配器与 deferred 切换留给第 7–8
篇;本篇只强调:版本号与是否提交由 Primary
决定,副本不能改写 eversion。
若 PG 处于 degraded 但仍 Active,写仍走当前
acting;min_size 不满足则根本进不了可写
Active(第 5 篇)。客户端侧看到的「卡住」要先区分:是
peering 未完成、是副本 ACK 慢,还是读租约
LAGGY——三者日志与状态字不同。
四、副本写:MOSDRepOp 与应答链
Primary → Replica
Primary 向 acting set 中每个副本发
MOSDRepOp(src/messages/MOSDRepOp.h
@ v19.2.5):数据载荷加上 Primary 已定好的
pg_log_entry_t。副本不能改版本号,只能接受或拒绝(拒绝可触发重新
peering)。这落实 PDSW 2007 的「Primary
为单点序列化者」。
Replica 处理与 ACK 含义
副本在 epoch 匹配时应用
ObjectStore::Transaction,再回
MOSDRepOpReply。回复携带的 flag(如 ACK /
ONDISK)表示持久化进度到了哪一档;客户端侧最终看到的成功,取决于请求
flags 与池策略要求等到哪一档。Squid
上常见路径是:副本侧先把更新记到可崩溃恢复的日志/WAL
语义路径,再让 Primary 向客户端确认——具体与 BlueStore
deferred 的交界在第 7–8 篇,本篇不展开分配器。
Primary 收齐后
RepGather(ReplicatedBackend)跟踪应答。达到策略要求的副本集合后,Primary
提交本地事务、更新
pg_info.last_update、向客户端发
MOSDOpReply。
sequenceDiagram
participant CLI as librados Client
participant PRI as Primary OSD
participant R1 as Replica 1
participant R2 as Replica 2
CLI->>PRI: MOSDOp (write, epoch N)
PRI->>R1: MOSDRepOp (data + pg_log_entry)
PRI->>R2: MOSDRepOp (data + pg_log_entry)
PRI->>PRI: local ObjectStore txn
R1-->>PRI: MOSDRepOpReply
R2-->>PRI: MOSDRepOpReply
PRI-->>CLI: MOSDOpReply
失败模式:某一副本慢 →
整次写尾延迟被最慢副本拖住(Primary 序列化的代价);副本
epoch 落后 → 忽略/拒绝,Primary 侧可能进入
peering。这与「读租约 LAGGY」不同层:写卡住看副本
ACK,读卡住看 readable_until。
五、可读性边界:Preventing
Stale Reads 与 readable_until
写路径在 acting set 同步提交后才
ACK,限制了写侧不一致。读默认只打
Primary,客户端用本地 OSDMap
选目标。多数情况下:map 正确,或旧 Primary 已知自己不再是
Primary,会塞给客户端一张更新的
map。官方要堵住的是另一类故障(docs.ceph.com/en/squid/dev/osd_internals/stale_read/):
某个 OSD 被隔开、尚未学到 map 更新,却继续为同样落后的客户端服务读;与此同时,新 Primary 已经可以接受写。
这才是「stale read」威胁模型——不是「RADOS 主动返回旧副本值」,而是被围栏的旧 Primary 在新区已经前进后仍提供读。
读租约:readable_until
/ readable_until_ub
每个 pool 可设
read_lease_interval;默认取
\[ \mathrm{read\_lease\_interval} \approx \texttt{osd\_pool\_default\_read\_lease\_ratio}\times \texttt{osd\_heartbeat\_grace} \]
其中 ratio 默认 0.8。意图:在 MON 把失败
OSD 标 down 之前,读租约通常已经过期。因此调大
osd_heartbeat_grace 会同时拉长「误判
down」的耐心和「旧 Primary 仍可能可读」的窗口——与 Stretch /
跨 AZ 部署直接相关。
Primary 与副本都维护:
| 字段 | 含义 |
|---|---|
readable_until |
本 OSD 允许服务读的截止时间(租约) |
readable_until_ub |
对 acting set 内任意 OSD 的 readable_until
的上界 |
Primary 发 pg_lease_t 抬高副本侧上界;收齐
pg_lease_ack_t 后,再抬高自己的
readable_until 并再次分享。不变量:
\[ \forall\, o\in\mathrm{acting}:\quad o.\mathrm{readable\_until}\le o'.\mathrm{readable\_until\_ub}\quad(\forall o') \]
全程用单调时钟,避免墙上时钟跳变;peer 估计本地时钟偏差的上下界,把 Primary 时间戳翻译成各自本地截止时刻。
Interval
切换:prior readable_until_ub、LAGGY、WAIT
Interval 变更时,需要 prior interval
上「谁还可能在服务读」的上界。各 OSD 把
readable_until_ub(作为一段剩余时长)放进
pg_history_t 参与 peering;因 peering
消息延迟,上界会略往前滑——对上界而言是安全方向。
| 状态 | 触发 | 对客户端 |
|---|---|---|
| LAGGY | Active 期间租约过期仍收到请求 | 读/相关请求阻塞,租约续上后重入队 |
| WAIT | Peering 已完成,但 prior interval OSD 仍可能可读 | 阻塞客户端请求直至等待足够或提前退出;recovery 可继续(用户可见内容不变) |
Dead OSDs 提前结束 WAIT:若能推断 prior
OSD 已崩溃(如 ECONNREFUSED),或该 OSD
已知自己被标 down(dead_epoch
等),则可提前离开 WAIT,而不用空等到租约自然过期。
sequenceDiagram
participant P as Primary
participant R as Replica
participant C as Client
P->>R: pg_lease_t (raise readable_until_ub)
R-->>P: pg_lease_ack_t
P->>R: pg_lease_t (raise readable_until)
Note over P,R: readable_until <= any readable_until_ub
C->>P: MOSDOp READ
alt lease valid
P-->>C: data
else readable_until expired
P->>P: enter LAGGY, block op
end
与 epoch 追赶、peering 的分工
| 机制 | 解决什么 |
|---|---|
| 客户端 map / Objecter 重试 | 客户端或 OSD 视野不一致时的路由纠错 |
readable_until 租约 |
阻止「旧 Primary + 旧客户端」在新区已写之后继续读 |
peering WAIT |
新区已 Active,但仍须等旧区读租约失效 |
waiting_for_active |
本 interval 尚未 Active(第 5 篇) |
因此「写成功后 Primary-routed 读立即可见」在同一
Active interval、租约有效时成立;跨 Primary
切换时,正确性靠租约与 WAIT,而不是「返回旧值」或单纯依赖
osd_op_timeout。
reqid
去重边界(写侧)
MOSDOp.reqid(client +
client_inc + tid)记在
pg_log_entry_t 上做 at-most-once。同一
client_inc
内重发可直接返回旧结果;client_inc 递增后旧
reqid 失效——网络分区下重连后不应盲目重放未确认写。
六、客户端重试与 epoch 追赶
| 错误 / 现象 | 原因 | Objecter 动作 |
|---|---|---|
附带新 MOSDMap 的重试 |
客户端 map 落后或 acting 变化 | 合并 map,_calc_target() 后重发 |
| OSD 尚未追上 map | 客户端相对新 | 等待 / 订阅后重发 |
| 连接超时 | Primary 无响应 | 受 osd_op_timeout /
rados_osd_op_timeout 约束后向 MON 要 map |
ENOENT 等应用语义错误 |
对象状态不符 | 上报应用,不盲目重试 |
重试中 Op 由 Objecter 持有,不丢请求;aio_*
回调看到的是最终结果。默认
osd_op_timeout = 0(无超时)时,宕机 Primary
可能导致长时间挂起——生产宜设有限超时,并与 Messenger
keepalive 区分开。
跨 Primary 切换时还可能先卡在第 5 篇的 peering,或本节的 LAGGY/WAIT(读租约),这与「单纯 epoch 数字不等」不是同一层。把三类阻塞画在同一时间线上,才能解释「集群看起来健康但读请求仍挂起」的现场现象。
七、小结
- 客户端路由本地完成:Objecter 用缓存 OSDMap 算 Primary;map 追赶对应用透明,表现为延迟毛刺。
reqid幂等边界:同一client_inc内可去重;重连递增后旧 tid 失效。- Primary 单点序列化:版本号只由 Primary 分配;副本只接受或拒绝。
- 写完成语义:默认等副本侧持久化路径(WAL/journal 语义)达到策略要求后 ACK;与 BlueStore deferred 的配合见第 7–8 篇。
- stale read 靠读租约堵住:威胁是隔开的旧
Primary 继续服务落后客户端;
readable_until/ LAGGY / WAIT 是官方机制(stale_read/)。 - 延迟抖动分层:epoch 追赶 / peering·租约 / BlueStore——三轴排查路径不同。
八、谱系、争论与开放问题
谱系
客户端写路径的核心设计(Objecter → MOSDOp → Primary → RepOp → Reply)在 Sage Weil 博士论文(2007)§6 与 RADOS 论文(PDSW 2007)中已成形:Primary 而非 Monitor 控制副本写;副本不能修改 PG log 版本。Squid 在此之上叠加的关键工程层是 Preventing Stale Reads 的读租约——把「map 最终会纠正路由」不够覆盖的围栏场景,收成可配置的时间界。
读租约与 MON 的 MMonLease、OSD 心跳 grace
处在同一「用时间换安全」家族,但作用点不同:MON
租约服务管理面读;readable_until
服务数据面读围栏。调参时不要只改其中一个却假设另一个自动正确。
争论:强一致 Primary-only 读 vs 副本读
A 侧(默认 Primary-only):与 Preventing
Stale Reads 的租约模型一致,RBD/CephFS 依赖强一致。
B 侧(BALANCE_READS /
LOCALIZE_READS):分散读热点,但一致性弱化;peering/租约窗口下的语义需应用自行承担。官方
stale-read 文档针对的是默认 Primary
读路径下的围栏问题,开启副本读等于换威胁模型。
实践中 RBD 默认不走副本读;若在跨 AZ 场景用
LOCALIZE_READS
换延迟,必须单独回答「可接受多旧、租约与 map
如何配合」——这不是开关级优化,而是一致性模型变更。
开放问题
- 跨地域 Primary 与租约时长:Stretch / 多
AZ 下写 RTT 与
read_lease_interval(随osd_heartbeat_grace)耦合——grace 调大减误判,却拉长 WAIT/LAGGY 窗口。可读入口:docs.ceph.com/en/squid/rados/operations/stretch-mode/与stale_read/。 - 大对象写与 BlueStore 尾延迟:覆盖写撞上 RocksDB compaction 时,租约与 epoch 都正常,延迟却来自本地存储轴——第 7–9 篇拆。可检验问题:在排除 LAGGY/WAIT 与 map 追赶后,延迟尖刺是否仍与 OSD 上 RocksDB stall 计数相关?
九、参考资料
规范 / 官方文档(A)
- Ceph Squid Preventing Stale Reads —
docs.ceph.com/en/squid/dev/osd_internals/stale_read/(readable_until、LAGGY、WAIT)。 - Ceph Squid Architecture —
docs.ceph.com/en/squid/architecture/。 - Ceph Squid OSD Internals —
docs.ceph.com/en/squid/dev/osd_internals/。 - Ceph Squid Stretch Mode —
docs.ceph.com/en/squid/rados/operations/stretch-mode/。
源码(A)
src/osdc/Objecter.h、src/osdc/Objecter.cc@ v19.2.5:客户端路由、epoch 追赶、_calc_target()。src/messages/MOSDOp.h@ v19.2.5:MOSDOp字段定义。src/messages/MOSDRepOp.h@ v19.2.5:MOSDRepOp与MOSDRepOpReply。src/osd/PrimaryLogPG.cc@ v19.2.5:do_op()、prepare_transaction()、issue_repop()。src/osd/ReplicatedBackend.h、src/osd/ReplicatedBackend.cc@ v19.2.5:副本写应答管理(RepGather)。
论文(A)
- Sage A. Weil, Andrew W. Leung, Scott A. Brandt, Carlos Maltzahn. “RADOS: A Scalable, Reliable Storage Service for Petabyte-scale Storage Clusters.” PDSW 2007——Primary-centric 写模型的原始定义。
- Sage A. Weil. “Ceph: Reliable, Scalable, and High-Performance Distributed Storage.” PhD Thesis, UC Santa Cruz, 2007,§6——客户端 I/O 路径。
站内对照
- PG 与 Peering(第 5 篇)(Active 状态与 missing 集合)
- OSD 与 Messenger(第 3 篇)(Op 进入 OSD 的通信层)
- BlueStore 架构(第 7 篇)(ObjectStore txn 的本地执行)
- RBD(第 12 篇)(librados 之上的块设备接入层)
实验台账
- 本篇无命令输出;完整写路径可通过在 vstart 集群上打开 OSD
debug
日志(
debug osd = 20)追踪,此处不伪造输出。
← 上一篇:PG 与 Peering · 下一篇:BlueStore 架构
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】PG 与 Peering:PrimaryLogPG、PG log 与状态机
拆解 PG 命名与生命周期、PrimaryLogPG 的日志式复制设计、pg_log_entry_t 格式、last_epoch_started 与 last_epoch_clean 的语义差异,以及 peering 状态机从 Reset 到 Active 的完整路径。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】MON/MGR 与 Map:quorum、epoch 传播与客户端可见性
拆解 Ceph MON Paxos quorum、OSDMap/PGMap 结构与 epoch 传播机制;说明 MGR 主备模型与模块框架;分析客户端 epoch 缓存与可见性窗口的边界条件。
【Ceph RADOS】RBD:image 到对象的映射、krbd 与 librbd、快照与克隆边界
拆解 RBD image 在 RADOS 上的对象命名、striping 规则、krbd 与 librbd 的驱动差异、以及快照 COW 与克隆父子依赖的边界——不写安装手册,只写机制与失败模式。