GES(Global Enqueue Service,全局排队锁服务)是 pgrac 在 Cache Fusion 之外的第二套跨节点协调协议。PCM 锁管理 buffer cache 内 8 KB block 的并发;GES 管理 buffer cache 之外的所有跨节点锁——行锁、表锁、事务锁、对象锁、advisory 锁。两者共用 GRD(Global Resource Directory)作为权威状态存储,但 keyspace 完全隔离,PCM 用 BufferTag,GES 用 ClusterResId。
本章按 Stage 2 当前设计描述 GES:哪些资源走集群路径(仅 4 类:RELATION / TRANSACTION / OBJECT / ADVISORY,spec-2.14 L261-264)、GRD entry 的内部结构(spec-2.15)、shard-to-master 的静态映射(spec-2.14)、Grant / Convert / Release 的基本操作(spec-2.16)、BAST 协作释放与 Tarjan SCC 死锁检测(spec-2.17 / 2.22)。Stage 2 当前实际上线的锁类别是 TX / TM / OBJECT 三类(参见 stage2-6-roadmap 中 spec 2.25);完整 8 模式 GES 矩阵(含 SEQ / CF / UL / TT / IS / CI / XR / PG 等)属于 Stage 5(spec 5.1 Full GES 8-mode lock matrix)。
本章使用 pgrac 命名(write-ahead invariant,spec-2.29;GRD entry,spec-2.15;ClusterResId,spec-2.14)。Oracle 风格的 GCS/GES 二元拆分、LC Lock、RC Lock 不存在于 pgrac——见 AD-011(PG 无 SGA shared pool,LC / RC Lock 不迁移;architecture-impact.md L386-387)。pgrac 沿用 PG 自身的 LOCKMODE 1..8 枚举(NOT Oracle 的 NL/IS/IX/SS/SX/EX),冲突判定调用 PG lmgr/lock.c 的 DoLockModesConflict()——见 AD-012(Cluster Visibility Path,architecture-impact.md L1141-1240)。
PCM 锁的操作对象是 BufferTag——物理 block。但数据库的大量跨节点协调与 buffer cache 无关:
LOCKTAG_TRANSACTION)的可见性必须跨节点ALTER TABLE → 全部节点的 DML 必须阻塞(LOCKTAG_RELATION)pg_advisory_lock(key) → 集群范围互斥,而非单节点(LOCKTAG_ADVISORY)LOCKTAG_OBJECT这些场景的共同特征是:锁对象是逻辑资源。把它们硬塞进 PCM 会让 PCM 的 N/S/X 状态机失去物理 block 这条不变量的依托——例如 PCM 的 "X→S downgrade" 会被错误解释为"行锁降级",事务隔离崩溃。GCS / GES 的职责分离不是 Oracle 的随意约定,而是语义不可替代。
pgrac GES 在协议层仅有 4 类锁是 cluster-aware(spec-2.14 L261-264);其它 PG 锁类型在 cluster_grd_locktag_is_cluster_aware() 中返回 false,走原生单机路径。
| PG LockTag 类型 | cluster-aware? | Stage 2 实际启用 | 说明 |
|---|---|---|---|
LOCKTAG_RELATION | 是 | 是(TM) | 表 / 索引级锁;DDL × DML 协调 |
LOCKTAG_TRANSACTION | 是 | 是(TX) | 跨节点事务可见性 + 行锁等待 |
LOCKTAG_OBJECT | 是 | 是(OBJECT) | 扩展定义的全局对象 |
LOCKTAG_ADVISORY | 是 | 否(Stage 5) | pg_advisory_lock |
LOCKTAG_TUPLE / SPECULATIVE / VIRTUAL / OBJECT-CLUSTER_* | 否 | — | 本地或非冲突路径 |
Stage 2 实际上线的子集是 TX / TM / OBJECT,由 stage2-6-roadmap 中 spec 2.25 锁定;LOCKTAG_ADVISORY 虽然在协议层已 cluster-aware,但实际启用推迟到 Stage 5(spec 5.1 Full GES 8-mode lock matrix)。其余传统 Oracle 概念(LC / RC Lock / SEQ / CF / UL / TT / IS / CI / XR / PG-specific)在 pgrac 当前阶段均不存在,详见 AD-011。
跨节点消息不能直接 memcpy PG 的 LOCKTAG 结构——LOCKTAG 含 padding、含本节点特有的字段、不保证跨编译器布局一致。spec-2.14 引入 ClusterResId:16 字节、字段顺序固定、跨节点 wire 安全的规范身份。
typedef struct ClusterResId {
uint32 field1; // 通常 = LOCKTAG.locktag_field1(dboid / relid / xid)
uint32 field2; // 通常 = LOCKTAG.locktag_field2
uint32 field3; // 通常 = LOCKTAG.locktag_field3
uint16 field4; // 元组偏移等;**不**参与 shard hash
uint8 type; // PG LockTagType 之一
uint8 lockmethodid; // PG lockmethod 索引
} ClusterResId; // exactly 16 bytes (spec-2.14 L199-217)
关键点:ClusterResId 不是 LOCKTAG 的 memcpy 副本,它是从 LOCKTAG 规范化派生而来的独立结构,由 cluster_grd_resid_from_locktag() 在跨节点边界构造。这样 wire 层就不依赖 PG 内部结构布局。
GRD 采用 4096 个固定 shard(PGRAC_GRD_SHARD_COUNT = 4096,spec-2.14 L196)。每个 ClusterResId 经过 hash 后落到唯一 shard,shard 再静态映射到一个 master 节点:
// shard_id 计算(spec-2.14 L290-296, Q7 L70)
// hash 输入为 ClusterResId **前 14 字节**——跳过 field4(元组偏移)
// 这样同一行的不同 tuple 落在同一 shard,便于行级争用聚合
shard_id = hash_bytes_extended(&resid, 14) % 4096;
// shard → master 映射(spec-2.14 L307-316)
// declared_list = sorted(cluster.conf 中所有声明的 node_ids)
// 注意:是对 declared_list 长度取模,不是 cluster_node_id
// 因为 cluster.conf 允许 node_id 稀疏(例如 1, 3, 7)
master[shard_id] = declared_list[shard_id % len(declared_list)];
实现形态:master[4096] 是 pg_atomic_uint32 数组,位于 shmem region "pgrac cluster grd"(spec-2.14 L238),路由查询无锁——一次原子 load 即得 master node_id。
Stage 2 的 master 映射是静态声明的——在 cluster_grd_master_map_init()(postmaster 启动期)一次性初始化后永不变更。不存在 advertise_master、transfer_ownership 这类运行时操作。动态 remastering(DRM)是 Stage 6 范围(spec-2.14 L126),需要协议层显式 hand-off 与 epoch 协调,Stage 2 故意不实现。
pgrac GES 不引入新的锁模式枚举——直接复用 PG 的 LOCKMODE 1..8(AccessShareLock .. AccessExclusiveLock)。冲突判定调用 PG 自身的 DoLockModesConflict()(来自 lmgr/lock.c,spec-2.16 Q2)。这条决策属于 AD-012 Cluster Visibility Path(architecture-impact.md L1141-1240):把单机 PG 的锁兼容矩阵原封不动扩展到集群范围,避免应用迁移时出现行为差异。
| LOCKMODE | 模式名 | 典型触发 | 主要冲突对象 |
|---|---|---|---|
| 1 | AccessShareLock | SELECT | AccessExclusive |
| 2 | RowShareLock | SELECT FOR UPDATE | Exclusive + |
| 3 | RowExclusiveLock | INSERT / UPDATE / DELETE | Share + |
| 4 | ShareUpdateExclusiveLock | VACUUM / ANALYZE | 同级 + |
| 5 | ShareLock | CREATE INDEX | RowExclusive + |
| 6 | ShareRowExclusiveLock | 触发器 | RowExclusive + |
| 7 | ExclusiveLock | REFRESH MATERIALIZED VIEW CONCURRENTLY | RowShare + |
| 8 | AccessExclusiveLock | ALTER TABLE / DROP | 全部 |
本地 fast path 仍然走 PG 原生 LOCALLOCK 缓存:本节点已持有兼容模式 → 不产生任何网络消息,延迟与单机 PG 一致。只有 LOCALLOCK miss 且 LockTag 属于 4 类 cluster-aware 之一时,才进入 GES 跨节点协议。
GRD(Global Resource Directory)是 GES 的权威状态存储。每个 shard 是一个 dshash 桶,桶内每条记录是一个 GrdEntry(spec-2.15 L332-344)——file-static、opaque、对调用者不暴露字段细节,所有访问通过 cluster_grd_* API。
typedef struct GrdEntry { /* spec-2.15 L332-344 */
ClusterResId resid; // 16 字节 key
slock_t lock; // 入口 spinlock
int ngranted; // 0 .. MAX_HOLDERS
ClusterGrdHolder holders[16]; // {node_id, mode, xid}
int nwaiters; // 0 .. MAX_WAITERS
ClusterGrdWaiter waiters[16]; // {node_id, mode, wait_start}
int nconverts; // 0 .. MAX_CONVERTS
ClusterGrdConvert converts[8]; // {node_id, current_mode, requested_mode}
uint64 last_modified_scn;
uint32 state_flags;
} GrdEntry;
| 常量 | 值 | 含义 |
|---|---|---|
PGRAC_GRD_MAX_HOLDERS | 16 | 同一资源最大并发持有者数 |
PGRAC_GRD_MAX_WAITERS | 16 | 最大等待队列长度 |
PGRAC_GRD_MAX_CONVERTS | 8 | 同时进行的 convert 请求数 |
这些上限是 Stage 2 的硬约束(spec-2.15 L306-308),溢出时返回 FULL,调用方负责重试或上抛。
核心入口是 cluster_grd_entry_lookup_or_create(resid, create, out),返回 5 值枚举(spec-2.15 L209-216, L417-498):
| 返回码 | 枚举值 | 含义 |
|---|---|---|
OK | 0 | 命中或新建成功,out 持有 entry 引用 |
NOT_READY | 1 | shard 尚未初始化(重配置期间) |
NOT_FOUND | 2 | create=false 且不存在 |
FULL | 3 | shard 容量耗尽 |
ERROR | 4 | 不变量违反 / 内部错误 |
所有跨节点 grant / convert / release / BAST 路径都先经过这个入口,确保 resid → entry 的解析是幂等且有原子保护的(entry 内部 slock_t lock 保护字段读写)。
BAST 是 Blocking AST 的缩写(AST = Asynchronous System Trap,ges-lock-protocol-design.md L91)。BAST 是 GES 区别于传统阻塞锁的关键设计——但更重要的是理解 BAST 是通知,不是授权(spec-2.17 Invariant I63,L304):BAST 告诉持有者"有人想要",但持有者继续持锁直到事务自然结束。Master 不能因为发了 BAST 就把锁授给请求者,授权必须等真正的 RELEASE 到达。
GES_REQUEST(opcode 1)或 GES_CONVERT(opcode 2),与现有 holders[] 中某项冲突。Master 向该持有者发 GES_BAST(opcode 4)。CLUSTER_GRD_PENDING_BAST_RECEIVED,并调用 SetProcSignal(PROCSIG_CLUSTER_GES_BAST)。cluster_grd_bast_handler(),仅设置 bast_pending 标志位——不立即降级,不立即释放。LockReleaseAll 时,原本就会发的 GES_RELEASE(opcode 3)envelope 上携带一个 BAST_ACK 逻辑标志。holders[] 移除,重新评估 waiters[] / converts[],向兼容的下一个 waiter 发 GES_GRANT。Node 1 (requester) Node 2 (master) Node 3 (current holder)
| | |
|--- 1. REQUEST ---->| |
| |--- 2. BAST ------->| ProcessInterrupts
| | | sets bast_pending
| | | (continues txn)
| | |
| |<-- 3. RELEASE -----| commit / rollback
| | (BAST_ACK) |
|<-- 4. GRANT -------| |
BAST 超时不杀死健康持有者(Invariant I64,spec-2.17)。若持有者的事务长时间运行(合法长事务),即便 BAST 已被忽略很久,master 也不会强制释放——这样可以保证健康节点上的事务语义不被网络协议层意外打断。"BAST 长时间未响应" 是 LMD(Lock Manager Daemon)死锁检测的输入信号,不是抢占信号。
跨节点 ProcSignal 必须防止"对已退出的 backend 投递信号"——procno 可能已被新 backend 重用。spec-2.17 L170 Q7 定义了 BAST 投递的 6 元组身份,缺一不可:
{ target_node_id, target_procno, target_generation,
request_seq, resid, mode, epoch }
其中 target_generation 是 procno 重用计数器,epoch 是 cluster epoch(spec-2.29,跨重配置后单调推进)。任一字段不匹配 → BAST 在 IC handler 阶段被丢弃 + 日志记录,不进入 ProcessInterrupts。
| 路径 | 目标延迟 | 说明 |
|---|---|---|
| 无冲突 grant(Tier 1 RDMA) | ~5 μs | REQUEST → 直接 GRANT,1 个 round-trip |
| 有冲突且持有者即将提交 | 10–20 μs | REQUEST → BAST → commit-time RELEASE → GRANT |
| 有冲突且持有者长事务 | 由持有者事务决定 | BAST 不抢占;可能秒级 |
BAST 是协作式的,因此 GES 必须独立检测循环等待。spec-2.22 把这个任务交给 LMD(Lock Manager Daemon)——它在每个 tick 上跑一次 Tarjan SCC 算法(强连通分量分解)。这是数据库教科书算法,但 pgrac 的实现细节有两点与教科书不同:迭代版本(非递归,避免栈溢出)+ snapshot 解耦(不在持锁时跑 Tarjan)。
跨节点 wait-for 图存放在独立 shmem region "pgrac cluster lmd graph"(spec-2.22)——故意与 LMD daemon 的进程局部状态分离:
cluster_lmd_graph_add_edge() 注册"我(procno X 在 node A)正在等待持有者(procno Y 在 node B)"这条 snapshot-based 设计的代价是图可能稍微过时,但 Tarjan 的输出永远是某一时刻 wait-for 图的合法 SCC——后续若死锁真的解除,下一个 tick 自然观察到。
Tarjan 找到 SCC(环)后必须选一个 backend 作为受害者(victim)来打破环。选择键按 spec-2.17 L186 Q16 / spec-2.20 Q6:
victim_key = ( cluster_epoch, // 高位:epoch 越早越优先(陈旧环)
local_start_ts_ms DESC,// 主键:最年轻 backend 优先(回滚代价最低)
node_id, // tie-break 1
xid ) // tie-break 2
"最年轻" 的逻辑是:年轻事务做的工作少,回滚代价最低。这也是 PG 单机 deadlock detector 的传统选择,pgrac 把它扩展到集群范围。
当受害者在本节点时,LMD 通过 PROCSIG_CLUSTER_GES_CANCEL(spec-2.17 Q9)向目标 backend 投递 ProcSignal。Backend 在 ProcessInterrupts 中观察到该 pending 后 ereport(ERROR, "deadlock detected"),SQLSTATE 40P01(ERRCODE_T_R_DEADLOCK_DETECTED,PG 标准 deadlock SQLSTATE)。
跨节点受害者取消转发(victim 在远端节点的情形)属于 spec-2.24 范围,Stage 2 当前仅支持本节点受害者直接取消。多节点受害者投递需要可靠的跨节点 cancel forwarding + 应答,spec-2.24 在 stage2 后续 spec 中落地。
GES 协议的核心操作码 GesRequestOpcode 列表如下,前 3 个是 Stage 2 MVP 必备,其余为辅助路径:
| Opcode | 名称 | 所属 spec | 用途 |
|---|---|---|---|
| 1 | REQUEST | spec-2.16 | 初次获取锁(backend 当前未持有) |
| 2 | CONVERT | spec-2.16 | 升级锁模式(已持有 current_mode,请求 requested_mode) |
| 3 | RELEASE | spec-2.16 | 释放(事务提交 / 回滚 / 显式 LockRelease) |
| 4 | BAST | spec-2.17 | master 通知 holder "有人需要这个锁" |
| 5 | BAST_ACK | spec-2.17 | 逻辑标志,piggyback 在 RELEASE 上 |
| 6 | DEADLOCK_PROBE | spec-2.22 | LMD 探测跨节点 wait-for 边 |
| 7 | CANCEL_PENDING | spec-2.22 | 取消 pending 请求(受害者本地路径) |
| 8 | DEADLOCK_REPORT | spec-2.22 | LMD 汇报检测到的 SCC |
两者都会让 backend 最终在 holders[] 中出现,但起点不同:
waiters[];不冲突则直接进 holders[]。converts[] 队列(最多 8 个),等其它冲突 holders 释放后由 master 改写 holders[] 中该条目的 mode。这条区分对死锁检测尤其重要——convert wait 与初次 grant wait 在 wait-for 图中是不同类型的边,LMD 需要分别处理。
spec-2.16 Q3 / spec-2.18 锁定:只有 shard 的 master 节点有权改写 holders[] / waiters[] / converts[]。非 master 节点的请求一律通过 IC 转发到 master,由 master 在 LMS(Lock Manager Server)的 inbound work_queue 中串行处理。这条规则配合 §3.3.1 的静态 master 映射,保证全局每个资源有唯一仲裁者。
pgrac 的全局正确性建立在 write-ahead invariant(spec-2.29)之上:任何节点对共享存储的写入必须先持有对应资源的 GES X 锁,并且 master 已在 GRD 中记录该 holder。该不变量是 Cache Fusion 和 GES 共同的写正确性基础——违反则导致丢失更新或脏读。Stage 2 通过两条路径执行:
cluster_qvotec_in_quorum() + 验证 holder 状态(参见第 5 章)PROCSIG_CLUSTER_FREEZE_WRITES 中断 in-flight 事务(参见第 5 章 §5.5.1)本章只讨论 GES 锁本身的获取 / 释放语义;写前不变量的端到端执行参见第 5 章。
深度协议细节请参阅以下资源:
Chapter 4 — SCN 将描述所有跨节点消息(包括 GES envelope)如何 piggyback 当前 SCN 以维持因果排序与一致性读;Chapter 5 — Reconfiguration 描述节点拓扑变化时 GRD shard / master[] 映射重建、holders 重新发布、孤立锁清理的完整流程。