Re: Reduce SyncRepLock contention on the commit path - Mailing list pgsql-hackers
| From | Yura Sokolov |
|---|---|
| Subject | Re: Reduce SyncRepLock contention on the commit path |
| Date | |
| Msg-id | 3024af42-d264-4bb8-aa58-377b286bd4b3@postgrespro.ru Whole thread |
| Responses |
Re: Reduce SyncRepLock contention on the commit path
|
| List | pgsql-hackers |
17.08.2026 11:29, Vadim Ponomarev пишет: > Hi hackers, > > On a primary with synchronous replication, every commit that wrote WAL > goes through SyncRepWaitForLSN(), and every standby reply goes through > SyncRepReleaseWaiters(). Both take SyncRepLock exclusively, although > much of their work does not need to happen under the lock. > > The attached series moves that work out of the critical section. The > four patches are independent; only 0003 changes observable behaviour. > > 0001 -- Wake released waiters after dropping the queue lock. > > SyncRepWakeQueue() sets each released backend's latch while holding > SyncRepLock. Setting a latch may call kill(), so at high commit rates > the walsender makes one syscall per released commit while committers > queue on the same lock. > > The patch collects released procs in a list and wakes them after dropping > the lock, as ProcArrayGroupClearXid() already does with ProcArrayLock. > The unlink, write barrier, and state update remain under the lock: a > waiter reads syncRepState without it and must not see itself as completed > while still on the queue. > > 0002 -- Compute synced positions before taking the queue lock. > > SyncRepReleaseWaiters() takes SyncRepLock before scanning the walsender > slots for synced write, flush, and apply positions. That scan takes a > spinlock per slot, allocates memory, and sorts quorum results. > > The patch moves the scan before the lock. The consumers only move lsn[] > forward, so a result that becomes stale while waiting for the lock simply > does not advance it. The patch also avoids the lock when the walsender > is not a synchronous standby. > > 0003 -- Release waiters once per drained batch of standby replies. > > ProcessStandbyReplyMessage() calls SyncRepReleaseWaiters() for every > reply, even when several replies are already waiting in the socket. > Except for the last one, each pass then uses positions that the next > message immediately replaces. > > The patch marks a release as pending and runs it once after draining the > socket. The main risk is losing a pending release on an early exit: no > other process will wake the committers acknowledged by that reply. > > The goodbye, EOF, invalid and unexpected message paths all run the > pending release before returning. A normal standby shutdown sends its > final reply and goodbye together, so that path matters in practice. > Errors while parsing a later message, including a torn message, go > through WalSndErrorCleanup(), which releases waiters after dropping the > locks. > > 0004 -- Skip the lock when the acknowledgement has already arrived. > > This patch mirrors lsn[] in an atomic watermark and checks it before > SyncRepWaitForLSN() takes the lock. The watermark is updated under the > lock immediately after lsn[]. Both only move forward, so a stale read > may take the slow path unnecessarily but cannot skip a required wait. > > I am least sure that 0004 is worth the extra shared state; see below. > > Prior work > ---------- > > 0001 was proposed in Thomas Munro's "Latches vs lwlock contention" > thread as part of a general SetLatches() facility: > > https://www.postgresql.org/message-id/CA%2BhUKGKmO7ze0Z6WXKdrLxmvYa%3DzVGGXOO30MMktufofVwEm1A%40mail.gmail.com > > The heavyweight-lock part of that work was committed in November 2024, > but SetLatches() and its sync-rep use were not. As far as I can tell, > the sync-rep patch did not get a separate review. I used a list local to > syncrep.c to keep the change contained and avoid the open questions > around buffers and allocation in the general facility. > > There is also precedent for 0004. Michael Paquier's 2e57790836c ("Fix > race with synchronous_standby_names at startup", April 2025) reads > WalSndCtl->sync_standbys_status without the lock. The same monotonicity > argument applies here; the LSN needs the atomics API because it is 64 > bits wide. > > Measurements > ------------ > > I compared devel master with the same master plus all four patches on > two hosts connected by a dedicated 100 GbE link (RTT 0.11 ms): > > primary 4-socket Xeon Platinum 8580, 240 threads, 2 TB RAM, NVMe > standby 2-socket Xeon Gold 5320, 104 threads, 1 TB RAM, NVMe > > The test used pgbench scale 2000, fillfactor 70, 750 clients, > -M prepared, the built-in TPC-B script, and 10-minute runs with > synchronous_commit = on, fsync and full_page_writes on. postgres used > two primary sockets and pgbench a third. > > Each point started from the same prepared cluster, and the standby was > rebuilt from a fresh base backup. I ran three interleaved pairs, > alternating base and patched: > > tps runs 122250/122591/123091 -> 134849/134447/132740 > mean tps 122644 -> 134012 +9.3% > mean latency 6.085 ms -> 5.558 ms -8.7% > failed transactions: none > > The spread was 0.7% for base and 1.6% for patched. > > pg_stat_activity samples taken every 5 seconds show the same effect. > The queue lock and standby acknowledgement wait are both named SyncRep; > wait_event_type separates them: > > LWLock/SyncRep samples 12222 -> 6912 -43% > IPC/SyncRep samples 20699 -> 14694 -29% > > Per committed transaction, lock-wait samples fell from 0.0997 to 0.0516. > The queue-lock wait was roughly halved while throughput rose by 9%. The > standby acknowledgement wait also fell by 35% per transaction, as > committers stopped queueing for SyncRepLock before waiting for the > standby. > > To see which patches contributed, I also ran -DLWLOCK_STATS builds on a > small single-host setup: 16 threads, scale 20, 128 clients, 30 seconds. > Across two runs, base -> patched: > > walsender acquisitions/commit 0.900-1.106 -> 0.538-0.610 > backend acquisitions/commit 0.9983-0.9995 -> 0.977-0.984 > commits that blocked 9.34-10.20% -> 0.24-0.27% > > At 32 clients, blocked acquisitions fell from 37809 to 362. > > 0003 makes most of the difference: the walsender takes the lock roughly > half as often. 0004 rarely takes its fast path, saving only 1.6-2.3% of > backend acquisitions at 128 clients. With synchronous_commit = on, it > can only catch a commit if an acknowledgement for a later transaction > happens to cover its LSN. > > I also ran four interleaved, 60-second single-client pairs. Mean > latency was 0.760 ms on base and 0.762 ms patched, so deferring the > release to the end of a one-reply drain showed no measurable delay. > > Testing > ------- > > 0003 adds src/test/recovery/t/056_syncrep_release.pl for two cases where > a deferred release could be lost: > > * A stopped walsender resumes after the standby has applied the commit > and shut down, then drains the final reply and goodbye together. > > * An injection point after a drained reply stands in for a torn message. > The walreceiver is stopped while replay advances from WAL already on > disk, so the first reply after resume carries the apply position the > committer needs. > > Notes > ------- > > The SyncRepReleaseWaiters() call on configuration reload is outside the > reply drain and is unchanged by 0003. > > 0001 allocates a MaxBackends-sized wake list once per process in > TopMemoryContext. I am open to changing that if there is a better fit. > > The patches are against 7e6e294e4e4. > > > ------- > > Review would be especially helpful on two points: > > * Did I miss any exit path from the reply drain that must run the > deferred release? > > * Is 0004 worth its new atomic in WalSndCtlData for a 1.6-2.3% reduction > in backend lock acquisitions in these tests? > > I plan to register 0001-0003 for the September CommitFest and drop 0004, > unless there is a workload where its fast path is more useful. > > Regards, > Vadim Ponomarev Good day, Vadim. We've measured your patches and confirm they improve performance of synchronous replication: - with couple of 56 vcore virtual machines and pgbench running on replica, 250 clients, scale 2000, "TPC-B like" improved from 67.3kTPS to 70.4kTPS. Which is quite impressive. 0001 i've found independently, so I fully share the idea. It really works. 0003 impressed me a lot. Great thing, imo! 0002 looks like "dirty-hack", but reading closely I found no issues: - SyncRepGetCandidatesStandbys syncs by spinlocks on every walsender - all WalSndCtl->lsn increases monotonically under lock. 0004 it really doesn't cost anything and gives some value. So let it be. We didn't measure things one-by-one, but I suppose 0001 and 0003 gives most of gain. Still other thing are useful as well, I believe 0002 and 0004 are worth to be committed. I've rebased patches and simplified a bit 0001 and 0004: - 0001 already uses static variable. So why don't just make it file-wide and use in SyncRepWakeQueue directly? - 0004 - there is no need in separate lsn and lsn_published. Lets simply convert lsn to atomic variable. And I've refactored condition under lock a bit to make it clear why test against of just atomic WalSndCtl->lsn[mode] could be enough. It was a bit non-obvious in the form it is in master branch. And I've added 027_stream_regress_sync.pl in 0005 as tweaked copy of 027_stream_regress.pl to test synchronous replication under concurrent load. -- regards Yura Sokolov aka funny-falcon
Attachment
- v2-0001-Wake-the-released-sync-rep-waiters-after-the-queu.patch
- v2-0002-Compute-the-synced-positions-before-taking-the-sy.patch
- v2-0003-Release-the-sync-rep-waiters-once-per-drained-bat.patch
- v2-0004-Let-a-committer-whose-acknowledgement-already-arr.patch
- v2-0005-Add-stream-regress-test-for-synchronous-replicati.patch
pgsql-hackers by date: