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

【Ceph RADOS】PG 与 Peering:PrimaryLogPG、PG log 与状态机

文章导航

分类入口
storagedistributed
标签入口
#ceph#rados#pg#primarylogpg#peering#pg-log#last-epoch-started#osd-internals#squid#v19.2.5

目录

PG(Placement Group,放置组)是 RADOS 的一致性单元——不是对象,也不是 OSD,而是介于二者之间的调度边界。一次写操作从客户端到达时,Primary OSD 需要知道该 PG 当前是否 Active(能服务请求);若不是,Op 在本地等待,不返回错误。理解 PG 状态机与 peering 是「写为什么堵住了」的核心问题。

本篇聚焦三件事:PG log 的精确格式与不变量、last_epoch_startedlast_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:日志式复制的核心

PrimaryLogPGsrc/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/):

  1. last_update:PG log 中最新条目的 version;Primary 处理写后立即更新。
  2. last_complete:所有 acting set OSD 都已应用到的最新 version;last_complete <= last_update
  3. log_tail:PG log 保留的最老条目 version;比 log_tail 更老的操作可能没有日志记录,需要 backfill 而非 log-based recovery。

当两个 OSD 的 PG log 都不包含某个对象的差异区间时(即需要比较的区间超过了双方的 log 范围),peering 退化为 backfill(全量对象比对),代价大幅上升。


四、pg_info_tinfo.last_epoch_startedhistory.last_epoch_started

pg_info_tsrc/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\),使得:

因为「已提交的写永不 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.Statechartsrc/osd/PeeringState.hPeeringState.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 副本。

另有 WaitFlushedPeeringdev/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_startedlast_epoch_clean 以及 peering 历史。在 peering 卡住的排障场景中,这个命令的输出比 ceph -s 更有价值——前者显示「当前在哪个状态机节点」,后者只显示聚合的 PG 状态计数。


七、小结

  1. PG 是一致性单元:peering 协商粒度为 PG 数量(通常几百个每 OSD),而非对象数量;PG 数量的选择影响恢复时间与 peering 开销之间的权衡。
  2. PrimaryLogPG 统一副本池与 EC 池:两者共享 PG log 和 peering 逻辑;EC 池通过 ECBackend 插件处理数据路径,但元数据一致性由同一个状态机维护。
  3. eversion_t 的 epoch 字段是跨 peering 的时间戳epoch.version 中 epoch 标识 OSDMap 版本,version 在 PG 内单调递增;两者组合保证了全序,是日志比对与权威日志选择的基础。
  4. info.LEShistory.LESlast_epoch_clean:前者是本 OSD 激活边界;history 给出「已接受写」的全集群下界;clean 才表示对象内容无缺失。find_best_info 对 incomplete peer 的 LES 还有例外规则。
  5. up_thru + past intervals:保证只信任真正可能完成过 peering 的 interval;挡住幽灵提交与错误权威历史。
  6. 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 拖死前台)。

开放问题

  1. async recovery 的可观测性:Squid v19.2.5 中 async recovery 允许 PG 在后台异步恢复 missing 对象,而不阻塞 Active 状态。但 async recovery 的进度目前在 ceph pg query 输出中可见性有限,难以区分「正在恢复但慢」和「卡住了」。改善恢复进度的细粒度可观测性是社区 tracker 上的已知需求。

  2. 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)

源码(A)

论文(A)

站内对照

实验台账


上一篇:CRUSH 加深 · 下一篇:客户端写路径

读完这篇,下一步读什么

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


By .