SCN(System Change Number)是 pgrac 用于跨节点事务排序的 Lamport 时钟。每个节点独立维护一个 64-bit 的本地计数器;事务提交时原子 +1(spec-1.16);任意收到的 envelope 携带远端 SCN 时通过 CAS-Lamport 取 max 推进(spec-2.4)。这条规则在 pgrac 集群中承担三重职责:为 MVCC 可见性判断提供因果基准、为 WAL commit/abort 记录打上全局时间戳(spec-1.18 xl_scn)、为所有 IC envelope 提供因果排序(spec-2.4 envelope.scn 偏移 20)。
本章按 Stage 标注实装状态。Stage 1(spec-1.15 / 1.16 / 1.17 / 1.18 / 1.19) 已 ship:8-byte 编码 + 3 cmp 函数 + 单节点 advance/observe + commit/abort WAL xl_scn 可选字段 + walwriter BOC tick + WAL page header xlp_thread_id 占位。Stage 2 Phase 2.C(spec-2.4 / 2.9 / 2.10 / 2.11 / 2.12) 把这些 ABI 接入跨节点 IC:envelope.scn piggyback 真激活、LMON-mediated BOC broadcast 真激活、commit_scn cross-instance lookup skeleton、SCN convergence boundary GUC。持久化 + safety_gap 在 SCN 设计文档 §8 描述,但 spec-1.16 §1.3 显式 defer 未实装;当前 crash recovery 通过 spec-1.18 WAL replay 重建 SCN,不依赖独立持久化文件。本章遵循这一边界。
PG 单机用 LSN(Log Sequence Number) 标识 WAL 位置:LSN 是 WAL 文件的字节偏移,在单机环境内单调递增,语义清晰。但 LSN 不携带任何"事务时序"信息——PG 的事务可见性完全由 xmin / xmax 加 CLOG 表决定,LSN 仅用于确定 WAL 持久化位置,与"哪个事务先提交"无关。
在 pgrac 集群中,这一机制暴露出两个根本性的不足:
第一,LSN 跨节点不可比。节点 1 的 WAL 偏移量 0/4A3F8B0 与节点 2 的 0/4A3F8B0 是完全独立的两个值,它们不反映任何因果关系。若节点 2 的读事务要判断节点 1 的某次提交是否在自己的快照之前,仅凭 LSN 无法做到。
第二,xmin / xmax 只在单节点可见。在集群中,一个事务的 XID 分配在提交节点,其他节点并不能仅凭 CLOG 查到它的完整可见性信息——这要求额外的跨节点协调,开销不可忽视。
pgrac 按照 Oracle RAC 的参考设计,引入 SCN 作为集群范围的 Lamport 时钟。每次提交分配一个 SCN,写入 WAL 记录、block 的 pd_block_scn、ITL slot 的 commit_scn;读事务快照用提交时的 SCN 作为可见性基准。这样,任意节点上的读事务只需比较 tuple 的 commit_scn 与自己的快照 SCN,即可在本地完成可见性判断,无需问询提交节点。
LSN 与 SCN 在 pgrac 中共存且职责不重叠:LSN 继续标识 WAL 物理位置(用于 recovery、replication slot、checkpoint 管理);SCN 负责跨节点事务因果排序和 MVCC 可见性判断。两者都写入 WAL record header,但语义完全独立。
pgrac SCN 是一个 **64-bit(8 字节)**无符号整数,分为两个字段:
node_id,取值 0–255,标识产生该 SCN 的节点。集群最多支持 256 个节点。local_scn,该节点的本地 Lamport 计数器值。这一编码有两个重要属性。首先,全局唯一性由构造保证:不同节点的 node_id 不同,因此它们生成的 SCN 高 8 位不同,不可能产生相同的 64-bit 值。其次,时序比较只用低 56 位:可见性判断时比较 local_scn 部分,丢弃 node_id 高位——这是因为高位反映的是来源节点而非因果先后,用高位参与大小比较会污染 Lamport 的 happens-before 语义。
+-------+--------------------------+
| node | local SCN |
| 8 bit | 56 bit |
+-------+--------------------------+
↑ ↑
256 nodes max ~280K years @ 100K events/sec
56 位的本地计数器可表示约 72 千万亿个值。在 OLTP 集群中,每秒约产生 10 万次提交 + 10 万次 piggyback 推进,合计约 20 万 SCN/秒,理论溢出时间超过 280,000 年。这个数字不是设计余量,而是编码选择的直接结果:8 位 node_id 支持 256 节点上限,已被证明对单集群足够,剩余 56 位全部用作计数器。
InvalidScn = 0 是协议保留值,表示"尚未设置"的哨兵,与 PG 的 InvalidTransactionId = 0 约定对齐。所有真实 SCN 值均 ≥ 1,因此零值可以安全用于零初始化的结构体成员。
SCN 按照经典 Lamport 时钟规则推进,pgrac 实现了三条推进路径:
路径 1 — 本地提交:事务提交时,节点对 local_scn 执行原子加一,得到新的提交 SCN,写入 WAL commit record、TT slot、ITL slot。提交 SCN 携带本节点的 node_id 编码,形成完整的 64-bit SCN。
路径 2 — 接收外部 SCN(BOC 或 Piggyback):收到来自远程节点的 SCN 时,节点执行 local_scn = max(local_scn, remote.local_scn) + 1。这个 max+1 操作确保本节点此后产生的所有 SCN 在因果上晚于收到的消息——这正是 Lamport 的 happens-before 保证。
路径 3 — WAL 写入时 stamp xl_scn:WAL 记录插入时,读取当前 local_scn 并写入 record header 的 xl_scn 字段。这一操作不推进 local_scn,只是快照当前值;推进只发生在提交和 piggyback。
Node 1: ─●────●─────────────●───── (commit @ 42)
↘ BOC(43)
Node 2: ───────●●──────────●────── (recv → max(12,43)+1 = 44)
↘ BOC(44)
Node 3: ────────────●─────●─────── (recv → max(8,44)+1 = 45)
Lamport 规则给出的是因果一致性(causal consistency),不是全序。两个并发事务(没有因果关联的事务)的 SCN 可以无法比较先后,但这在 MVCC 可见性判断中是允许的——快照隔离的正确性不依赖于并发事务的全序,只要求因果事件有序。
跨节点 SCN 比较有两种语义,各有其专属 API:时序比较(visibility 路径)只比较低 56 位的 local_scn,对应 scn_time_cmp();全序比较(ITL slot 排序、deadlock 检测)包含高 8 位 node_id,对应 scn_total_cmp();Recovery merge 比较在同 local_scn 时加入 LSN + node_id 的二级 tie-break,对应 scn_recovery_cmp()。业务代码禁止对 SCN 值进行裸 uint64 大小比较。
SCN 在集群中通过两条互补路径传播。当前 Stage 2 实装的术语与早期 SCN 设计文档(2026-04-25 v1.0)有出入:所谓的"piggyback in CF / GES messages"在 Stage 2 实际由 IC envelope 帧级 piggyback(spec-2.4)承担。
Envelope Piggyback(spec-2.4 实装态):每条 IC envelope 在偏移 20 处携带 8 字节 scn 字段,发送端 build 时填入 cluster_scn_current()。接收端经过 framing + epoch + CRC 校验通过后调用 cluster_ic_envelope_observe_scn() → cluster_scn_observe(),按 Lamport ≥ 严格边界做 CAS-bump(spec-2.4 §2.7,spec-1.16 lock-free CAS retry loop)。这条路径不引入额外消息——任何跨节点流量(heartbeat / GES / SCN broadcast / sinval / reconfig)都自动驱动 SCN 收敛。
BOC(Broadcast on Commit,spec-2.9 + spec-2.10):walwriter 每个 tick 调 cluster_scn_boc_tick() 刷新 boc_sweep_count(spec-1.17);LMON 主循环(默认 1 s)调 cluster_scn_lmon_drain_boc_broadcast(),把累积的 sweep coalesce 为 1 个 PGRAC_IC_MSG_BOC_BROADCAST=3 帧 fanout 给所有 alive peer(spec-2.9)。BOC 的真实 wire cadence 受 LMON tick 上限约束,约 1 fanout/s/peer。
GUC cluster.boc_sweep_interval_ms(PGC_SIGHUP,default 100 ms,range 1..1000,spec-2.10 D1)控制 walwriter sweep 节流间隔。Path C 5-spec plan 收口前 default 为 1 ms(spec-1.17 v0.2),spec-2.10 production-sane 调整为 100 ms——降的是 walwriter wake / shmem atomic churn,不是 IC bandwidth(IC fanout 仍由 LMON tick 限速 ~1/s)。SCN 设计文档 §6.4 写的"100 μs flush"是 Stage 1 早期 GUC 默认,不再适用于当前 Stage 2 production-sane 默认。
| 机制 | 推进方式 | 实装 spec | 实测 wire 频率 |
|---|---|---|---|
| Envelope Piggyback | 每帧 envelope.scn 偏移 20 | spec-2.4 D4 | 跟随所有 IC 流量(heartbeat / GES / SCN broadcast / sinval / reconfig) |
| BOC fanout | walwriter sweep → LMON drain → IC fanout | spec-1.17 + spec-2.9 + spec-2.10 | ~1 fanout/s/peer(LMON tick 限速) |
spec-2.10 §0 Q5 已显式纠正一个早期误认知:"1 ms BOC sweep ≠ 1000 BOC/s on wire"——真正的 wire cadence 是 MIN(walwriter sweep, LMON tick) ≈ LMON tick ~1/s。BOC 的价值不在高频,而在当集群无任何其他跨节点流量时(idle peer),仍能驱动 SCN 收敛;envelope piggyback 是繁忙期主力。
4-层 observability chain(spec-2.10 Q2.2):(1) walwriter sweep scn_boc_sweep_count →(2)LMON fanout scn_boc_broadcast_fanout_count →(3)接收端 CAS-bump scn_observe_bump_count →(4)per-peer lamport_observe_advance_count(spec-2.4 D10)。这四个计数器从 pg_cluster_state 'scn' category + pg_cluster_ic_peers view 暴露。
xl_scn Optimization在高并发写入场景下,如果每个 backend 在 WAL 插入时都争抢同一个全局 local_scn 原子计数器,会产生明显的 cache-line bouncing 热点。pgrac 通过 per-thread xl_scn 优化规避这一问题。
spec-1.18 把 xl_scn 实装为 commit / abort WAL record 的可选字段:在 cluster.enabled=on 且 SCN_VALID(commit_scn) 时,XactLogCommitRecord / XactLogAbortRecord 写入 XACT_XINFO_HAS_SCN(bit 9)flag + 8 字节 xl_xact_scn(紧贴 xl_xact_origin 后)。Recovery 时 ParseCommit/AbortRecord memcpy unaligned 解析(HC2)后调 cluster_scn_recovery_replay_observe()。XLOG_XACT_PREPARE 不携带 xl_scn(PREPARE 不是 durable commit point,spec-1.16 Q5);COMMIT_PREPARED / ABORT_PREPARED 携带(真 durable point)。
Bootstrap mode commit 与 cluster.enabled=off 路径 commit_scn 返回 InvalidScn,XactLogCommitRecord 看到 InvalidScn 不写 HAS_SCN flag——这一路径与 vanilla PG 16 WAL byte-identical,保 initdb / pg_upgrade 兼容性。
SCN 设计文档 §5.5 描述的 "WAL stamp xl_scn on every record header" 是 AD-008 第二次扩展的设计目标(unconditional 32 B header),实装时 spec-1.18 选择更保守的 optional-flag 路径:record header on-disk 布局不变,xl_scn 仅在需要时插入。local_scn 的真正推进只发生在 cluster_scn_advance_for_commit()(commit/abort hot path,spec-1.16 + spec-1.17 lock-free atomic fetch_add)和 cluster_scn_observe()(envelope verify 路径 + WAL replay observe 路径)两个串行化点。多个并发 backend 写 WAL 时各自原子 load current_local_scn,互不竞争。
local_scn 存储在节点的 shared memory,是易失数据。若节点在 local_scn = N 时崩溃,重启后必须避免从 0 重新累加——否则给出比已提交事务更小的 SCN,违反单调性。
Stage 1 当前实装(spec-1.18 路径):crash recovery 通过 WAL replay 重建 SCN。XLOG_XACT_COMMIT / XLOG_XACT_ABORT / XLOG_XACT_COMMIT_PREPARED / XLOG_XACT_ABORT_PREPARED 四种 WAL record 在 cluster.enabled=on 且 commit_scn 真分配时携带 XACT_XINFO_HAS_SCN flag(bit 9)+ 8 字节 xl_xact_scn(spec-1.18 D2-D4)。Recovery 时 xact_redo_commit/abort 在 parsed.xinfo & XACT_XINFO_HAS_SCN 时调 cluster_scn_observe(parsed.scn),按 Lamport ≥ 推进 current_local_scn。整个 WAL replay 完成后,current_local_scn 不会小于崩溃前任何已 fsync 提交的 SCN。
SCN 设计文档 v1.0 §8(2026-04-25)描述了三个独立机制:(1) 每节点 100 ms 周期 fsync 到 pg_scn/instance_N.scn 文件;(2) 重启时读取持久化值 P 并起始于 P + safety_gap(默认 1,000,000);(3) shutdown / checkpoint 强制持久化。这三个机制目前都未在任何 spec 实装。spec-1.16 §1.3 明文 defer:"持久化 local_scn 到 pg_control / control file ⋯ 本 spec 范围不含;spec-1.16+(待立)做。当前 crash → restart 后 local_scn = 0 + 重新累加。" spec-1.18 §3 改用 WAL replay 路径重建,取代独立持久化文件。safety_gap = 1,000,000 与 pg_scn/ 目录是设计目标,不是 Stage 1 实装事实。
Stage 2 Phase 2.C("Path C 5-spec plan")以 spec-2.9 → 2.10 → 2.11 → 2.12 四个 sub-spec 收尾 SCN 子系统。本节列出当前可观测面与 API surface。
spec-2.11 commit_scn cross-instance lookup skeleton:引入 ClusterScnLookupResult enum(FOUND=0 / DEFER=1 / NOT_FOUND=2 / ERROR=3)+ cluster_scn_lookup_commit_remote(xid, *out_commit_scn) API。Skeleton 阶段 stub 永远 return DEFER 并递增 scn_commit_lookup_defer_count。Caller 看到 DEFER 时必须 fall back 到 PG-native visibility 路径——禁止把 DEFER 当作 INVISIBLE。真激活路径推迟到 spec-2.26 dual-dim visibility entry / Stage 3。AD-012 例外 9 invariant 严守:heapam_visibility.c 0 PGRAC modifications。
spec-2.12 SCN convergence boundary verification:新增 GUC cluster.scn_max_propagation_lag_ms(PGC_SIGHUP,default 5000 ms,range 100..60000,GUC_UNIT_MS)+ 2 个 shmem 字段(last_observe_at + observed_max_observe_gap_ms,atomic uint64 lock-free)+ 3 个 SQL 暴露行(scn_last_observe_at / scn_seconds_since_last_observe / scn_observed_max_observe_gap_ms)。Skeleton 阶段仅 measure,不 enforce:超出 bound 不触发 WARNING / FATAL / overrun counter。TAP 102_scn_convergence_bound_2node.pl 通过 3 round × 双向 = 6 measurements 真测 propagation latency < GUC 实际值。
本章建立了 SCN 的概念框架:LSN 跨节点不可比是引入 Lamport 时钟的根本动机;8 字节编码将 node_id(高 8 位)与 local_scn(低 56 位)分离,前者保证全局唯一、后者承载因果序;三条推进路径(commit atomic +1、observe CAS max+1、WAL stamp 只读)覆盖所有 SCN 写入场景;envelope piggyback(spec-2.4,每帧偏移 20)是繁忙期主力、LMON-mediated BOC fanout(spec-2.9 + 2.10)是 idle 兜底;per-thread xl_scn 把 WAL 热路径降为原子 load;crash recovery 通过 spec-1.18 WAL replay 路径重建 SCN,不依赖独立持久化文件——SCN 设计文档 §8 的 safety_gap / pg_scn/ 仍是设计目标而非实装事实。
深度协议细节请参阅以下资源:
scn_encode / scn_time_cmp / scn_recovery_cmp)、BOC 批量 flush 时序、持久化文件布局、Reconfig freeze 期间的 SCN 处理、可见性路径与 WAL k-way merge 的完整算法msg_scn piggyback 字段;block 的 pd_block_scn 与 ITL commit_scn 字段定义Chapter 5 — Reconfiguration 介绍节点拓扑变化时的集群状态重建:节点离开或加入时,SCN 如何在 freeze / unfreeze 阶段维持单调性,故障节点的 local_scn 如何由 WAL recovery 的 xl_scn 重建,以及新节点加入时通过 KEEPALIVE + piggyback 完成 SCN 收敛的完整流程。