当集群成员关系发生变化——某个节点离开、加入,或网络分区将集群切分为两半——pgrac 需要在毫秒到秒级内重建全局共享状态,才能继续提供服务。这个过程称为 Reconfiguration,是 RAC 架构可用性设计中最复杂、代价最高的环节。
Stage 2 当前实现的是 Reconfiguration 的最小闭包:CSSD heartbeat 检测节点故障(spec-2.5)→ voting disk quorum 仲裁多数派(spec-2.6)→ fence-lite 自隔离少数派(spec-2.28)→ reconfig coordinator 推进 epoch 并广播 ProcSignal(spec-2.29)→ backend 在 ProcessInterrupts 中 fail-closed。完整的 Drain / Quiesce / Commit / Resume 四阶段状态机推迟到 spec-2.31;产品级硬件 fencing(STONITH / SCSI-3 PR)推迟到 Stage 6 production hardening。本章按当前 Stage 2 设计描述。
本章引用的所有数字、GUC、SQLSTATE、视图名都来自 Stage 2 spec 原文(spec-2.5 / 2.6 / 2.28 / 2.29)。Oracle 风格的 misscount=30s、disktimeout=200s、三路心跳、SCSI-3 PR / STONITH、IMR (Instance Membership Recovery) 等术语不属于 pgrac Stage 2 概念,本章不使用。
Reconfiguration 由 CSSD(Cluster Synchronization Service Daemon)在检测到集群成员关系变化后触发。触发场景分四类,但都走同一条最小闭包路径:
| 场景 | 触发来源 | 典型耗时 | 备注 |
|---|---|---|---|
| 节点计划退出 | 节点主动通告 CSSD | < 1 秒 | graceful,无需 fence |
| 节点 crash / 进程死亡 | CSSD dead 检测(3 × 1s = 3 秒默认) | 3–5 秒 | fence-lite 自隔离 + reconfig coordinator epoch++ |
| 网络分区 | CSSD heartbeat 丢失 + voting disk quorum 决议 | 5–10 秒 | 赢得 quorum 一侧继续;落败一侧自动 fail-closed |
| 节点 hang / IO 卡死 | CSSD dead (3s) + voting disk lease 过期 (2 × poll_interval = 4s 默认) | 5–10 秒 | lease 过期触发 quorum_state → LOST |
网络分区场景下两侧都认为对方失联,这正是 split-brain 问题的根源。pgrac 通过两层防护解决:第一层是 voting disk quorum(§5.3.2),只有持有多数票的一侧 cluster_qvotec_in_quorum() 返回 true;第二层是 fence-lite(§5.5.1),落败一侧的 ClusterFenceFreezePending 标志位被设置,所有 in-flight 事务在 ProcessInterrupts 中 fail-closed。两层冗余:任一生效即可防止少数派写入。
Reconfiguration 的概念模型仍是经典的 Freeze / Rebuild / Thaw 三阶段,但 Stage 2 的实现是"最小闭包":spec-2.28 提供 Freeze / Thaw 信号,spec-2.29 提供 epoch 推进与 coordinator 选举,完整的多阶段状态机(drain / quiesce / commit / resume)推迟到 spec-2.31。
Freeze(冻结)通过两条独立路径生效,互为冗余:
cluster_qvotec_in_quorum(),若返回 false 则在 commit 边界 ereport(ERROR, 53R40 ERRCODE_CLUSTER_QUORUM_LOST)。这是权威的 fail-closed 谓词。quorum OK→LOST 后立即广播 PROCSIG_CLUSTER_FREEZE_WRITES;每个 backend 在 ProcessInterrupts 中读取 ClusterFenceFreezePending 标志位,若 IsTransactionState() 为真则 ereport(ERROR, 53R50 ERRCODE_CLUSTER_QUORUM_LOST_BACKEND)。这条路径只是减少了正在等锁的 backend 的延迟,不是权威判定。Rebuild(重建)的实质是 epoch 推进 + ProcSignal 广播:reconfig coordinator(§5.2.1)通过原子 CAS 把 cluster_epoch 从 N 推进到 N+1,并向所有 in_quorum 幸存者的本节点 ProcArray 中广播 cluster_reconfig_start_pending。每个 backend 在 ProcessInterrupts 中读取该标志,若仍 in_quorum 则 ereport(ERROR, 53R60 ERRCODE_CLUSTER_RECONFIG_IN_PROGRESS)(事务可重试);若已 quorum lost 则 53R50 优先。
Thaw(解冻)是信息性的:PROCSIG_CLUSTER_THAW_WRITES 只更新 last_thaw_at_us 时间戳,不清除 ClusterFenceFreezePending,不改变 cluster_qvotec_in_quorum() 的返回值。commit-gate 始终是权威;Thaw 仅供 LMON 协调与运维监控使用。
T0 ─── T1 ───── T2 ──────── T3
│ │ │ │
CSSD qvotec coordinator in_quorum
peer detects picks min restored
DEAD quorum survivor (Thaw is
LOST +epoch++ informational)
+PROCSIG
broadcast
│ │
↓ ↓
freeze reconfig
signal signal
53R40 53R60
|<-- 当前 Stage 2 最小闭包,3-5s 典型 -->|
spec-2.29 把 reconfig 的协调逻辑实现为一个无状态确定性函数 cluster_reconfig_lmon_tick(),在 LMON daemon 的每个 tick(cluster.lmon_tick_interval_ms 默认 100 ms)中调用。它不引入新的 aux 进程、不持有状态、可被多次重入无副作用——故障恢复后下一个 tick 即可重新计算。
每个 tick 的执行步骤:
cluster_qvotec_in_quorum() == false,本节点不参与协调,返回。dead_bitmap(16 字节,最多 128 节点)。若 bitmap 为 0,无 peer 死亡,返回。alive_set(state ∈ {ALIVE, SUSPECTED})和 survivor_set = alive_set & ~dead_bitmap(加上 self 若 in_quorum)。coordinator_node_id = lowest_bit_set(survivor_set)。规则:min(survivor_set),约束于 cluster_qvotec_in_quorum() == true。event_id = siphash2_4(dead_bitmap || cssd_dead_generation)。若等于上次已应用的 event_id,去重跳过。cluster_reconfig_broadcast_local_procsig(),向本节点 ProcArray 中所有 backend 广播 PROCSIG(survivor-broadcast symmetry,I7)。cluster_reconfig_apply_epoch_bump_as_coordinator():原子 CAS new_epoch = old_epoch + 1,记录 changed_at_lsn,发布 observer_role = 'coordinator' 事件。observer_role = 'survivor' 事件(其 epoch 稍后通过 IC envelope piggyback 收敛)。event_id 用 SipHash-2-4 而非简单 hash,是因为同一 dead bitmap 可能因"死 → 复活 → 再死"反复出现,cssd_dead_generation(CSSD 每次状态翻转推进一次的单调计数器)作为第二维消歧。old_epoch 故意不进 hash 输入,避免 self-bump 循环。
Epoch 传播:每条 IC envelope(spec-2.3 格式)的 epoch 字段(偏移 12,8 字节)由发送端写入 cluster_epoch_get_current(),接收端经过 CRC + 认证校验后调用 cluster_epoch_observe_remote()。单次观察的跳跃幅度受 CLUSTER_EPOCH_OBSERVE_MAX_JUMP(默认 16)限制,防止恶意/损坏帧造成 epoch 飙升。
视图 pg_cluster_reconfig_state 永远返回 1 行(P2.9 契约),从未触发时 event_id = 0 / observer_role = 'none' / applied_at IS NULL。9 列:event_id / coordinator_node_id / old_epoch / new_epoch / dead_bitmap / applied_at / observer_role / event_seq / cssd_dead_generation。
spec-2.29 故意不实现显式的多阶段状态机。Q1 把 Stage 2 的目标定义为"最小闭包":CSSD DEAD → LMON 确定性协调 → epoch++ → PROCSIG 广播 → ProcessInterrupts fail-closed。Drain / Quiesce / Commit / Resume 四阶段状态机推迟到 hypothetical spec-2.31。Stage 2 的 ClusterReconfigState 只保留最后一次已应用事件(CLUSTER_RECONFIG_MAX_EVENT_HISTORY = 1),事件历史 ring buffer 也是 spec-2.31+ 的范围。
CSSD 是 Reconfiguration 链路的起点。pgrac 实现两层 dead 检测:socket-level(spec-2.4,TCP keepalive,最坏 120 秒)+ application-level(spec-2.5,CSSD heartbeat,默认 3 秒 dead 检测)。CSSD 本身只声明状态(写 LOG、计数器、视图行),不直接触发 reconfig;reconfig 由 §5.2.1 的 coordinator 在下一次 LMON tick 中决策。
| 层 | 实现 | 典型 dead 检测时长 | 责任 |
|---|---|---|---|
| Socket-level(kernel) | TCP keepalive:SO_KEEPALIVE + TCP_KEEPIDLE/INTVL/CNT | 最坏 60s idle + 6 × 10s probe = 120 秒 | 对端 close / 链路断 → EPIPE / ECONNRESET → reconnect |
| Application-level(CSSD) | 每 cssd_heartbeat_interval_ms 广播一次 heartbeat envelope(msg_type 11,payload 12 字节) | 3 × 1000 ms = 3 秒(默认) | shmem 状态 + LOG / WARNING + 视图行;不触发 reconfig |
CSSD 守护进程(aux #5,位于 LMON / LCK / DIAG / Stats 之后)维护每个 declared peer 的三态状态机:ALIVE → SUSPECTED → DEAD。任何 recv 立即把状态回退到 ALIVE(hysteresis 恢复)。CSSD 不持有 TCP fd——它写入 shmem 出站队列,由 LMON 通过 tier1 IC transport 发送。
| GUC | 默认值 | 范围 | 说明 |
|---|---|---|---|
cluster.cssd_main_loop_interval_ms | 1000 | 100–60000 | CSSD 主循环节拍 |
cluster.cssd_heartbeat_interval_ms | 1000 | 100–10000 | 心跳广播间隔 |
cluster.cssd_dead_deadband_factor | 3 | 2–10 | dead 阈值 = factor × interval |
状态转换的时间数学:
suspected_factor = max(2, deadband_factor - 1) = 2(默认)ALIVE → SUSPECTED:2 × interval = 2000 ms 无 recvSUSPECTED → DEAD:deadband_factor × interval = 3 × 1000 = 3000 ms 无 recvready_at 开始;宽限期内的 deadband 扫描被抑制CSSD 视图 pg_cluster_cssd_peers 暴露每个 peer 的当前状态、last recv 时间、累计 recv 计数。SQLSTATE:53R32 ERRCODE_CLUSTER_CSSD_PEER_SUSPECTED(LOG)/ 53R33 ERRCODE_CLUSTER_CSSD_PEER_DEAD(WARNING,但不触发 reconfig——reconfig 由 coordinator 在下一次 LMON tick 中决策)。
spec-2.6 引入独立守护进程 cluster_qvotec(Quorum Voting Coordinator,aux #6),负责仲裁。Voting disk 的 slot 设计:每个 instance 占恰好 512 字节、扇区对齐(但不声明扇区原子写),通过 generation counter + CRC32C 检测撕裂写。
| GUC | 默认值 | 范围 | 说明 |
|---|---|---|---|
cluster.voting_disks | ""(空 = qvotec 禁用) | CSV 路径列表,1–5 个 | 3 个为推荐默认部署值 |
cluster.quorum_poll_interval_ms | 2000 | 500–30000 | qvotec 轮询周期 |
cluster.voting_disk_io_timeout_ms | 5000 | 1000–60000 | 单次盘 IO 超时 |
cluster.voting_disk_size_bytes | 65536(64 KB) | 4096–1048576 | 每盘 slot 区域大小 |
Quorum 计算:quorum_size = (N/2) + 1,N = cluster.voting_disks 数量。节点 in_quorum 的条件:
disks_ok_count >= (disks_total_count / 2) + 1
AND
alive_bitmap_count >= (cluster_node_count / 2) + 1
四种 quorum 状态:INITIALIZING / OK / UNCERTAIN / LOST。fail-closed 语义——任何非 OK 状态在 commit 边界都会触发 53R40 / 53R41。
Lease 防御机制:cluster_qvotec_in_quorum() 仅在状态 == OK 且 now < lease_expire_at_us 时返回 true。lease = last_poll_ts + 2 × poll_interval。这条 lease 保证了即使 qvotec 进程卡死,backend 仍能在 4 秒(默认)内识别到 quorum 已不可信并 fail-closed。
失败场景:
disks_ok_count == 0 → quorum_state = LOST → fail-closed0 < disks_ok_count < majority → quorum_state = UNCERTAIN → fail-closeddisk_io_failure_inflight,下个 cycle 重试,仍可通过其他盘形成 quorumself.incarnation > slot.incarnation)→ self FATAL with 53R43SQLSTATE:53R40 CLUSTER_QUORUM_LOST(commit 边界)/ 53R41 CLUSTER_QUORUM_UNCERTAIN(轮询 inflight)/ 53R42 CLUSTER_VOTING_DISK_IO_FAILURE(EIO / EOF / CRC 不匹配)/ 53R43 CLUSTER_NODE_ID_COLLISION。
视图:pg_cluster_quorum_state(7 列)+ pg_cluster_voting_disks(每盘一行)。
Rebuild 阶段最复杂的子任务是合并故障节点的 WAL redo。在 pgrac 集群中,每个节点维护独立的 WAL stream(pg_wal_node_N/);故障时,故障节点上已持久化但尚未广播给所有节点的 WAL record 需要被存活节点读取并按正确的 SCN 顺序合并回放,才能确保 GRD 中的 block 状态、PI 链、ITL slot 与 WAL 一致。
为什么不是简单串行顺序:每个节点的 WAL stream 只在本节点内单调递增;不同节点的 SCN 是交叉分布的,不存在"先 replay 节点 1 再 replay 节点 2"这样的天然顺序。若按节点顺序串行回放,会破坏跨节点事务的因果关系——例如节点 2 的某次写依赖于节点 1 的前一次写,必须保证 replay 时后者先于前者。
Merged Redo Apply 的正确做法:对所有存活节点(以及从共享存储中读取的故障节点 WAL)的 stream 进行 k-way merge,按 commit_scn(低 56 位 local_scn 部分)排序,得到全局因果有序的 replay 序列。同 SCN 的 tie-break 使用 LSN + node_id 二级排序(对应 scn_recovery_cmp() API,详见 Chapter 4 §4.3)。合并后的序列按顺序逐条 replay,保证 GRD 重建结果与单节点串行执行语义等价。
Node 1 stream: ─●─●─●─●──────●─ (SCN: 42, 43, 50, 51, 61)
Node 2 stream: ─●─●─●────●─●─── (SCN: 12, 44, 45, 55, 60)
Node 3 stream: ─●─●───────────── (SCN: 8, 47)
↓ 按 SCN 排序合并 ↓
Merged: ●─●─●─●─●─●─●─●─●─● (SCN: 8, 12, 42, 43, 44, 45, 47, 50, 51, 55, 60, 61)
replay 顺序
PI 链合并是 Merged Redo Apply 的配套操作:每个 block 在多节点间可能存在多版本 Past Image;Rebuild 阶段在重放 redo 的同时,将故障节点持有的 PI 合并进存活节点的 PI 链,维持 GRD 中 PI 链的完整性,确保后续的 Cache Fusion block transfer 能正确服务旧版本读请求。
Merged Redo Apply 的总工作量取决于故障节点 WAL 的积压量(即从上次 checkpoint 到故障时刻产生的 WAL 量)和 PI 链的长度。在增量重建路径中(spec-2.31+ 范围),只需 replay 故障节点 master 资源对应的 WAL,大幅缩短 Rebuild 阶段耗时。
已提交事务零丢失是 pgrac Reconfiguration 最核心的承诺。任何 Freeze 前已提交的事务,其 WAL record 已持久化到共享存储,在 Rebuild 的 Merged Redo Apply 阶段必然被 replay。Freeze 期间在 commit 边界的事务被 cluster_qvotec_in_quorum() 阻断并 ereport(53R40),事务回滚但数据未污染;in-flight 事务通过 PROCSIG_CLUSTER_FREEZE_WRITES + ClusterFenceFreezePending 标志位被中断(53R50);正在等 reconfig 的事务收到 53R60(retry-safe)。
split-brain 防护通过两层叠加:
第一层 voting disk quorum:集群分区后,只有持有多数票((N/2)+1)的一侧 cluster_qvotec_in_quorum() 返回 true;落败一侧自动 quorum_state → LOST 并触发 fence-lite。
第二层 fence-lite(§5.5.1):PROCSIG_CLUSTER_FREEZE_WRITES 广播 + ClusterFenceFreezePending 标志位 + ProcessInterrupts 检查。任一生效即足以阻止少数派写入共享存储。
spec-2.28 实现 fence-lite(自隔离)。当 LMON 检测 quorum_state 由 OK 转 LOST 时立即广播 freeze(无 grace)。机制由三个组件构成:
procsignal.h 中早已预声明(自 Stage 0.15+,2024 年 10 月),spec-2.28 仅激活处理函数,无 ABI 破坏。volatile sig_atomic_t 标志位,在信号处理函数中信号安全地写 1。cluster_fence_check_interrupts() 在 ProcDiePending 之后、QueryCancelPending 之前;遵循读取-清零-决策模式:先清零 ClusterFenceFreezePending,然后若 cluster_freeze_writes_enabled 启用且 IsTransactionState() 为真,则 ereport(ERROR, 53R50);idle backend 静默吸收。Thaw 是信息性的(PROCSIG_CLUSTER_THAW_WRITES):处理函数更新 last_thaw_at_us,不清除 ClusterFenceFreezePending,不改变 cluster_qvotec_in_quorum() 的返回值。commit-gate 始终是权威 fail-closed 谓词(Invariant I2)。
| GUC | 默认值 | 说明 |
|---|---|---|
cluster.self_fence_enabled | on | 是否启用 self-fence 升级路径 |
cluster.self_fence_grace_ms | 30000 | self-fence grace 窗口 |
cluster.freeze_writes_enabled | on | 是否启用 ProcessInterrupts in-flight abort 路径 |
cluster.fence_audit_log | log | off / log / debug |
Self-fence 升级:LMON 设置 self_fence_requested_at_us = now 之后,postmaster 在 ServerLoop tick 中调用 cluster_fence_postmaster_check();若 now - requested_at_us >= self_fence_grace_ms × 1000,postmaster 自我 SIGINT(走 PG 原生 fast-shutdown 路径,不是硬件重启)。
视图 pg_cluster_fence_state(8 列单行):last_freeze_at / last_thaw_at / self_fence_pending / self_fence_grace_remaining_ms / freeze_broadcast_count / thaw_broadcast_count / self_fence_initiated_count / freeze_signal_received_count。
fence-lite 不包含:外部 cluster.fence_command shell GUC、peer-fence actor(杀远程节点)、IPMI / iLO / vSphere kill peer 集成、SCSI-3 PR / 硬件 fencing、pgracd 监督守护进程。STONITH 与硬件级 fencing 推迟到 Stage 6 production hardening(spec-2.0 Q-C 已锁定 Stage 2 最小不变量 = quorum-lite + fence-lite + fail-closed)。Stage 2 的 fence-lite 足以实现"已提交事务零丢失 + split-brain 防护",因为 quorum 失败 + commit-gate fail-close 是双重保险。
未提交事务的处理:Reconfiguration 完成后,故障节点上所有未提交事务(xmin 未提交、CLOG 未标记 committed)均被视为已中止;存活节点在 Rebuild 期间完成这些事务的回滚,在 Thaw 后新快照中它们对所有节点不可见。
初版实现中,跨 Reconfiguration 的长事务收到 53R60 后回滚(retry-safe,可重试一次)。Oracle 11g+ 支持长事务在 Reconfiguration 后继续,这一能力在 pgrac 中是 spec-2.31+ 的优化目标。当前 Stage 2 范围内,跨 Reconfiguration 的事务一律回滚。
故障演练(kill -9):在维护窗口内,向单节点发送 kill -9 $postmaster_pid 是最直接的 Reconfiguration 演练方式。CSSD 默认 1s × 3 = 3s 内会把 peer 标为 DEAD;如需更快演练,可在测试集群将 cluster.cssd_heartbeat_interval_ms 降至 200 ms(dead 阈值随之降为 600 ms)。演练后查询:
SELECT event_id, coordinator_node_id, old_epoch, new_epoch,
applied_at, observer_role
FROM pg_cluster_reconfig_state;
SELECT * FROM pg_cluster_fence_state;
SELECT * FROM pg_cluster_quorum_state;
SELECT * FROM pg_cluster_cssd_peers;
确认 new_epoch = old_epoch + 1、observer_role 为 coordinator 或 survivor、self_fence_initiated_count 与 freeze_signal_received_count 一致。
Reconfiguration 频次监控:非计划 Reconfiguration 频次异常(例如 1 小时内超过 3 次)是网络抖动、存储 I/O 抖动或 backend hang 的早期信号。pg_cluster_reconfig_state 当前只保留最后一次事件(CLUSTER_RECONFIG_MAX_EVENT_HISTORY = 1),事件历史 ring buffer 在 spec-2.31+ 范围。运维侧应通过 event_seq 单调推进的差值监控频率。
应用层 retry:客户端在 brownout 期间收到的错误码及处理策略:
| SQLSTATE | 含义 | 客户端动作 |
|---|---|---|
| 53R40 CLUSTER_QUORUM_LOST | commit 边界 quorum 失效 | 事务已回滚,等待 quorum 恢复后重试 |
| 53R50 CLUSTER_QUORUM_LOST_BACKEND | in-flight 被 fence-lite 中断 | 同上,retry-safe |
| 53R60 CLUSTER_RECONFIG_IN_PROGRESS | reconfig 期间 in-flight 事务 | 立即可重试,通常单次成功 |
建议 exponential backoff retry:首次 100 ms,最多 5 次,最大间隔 5 秒。pgrac 的 epoch 推进完成(典型 < 1 秒)后,新连接和新事务可立即提供服务;持久连接池(如 PgBouncer)配置 server_connect_timeout = 10s 可覆盖大多数 brownout 窗口。
深度协议细节请参阅以下资源:
Chapter 6 — Wait Events Reference 将介绍与 Reconfiguration 相关的等待事件细节(Cluster: Reconfig 类共 5 个事件),包括 Reconfig: GRD rebuild、Reconfig: lock recovery、Reconfig: fence wait、Reconfig: master selection、Reconfig: barrier wait 的触发条件、典型持续时间和诊断方法。