Reduce SyncRepLock contention on the commit path - #409
Open
pg-hub-mirror[bot] wants to merge 5 commits into
Open
pg-hub-mirror[bot] wants to merge 5 commits into
pg-hub-mirror[bot] wants to merge 5 commits into
Conversation
SyncRepWakeQueue() sets each released backend's latch while holding SyncRepLock exclusively. A latch is a kill() syscall whenever its proc is asleep, and at a high commit rate the walsender runs one of them per released commit inside the very section every committer lines up on. ProcArrayGroupClearXid() already wakes its batch only after ProcArrayLock is down, for the same reason. Collect the released procs into a list instead, and set their latches once the lock is released. The unlink, the write barrier and the state store stay under the lock: a waiter reads syncRepState without the lock and must never find itself completed while still on the queue. Nothing in the deferred loop can error out, so a released proc cannot be left completed but unlatched short of the process dying outright -- a window the in-lock SetLatch had as well. A proc that noticed its state on its own and moved on, even into a new wait, gets a spurious latch set, which every latch sleeper tolerates. The list is sized to MaxBackends and allocated once per releasing process. A proc waits in at most one queue, so one list of that size bounds a walk over all three.
SyncRepReleaseWaiters() takes SyncRepLock and only then walks the walsender slots to work out the synced write, flush and apply positions. That walk takes a spinlock per slot, allocates, and for a quorum set sorts the result, and every cycle of it is spent in the section every committer lines up on. The comment there conceded the work does not need the lock and kept it inside anyway, to guarantee the positions are newer than any previous execution of the routine used. That guarantee is not needed. The three sites that consume the positions each move lsn[] forward only when the new reading is ahead of the stored one, so positions gone stale while the lock was being taken release nobody and change nothing; a concurrent walsender that got further has already stored its own. Compute them before taking the lock, and leave without taking it at all when this walsender turns out not to be a sync standby.
ProcessStandbyReplyMessage() calls SyncRepReleaseWaiters() for every reply it processes, and several replies routinely sit in the walsender's socket together. Each of those calls takes SyncRepLock exclusively, so a batch of replies costs the committers one period of that lock apiece -- computed, for all but the last reply, from positions the next message in the same batch immediately makes stale. Have a reply only mark a release as pending, and run one release at the end of the drain. The positions in shared memory are the newest of the batch by then, so the single pass releases everything the individual passes would have. A deferred release must survive every way out of the drain, because the positions the reply already stored are valid whatever follows and the committers it acknowledged have no other process to wake them: - the standby's goodbye, an EOF, an invalid message type and an unexpected message type each run the pending release before leaving. A clean standby shutdown sends its final reply and the goodbye back to back, which makes that exit the routine one rather than the exotic one. - an error thrown while a later message in the same drain is parsed -- a torn message above all -- leaves through WalSndErrorCleanup(), which runs the pending release after the locks are dropped. The test makes both coincidences certain instead of likely. For the first, a paused standby holds a remote_apply committer in the queue, the walsender is held with SIGSTOP while the standby applies past the commit and shuts down, and the released walsender drains the final reply and the goodbye in one pass. For the second, an injection point right after a drained reply stands in for the torn message; it fires on every reply, so the walreceiver is held with SIGSTOP while replay proceeds from WAL already on standby disk, which makes the first reply after release the one carrying the apply position the committer waits for. Both halves fail without their fix.
…lock SyncRepWaitForLSN() runs on every commit that wrote WAL, and it takes SyncRepLock exclusively before it can find out whether there is anything to wait for. On a busy primary a large share of those commits find their LSN already acknowledged and queue for nothing, so the answer costs them a period of the lock every other committer is lining up on. Turn lsn[] into an atomic watermark, and read it before taking the lock. A watermark that already covers the commit's LSN says a valid quorum acknowledged it, which is exactly the answer the check under the lock gives. It is only ever moved forward, so a read gone stale can send a committer to the slow path that would have exited, but never past a wait it owes. On platforms where pg_atomic_read_u64() is not a plain load the read is itself a compare-and-exchange, or a spinlock acquisition where 64-bit atomics are emulated. Whether the exit still pays for itself there is untested.
It will allow to experiment with synchronous replication code more wildly. 027_stream_regress_sync.pl is a copy of 027_stream_regress.pl with couple of changes: - synchronous_commit is set to remote_apply - second standby added and checked - both standbys are in synchronous_standby_names - rely on remote_apply for standbys to catchup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pgsql-hackers3024af42-d264-4bb8-aa58-377b286bd4b3@postgrespro.ruPatch files:
17.08.2026 11:29, Vadim Ponomarev пишет:
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:
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:
and use in SyncRepWakeQueue directly?
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