PG(Placement Group,放置组)是 RADOS 的一致性单元——不是对象,也不是 OSD,而是介于二者之间的调度边界。一次写操作从客户端到达时,Primary OSD 需要知道该 PG 当前是否 Active(能服务请求);若不是,Op 在本地等待,不返回错误。理解 PG 状态机与 peering 是「写为什么堵住了」的核心问题。
本篇聚焦三件事:PG log
的精确格式与不变量、last_epoch_started 与
last_epoch_clean 的语义差异(经常被混淆)、以及
peering 状态机的关键节点。不写 backfill/recovery 细节(见 第
10 篇)。
PG 机制是 Ceph 最容易「知道有这个东西但不知道为什么这样设计」的部分。一个常见的误解是把 PG 和「文件目录」类比——PG 没有目录语义,它只是一个哈希桶,决定「哪些对象由哪组 OSD 共同管理」。另一个常见误解是把 peering 和「选主」混淆——peering 不只是选出 Primary,而是完整地重建 PG 内所有对象的权威版本视图,确保 acting set 内的所有 OSD 对「哪些对象需要恢复」达成一致认知。
本文是「Ceph RADOS 存储内核」系列第 5 篇(共 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、last_epoch_started、状态机 第 6 篇 · 客户端写路径 librados → 副本应答、stale read 边界
版本锚定:Ceph Squid v19.2.5。Peering 状态机文档:
docs.ceph.com/en/squid/dev/osd_internals/;last_epoch_started语义:docs.ceph.com/en/squid/dev/osd_internals/last_epoch_started/。
一、PG 的命名与目的
命名格式
PG 的名字是
<pool_id>.<pgid>,例如
1.0 表示 pool 1 的 PG 0,2.3f 表示
pool 2 的 PG 63(十六进制)。pgid
是对象哈希值对 pg_num 取模的结果,范围
[0, pg_num)。
PG 不存储对象的实际数据,而是作为一组对象的一致性管理单元:同一 PG 内的所有对象由同一组 OSD(acting set)服务,并共享同一个 PG log。
为什么需要 PG
如果直接以对象为一致性单元,peering 和 recovery 需要在
\(O(\text{total objects})\)
规模上协商,规模爆炸。PG 把这个协商压缩到 \(O(\text{pg\_num})\)(通常每个
OSD 上 100–250 个 PG)。代价是:pg_num
的选取是静态的(Luminous 后 pg_autoscaler
可调,但变更本身有代价),且 pg_num 不足时单个 PG
可能过大,recovery 时间长。
二、PrimaryLogPG:日志式复制的核心
PrimaryLogPG(src/osd/PrimaryLogPG.h
@ v19.2.5)是 Ceph 中处理副本写和日志式 peering
的核心类,继承自
PG。它同时用于副本池(replicated)和纠删码池(EC)——EC
池通过 ECBackend 插件处理,但 PG log 和 peering
逻辑由 PrimaryLogPG 统一管理。
名字中「Log」的含义:所有写操作在应用到 ObjectStore 之前,先追加到 PG log(在 RocksDB 中持久化)。这使得 peering 可以通过比对日志重建各 OSD 的状态,而不需要全量扫描数据。
PrimaryLogPG::do_op()
是前台写的入口(src/osd/PrimaryLogPG.cc @
v19.2.5):
// 简化路径(src/osd/PrimaryLogPG.cc @ v19.2.5)
void PrimaryLogPG::do_op(OpRequestRef& op) {
// 1. 检查 PG 状态(active? readable? writeable?)
// 2. 处理 snapshots
// 3. 准备 ObjectContext
// 4. 调用 prepare_transaction(),生成 BlueStore txn
// 5. 发送 MOSDRepOp 到副本
// 6. 等待副本 ack 后 commit_txn() 并回复客户端
}三、PG log 条目格式与不变量
PG log 记录了该 PG 上所有写操作的版本链,以
pg_log_entry_t
为单元(src/osd/osd_types.h @ v19.2.5):
struct pg_log_entry_t {
eversion_t version; // 本次操作的版本(epoch + version)
eversion_t prior_version; // 同一对象上一次操作的版本
eversion_t reverting_to; // rollback 时使用
osd_reqid_t reqid; // 请求 ID(client + tid),用于 at-most-once 去重
hobject_t soid; // 目标对象(含 snap 信息)
uint8_t op; // 操作类型(WRITE / CLONE / DELETE / LOST_REVERT 等)
// ... 其他字段(user_version, snaps, ...)
};eversion_t
格式:epoch.version,其中
epoch 是 OSDMap epoch,version 是
PG 内单调递增的序列号。例如 5.0000000a 表示
epoch 5 下第 10 次操作。version
的单调性保证了在同一 epoch 内日志条目的全序。
PG log 不变量
PG log 维护以下不变量(来自
docs.ceph.com/en/squid/dev/osd_internals/log_based_pg/):
- last_update:PG log 中最新条目的 version;Primary 处理写后立即更新。
- last_complete:所有 acting set OSD
都已应用到的最新
version;
last_complete <= last_update。 - log_tail:PG log 保留的最老条目
version;比
log_tail更老的操作可能没有日志记录,需要 backfill 而非 log-based recovery。
当两个 OSD 的 PG log 都不包含某个对象的差异区间时(即需要比较的区间超过了双方的 log 范围),peering 退化为 backfill(全量对象比对),代价大幅上升。
四、pg_info_t:info.last_epoch_started
与 history.last_epoch_started
pg_info_t(src/osd/osd_types.h
@ v19.2.5)是每个 OSD 本地保存的 PG 摘要。官方文档把
LES
钉得很细(docs.ceph.com/en/squid/dev/osd_internals/last_epoch_started/),不能简化成「最近一次
Active 的 epoch」一句话。
struct pg_info_t {
spg_t pgid;
eversion_t last_update;
eversion_t last_complete;
epoch_t last_epoch_started; // info.LES:本 OSD 本地视角
// history 内还有 history.last_epoch_started / last_epoch_clean
eversion_t log_tail;
pg_stat_t stats;
};info.last_epoch_started(本
OSD)
info.last_epoch_started 记录一个激活 epoch
\(e\),对应某个 peering
interval \(i\),使得:
- 在 \(i\) 或更早 interval 已提交的写,都已反映在该 OSD 的本地 info/log 中;
- 晚于 \(i\) 的写,不会出现在这份本地 info/log 中。
因为「已提交的写永不 divergent」,即使后来拿到一份权威
log/info 的 info.last_epoch_started
更旧,也可以保留自己更大的
info.last_epoch_started(见文档对
PG::proc_master_log 的说明)。
激活消息到达时就会更新
info.last_epoch_started;但
history.last_epoch_started 要等新的
info.last_epoch_started
持久化之后才更新(常常与第一次写一起落盘)。这样后续
peering 不会在「acting set 尚未全体记下
LES」之前,就强制要求某个 OSD 必须带着最新 LES 现身。
history.last_epoch_started(PG
历史下界)
history.last_epoch_started 是「PG
作为整体最近一次进入 Active
并接受写」的下界。在某个 OSD
上,它也是「本地 PG log 里出现过写的激活
epoch」的上界——因为接受写之前会先更新它。
由于已提交写会被当时 acting set 全员记下,一旦 peering
能查到回溯到某个已见 history.last_epoch_started
的每个 past interval 中至少一名成员,就可以推断:在 \(\mathrm{MAX}(\mathrm{history.last\_epoch\_started})\)
之后,没有 interval 能向客户端报告写已提交。于是:
\[ \min\{\,\mathrm{last\_update}\mid \mathrm{info.last\_epoch\_started}\ge \mathrm{MAX}(\mathrm{history.last\_epoch\_started})\,\} \]
是对「已向客户端报告提交的写」的上界。这是 past
intervals + LES
一起构成权威历史边界的核心不变量(对照
docs.ceph.com/en/squid/dev/peering/)。
find_best_info
里的工程细节
find_best_info 计算
max_last_epoch_started_found 时会纳入各 peer 的
info.last_epoch_started,避免把「先前 interval
里本可非 divergent、甚至可能已服务过读」的 log 条目标成
divergent。activate() 则用 peer 的 LES
作为「divergent 条目能回退多远」的界。
文档给出的反例:某个 incomplete peer 单独记下了更大的
info.les,而当时 acting set
成员并未记下——若盲目用它抬高
min_last_epoch_started_found,会把本来可选的
Primary 判成 incomplete。因此对 incomplete peer 的
info.les 不参与
min_last_epoch_started_found
的计算。排障时看到「某个 OSD LES 特别大却仍
incomplete」时,要对照官方这条规则,而不是简单「谁 LES
大谁权威」。
last_epoch_clean
last_epoch_clean 是 acting set
对象内容与 log 都完全跟上的最近
epoch(recovery 完成)。PG 可以 Active 但仍 Degraded:此时
LES 相关字段会前进,last_epoch_clean 不动。MGR
判断「PG 是否干净/可均衡」时看 clean,不看 started。
| 字段 | 钉住什么 | 典型误用 |
|---|---|---|
info.last_epoch_started |
本 OSD 激活 interval 边界 | 当成全集群统一时钟 |
history.last_epoch_started |
全集群「已接受写」的下界 | 与 info.LES 混为一谈 |
last_epoch_clean |
无 missing 的干净点 | 用它判断能否服务读(Active 即可服务,clean 另论) |
五、Peering 状态机:interval、past intervals 与 Active
Peering 状态机使用
Boost.Statechart(src/osd/PeeringState.h、PeeringState.cc
@ v19.2.5)。概念锚点见
docs.ceph.com/en/squid/dev/peering/ 与
dev/osd_internals/pg/。
Interval(current / past):一段连续
OSDMap epoch,其间该 PG 的 up set 与 acting set
不变。acting/up
一变,状态机经 Reset 开启新
interval(PG::start_peering_interval)。
Acting vs Up:Up 是当前 epoch 下 CRUSH 算出的集合;Acting 在 backfill 等场景可被 PG temp 临时覆盖(例如新 Primary 为空时,用旧成员顶 Primary 继续服务,同时 backfill 新人)。
状态机简图
stateDiagram-v2
[*] --> Initial
Initial --> Reset: OSD boot or map change
Reset --> Started: RecoveryCtx created
Started --> GetInfo: NeedUpThru check passed
GetInfo --> GetLog: pg_info collected from peers
GetLog --> GetMissing: authoritative log found
GetMissing --> WaitUpThru: missing set computed
WaitUpThru --> Active: up_thru recorded in OSDMap
Active --> Reset: acting/up set change
Active --> Recovering: degraded objects found
Recovering --> Active: recovery advances
关键阶段与 Golden Rule
Peering 的 Golden Rule(官方 Peering 文档):任何写在向客户端确认前提交到 当时 acting set 全体。因此只要能联系到自上次成功 peering 以来每个 past interval acting set 中的至少一名成员,就能构造包含一切已确认写的权威历史。
GetInfo:Primary 用 MQuery
收集 pg_info_t(含
last_update、LES)。Past intervals
回溯下界由已知的 last_epoch_started
截断;若发现更新的 LES,可剪掉更老的
interval,减少必须联系的 OSD。
GetLog / GetMissing:合并权威 log,识别
divergent 条目(未向客户端确认、可丢弃)与 missing
对象。副本池倾向于选最新合法
head,减少客户端重放;EC 池倾向于选最老合法
head,因为单副本不足以重构(log_based_pg/)。
WaitUpThru:Primary 在 OSDMap 里登记
up_thru,证明「在当前 interval
起点之后仍存活并完成
peering」。文档给出的失败序列([A,B]→[A]→[]→[B])说明:没有
up_thru,后续 peering 可能误信从未真正完成过
peering 的 interval。
Active:更新本地
last_epoch_started,并可开始接受写;missing
则并行 recovery。last_epoch_clean
要等对象内容也追上后才更新,此时才可安全清掉 stray
副本。
另有
WaitFlushedPeering(dev/osd_internals/pg/):Primary
必须等上一 interval 的 unstable 事务从 PG op sequencer
刷完,才能 Active——否则可能读到上一 interval 未落稳的
on-disk 状态。
六、missing 集合与 PG 的 Active+Degraded
进入 Active 状态后,Primary 维护一个
pg_missing_t
结构(src/osd/osd_types.h @
v19.2.5)记录哪些对象的哪些版本在哪个 OSD 上缺失。这个集合由
GetMissing 阶段的日志比对生成,在 recovery
过程中逐项清除。
Active+Degraded:PG
可服务读写,但对象副本未齐。写仍按 当前 acting
set(可能含 PG temp)复制并满足
min_size;读默认由 Primary 服务。Up set 是
CRUSH 视图,与 acting 在 backfill/PG temp
时可能不同——不要把「写到全体 up」当成默认路径。
当存活副本数低于 min_size(默认常为
size-1)时,PG 不能进入可写
Active,客户端写被堵住。
pg_query <pgid> 命令(需真实集群或
vstart)可以打印 PG 的详细状态,包括:acting set、up
set、missing
对象列表、当前状态机状态、last_epoch_started、last_epoch_clean
以及 peering 历史。在 peering
卡住的排障场景中,这个命令的输出比 ceph -s
更有价值——前者显示「当前在哪个状态机节点」,后者只显示聚合的
PG 状态计数。
七、小结
- PG 是一致性单元:peering 协商粒度为 PG 数量(通常几百个每 OSD),而非对象数量;PG 数量的选择影响恢复时间与 peering 开销之间的权衡。
- PrimaryLogPG 统一副本池与 EC
池:两者共享 PG log 和 peering 逻辑;EC 池通过
ECBackend插件处理数据路径,但元数据一致性由同一个状态机维护。 - eversion_t 的 epoch 字段是跨 peering
的时间戳:
epoch.version中 epoch 标识 OSDMap 版本,version在 PG 内单调递增;两者组合保证了全序,是日志比对与权威日志选择的基础。 info.LES≠history.LES≠last_epoch_clean:前者是本 OSD 激活边界;history 给出「已接受写」的全集群下界;clean 才表示对象内容无缺失。find_best_info对 incomplete peer 的 LES 还有例外规则。up_thru+ past intervals:保证只信任真正可能完成过 peering 的 interval;挡住幽灵提交与错误权威历史。- waiting_for_active / WaitFlushed:peering 与刷盘未完成时 Op 暂存而非丢弃;客户端表现为延迟升高。
八、谱系、争论与开放问题
谱系
PG log 式 peering 的设计可追溯到 Sage Weil
的博士论文(2007)与 RADOS 论文(PDSW
2007)。论文中定义了「OSD 自治」模型:OSD
不需要中心节点协调才能决定对象的最新版本——通过比较 PG log 和
last_epoch_started,acting set
成员可以自主达成一致。
官方开发者文档
docs.ceph.com/en/squid/dev/osd_internals/ 是
Classic OSD 机制的 A 级来源,特别是
log_based_pg.rst(log 不变量)和
last_epoch_started.rst(epoch
语义)两篇是本文核心依据。
争论:peering 速度 vs 数据安全性
A 侧(快速 peering,容忍短暂
degraded):增大 pg_num、减少
min_size 或调大
osd_recovery_sleep,使 peering
快速完成,系统尽快恢复可写状态。代价是若在 degraded 期间再有
OSD 失效,可能丢数据。
B 侧(严格 min_size,宁可阻塞):保持
min_size = size - 1(或更严格),直到副本足够才服务写请求。代价是故障期间写延迟大幅上升甚至完全阻塞。
这个权衡没有「正确答案」,取决于工作负载(能否容忍写阻塞)和数据重要性(能否容忍丢数据)。在生产中,常见做法是:size=3, min_size=2(允许一个副本缺失时继续写),osd_recovery_sleep
设得足够小以快速恢复,但不设为 0(避免恢复 I/O
拖死前台)。
开放问题
async recovery 的可观测性:Squid v19.2.5 中 async recovery 允许 PG 在后台异步恢复 missing 对象,而不阻塞 Active 状态。但 async recovery 的进度目前在
ceph pg query输出中可见性有限,难以区分「正在恢复但慢」和「卡住了」。改善恢复进度的细粒度可观测性是社区 tracker 上的已知需求。Peering 在大规模 PG 变更下的雪崩:当大量 OSD 同时 down(如机架掉电)时,涉及这些 OSD 的所有 PG 同时触发 peering,MON 的 OSDMap epoch 可能快速递增,进而触发更多 PG 的 peering——形成 peering 雪崩。
osd_max_backfills和 mClock 的recovery_ratio参数是主要缓解手段,但如何在「快速恢复数据安全性」和「不让 peering 雪崩拖死前台」之间自适应调节,仍是开放问题。可读入口:OSDI/FAST 方向的 distributed storage recovery 研究。
九、参考资料
规范 / 官方文档(A)
- Ceph Squid Peering —
docs.ceph.com/en/squid/dev/peering/(interval、up_thru、Golden Rule)。 - Ceph Squid Last Epoch Started —
docs.ceph.com/en/squid/dev/osd_internals/last_epoch_started/。 - Ceph Squid Log Based PG —
docs.ceph.com/en/squid/dev/osd_internals/log_based_pg/。 - Ceph Squid PG Internals —
docs.ceph.com/en/squid/dev/osd_internals/pg/(WaitFlushedPeering)。 - Ceph Squid PG States —
docs.ceph.com/en/squid/rados/operations/pg-states/。
源码(A)
src/osd/PrimaryLogPG.h、src/osd/PrimaryLogPG.cc@ v19.2.5:PG 主类与do_op()入口。src/osd/PeeringState.h、src/osd/PeeringState.cc@ v19.2.5:Boost.Statechart 状态机定义。src/osd/osd_types.h@ v19.2.5:pg_log_entry_t、pg_info_t、pg_missing_t结构定义。src/osd/PGLog.h、src/osd/PGLog.cc@ v19.2.5:PG log 管理与合并逻辑。
论文(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——PG 模型与 OSD 自治存储的原始定义。
- Sage A. Weil. “Ceph: Reliable, Scalable, and High-Performance Distributed Storage.” PhD Thesis, UC Santa Cruz, 2007,§5——peering 协议与 recovery 设计。
站内对照
- OSD 与 Messenger(第 3 篇)(Op 如何进入 PG)
- 客户端写路径(第 6 篇)(Active PG 上的完整写路径)
- Recovery / Backfill / Scrub(第 10 篇)(missing 集合的修复)
实验台账
- 本篇无命令输出;
ceph pg query <pgid>和ceph pg dump可在真实集群或 vstart 上观察 peering 过程,此处不伪造输出。
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】客户端写路径:从 librados 到副本应答与 stale read 边界
追踪一次 RADOS 写从 librados IoCtx 构建 MOSDOp、Objecter 路由、Primary 的 do_op() 执行、副本 MOSDRepOp 应答链,到客户端可读性边界与 stale read 触发条件的完整路径。
【Ceph RADOS】MON/MGR 与 Map:quorum、epoch 传播与客户端可见性
拆解 Ceph MON Paxos quorum、OSDMap/PGMap 结构与 epoch 传播机制;说明 MGR 主备模型与模块框架;分析客户端 epoch 缓存与可见性窗口的边界条件。
【Ceph RADOS】Recovery、Backfill 与 Scrub:修复路径、reservation 机制与 mClock QoS 边界
区分三种修复机制的触发条件与保证边界:recovery 依赖 PG log 修复分歧对象,backfill 做全量对象枚举拷贝,scrub/deep-scrub 主动扫描校验;重点解释 reservation 如何限流、async recovery 如何减少前台阻塞,以及 mClock 调度器对修复流量的控制边界与局限。