perf(tuning): coalesce a scale-up into one retune, and stop sleeping through the pause#14
Merged
Merged
Conversation
…through the pause
Measured on a real VM: a full scale-up (128 -> 1152 MiB of plugged memory, in
four steps ~80s apart) cost TWO postmaster restarts and TWO client stalls of
3783 ms and 3272 ms. Both numbers were self-inflicted.
1. Wait for the pool to actually drain, instead of sleeping 3s blind.
RETUNE_PAUSE_GRACE was a fixed `sleep(3s)` after signalling PgBouncer to
PAUSE. It was the entire client-visible stall: of the 3056 ms a client
blocked, 3000 ms was that sleep.
PAUSE releases each server connection as it goes idle, which normally takes
milliseconds. `pg::wait_quiesced` now polls postgres for in-flight work and
returns as soon as no client backend is executing. The 3s survives only as a
CAP, so a stuck long-running query cannot hold the retune open forever.
2. Wait for RAM to settle before retuning.
The host grows a starved VM in steps, not one jump. Retuning on the first
threshold crossing therefore guarantees another restart when the later steps
land — which is exactly what the two restarts were.
The watcher now tracks when visible RAM last CHANGED and defers the
postmaster-context retune until it has been quiet for RETUNE_SETTLE (90s,
which must exceed the ~80s inter-step interval or it would fire between
steps). The whole climb coalesces into one restart.
The reload-safe half (effective_cache_size, work_mem) still applies
immediately on every step, and newly-plugged RAM is already working as page
cache the moment it arrives — so the only cost is that shared_buffers lags
the memory by up to ~90s.
Re-measured on the same climb, same load, same client:
before after
restarts 2 1
client stalls >1s 2 0
slowest query 3783 ms 253 ms
errors 0 / 1056 0 / 1084
median 25 ms 24 ms
Worst-case client stall drops 15x, and no query even crosses one second.
Settling also sizes BETTER: shared_buffers lands at 471MB instead of 407MB,
because it now tunes against the settled RAM rather than a mid-climb snapshot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
A full scale-up (128 → 1152 MiB of plugged memory, four steps ~80s apart) cost two postmaster restarts and two client stalls of 3783 ms and 3272 ms. Both numbers were self-inflicted.
1. Wait for the pool to drain, don't sleep through it
RETUNE_PAUSE_GRACEwas a fixedsleep(3s)after signalling PgBouncer toPAUSE. It was the client-visible stall: of the 3056 ms a client blocked, 3000 ms was that sleep.PAUSEreleases each server connection as it goes idle — normally milliseconds.pg::wait_quiescednow polls Postgres for in-flight work and returns the moment no client backend is executing. The 3s survives only as a cap, so a stuck long-running query can't hold the retune open forever.2. Wait for RAM to settle before retuning
The host grows a starved VM in steps, not one jump. Retuning on the first threshold crossing therefore guarantees another restart when the later steps land — which is precisely what the two restarts were.
The watcher now tracks when visible RAM last changed and defers the postmaster-context retune until it's been quiet for
RETUNE_SETTLE(90s — must exceed the ~80s inter-step interval, or it would fire between steps). The whole climb coalesces into one restart.Cost, stated honestly:
shared_bufferslags the memory by up to ~90s. That's cheap — the reload-safe half (effective_cache_size,work_mem) still applies immediately on every step, and newly-plugged RAM is already doing useful work as page cache the moment it arrives.Measured — same climb, same load, same client
Worst-case client stall drops 15×, and no query even crosses one second.
The trace, on a live VM:
Settling also sizes better:
shared_bufferslands at 471MB instead of 407MB, because it now tunes against the settled RAM rather than a mid-climb snapshot.Tests: 107 + 41 + 33 green. New test pins the reason for the debounce — a single measured climb crosses the ratio threshold more than once.
🤖 Generated with Claude Code