openGauss 没有用 PG 原生的 SyncRepWakeQueue 批量唤醒
PG 原生的等待模型
PostgreSQL 使用的是共享内存双向链表队列(WalSndCtlData.SyncRepQueueNUM_SYNC_REP_WAIT_MODE):
后端进入等待:
-
申请 SyncRepLock
-
把自己插入 SyncRepQueuemode(按 LSN 排序)
-
syncRepState = SYNC_REP_WAITING
-
释放 SyncRepLock
-
等自己的 latch
walsender 更新 LSN 后:
-
申请 SyncRepLock
-
调用 SyncRepWakeQueue() ------ 遍历队列,把 LSN 满足的所有 backend 一次性全部唤醒
-
释放 SyncRepLock
这样 N个backend排队,一次唤醒全部,没有惊群。
openGauss 的等待模型
openGauss 没有用这个队列机制,而是引入了一个串行锁 walSyncRepWaitLock:
// syncrep.cpp:496-511
if (LWLockAcquireOrWait(walSyncRepWaitLock, LW_EXCLUSIVE)) {
WalSndCtl->syncWaitProc = t_thrd.proc; // 记录当前等锁的人
ResetLatch(&t_thrd.proc->procLatch);
WaitLatch(&t_thrd.proc->procLatch, ..., 1000L); // 最多等1秒
WalSndCtl->syncWaitProc = NULL;
LWLockRelease(walSyncRepWaitLock);
}
同一时刻只有 1 个 backend 持有这个锁、处于"可被唤醒"状态,其他 N-1 个 backend 阻塞在锁上。
唤醒链路是:
walsender 更新LSN → SyncRepWakeBackend() → SetLatch(syncWaitProc->latch)
→ 那1个backend醒来,检查LSN,释放锁
→ 下一个backend获得锁,成为新的syncWaitProc
→ 等待下次LSN更新再被唤醒(或等1秒超时)
性能问题确实存在
惊群问题:是的,有。每个 walsender 刷新 LSN 后只唤醒 1 个 backend,如果 N 个 backend 都在等,需要 N 次 LSN
更新(或1秒超时)才能轮完。
对比 PG 原生方案:
┌──────────┬──────────────────────────────┬────────────────────────────────┐
│ 维度 │ PG 原生 (SyncRepQueue) │ openGauss (walSyncRepWaitLock) │
├──────────┼──────────────────────────────┼────────────────────────────────┤
│ 等待方式 │ 共享内存链表队列 │ 串行锁排队 │
├──────────┼──────────────────────────────┼────────────────────────────────┤
│ 唤醒方式 │ 一次唤醒所有满足LSN的backend │ 只唤醒当前持锁的1个backend │
├──────────┼──────────────────────────────┼────────────────────────────────┤
│ 并发等待 │ N个backend同时等latch │ 只有1个在等,其余阻塞锁 │
├──────────┼──────────────────────────────┼────────────────────────────────┤
│ 惊群 │ 无 │ 有(逐个唤醒) │
├──────────┼──────────────────────────────┼────────────────────────────────┤
│ 适用场景 │ 通用 │ 特定优化场景 │
└──────────┴──────────────────────────────┴────────────────────────────────┘
为什么 openGauss 这么改?
这段代码注释说得很清楚(syncrep.cpp:336):
▎ "Each proc try to lock walSyncRepWaitLock, and only one backend can get that lock, other backends will wait for it. The backend
▎ which get lock will wait latch for a while. If it is waked by WalSnder or wait time out, it will release lock. And all backends
▎ will reloop again."
这是 openGauss 针对分布式场景(ENABLE_MULTIPLE_NODES 等)做的特殊改造。核心原因是:
-
PG 的 SyncRepQueue 机制是为单机设计的,在分布式架构下,多个 DN 之间需要同步协调,批量队列唤醒的语义不够清晰
-
串行锁简化了状态机------只有"持有锁的那个人"需要关心 LSN 是否满足,其他人只是排队等锁,避免了多 backend 同时被唤醒后的竞争
-
1秒超时兜底------即使某个 backend 拿锁后没被唤醒,最多等1秒就会醒来重新参与下一轮竞争
但你说得对,确实有性能隐患
在高并发同步复制场景下(比如大量 backend 同时 COMMIT 且都要求 sync replication):
-
每个 backend 需要等前面一个被唤醒 + 检查 + 释放锁,才能轮到下一个
-
串行化程度高,延迟会随等待 backend 数量线性增长
-
PG 原生方案是 O(1) 唤醒所有,而 openGauss 是 O(N)
这是一个正确性优先于性能的设计取舍,在分布式场景下可能是合理的,但单机高并发场景下确实不如 PG 原生方案高效。