fix(tuning): retune shared_buffers behind a pgbouncer PAUSE#11
Merged
Conversation
…hind a pgbouncer PAUSE The guest's visible RAM can change at runtime (memory is hot-plugged in after boot), but beyond-pg only read RAM at boot. It derived shared_buffers from whatever RAM happened to be visible then, and never revisited it — so an instance that grows stays sized for its smallest moment, for the life of the postmaster. The memory watcher now follows RAM across both tuning halves: - 02-memory.conf (sighup-context: effective_cache_size, work_mem, maintenance_work_mem, the per-gather knobs) — rewritten and applied with pg_reload_conf(), as before. - 01-tuning.conf (postmaster-context: shared_buffers, max_connections, wal_buffers, worker counts) — postgres fixes these at postmaster start and cannot be talked out of them. The watcher therefore asks the supervisor loop for a retune cycle: rewrite the conf, resize the hugepage reservation, PAUSE every pgbouncer worker (SIGUSR1), fast-shutdown and respawn the postmaster, then RESUME (SIGUSR2) on every path out — including failure, since a pooler left paused is an outage. PAUSE waits for in-flight transactions to be released and then queues new clients rather than dropping them, so the restart is a stall, not an error. Gated by hysteresis: shared_buffers must move >=25% (RETUNE_MIN_RATIO), and at most once per RETUNE_MIN_INTERVAL, so a flapping balloon can't put the postmaster into a restart loop. Also fixes a latent brick: apply_kernel_settings only checked whether the nr_hugepages *write* succeeded, never whether the kernel actually granted the pages. Writing nr_hugepages is a request, not a guarantee — hugetlb pages must be physically contiguous, so the kernel reserves what it can and silently clamps the rest. With huge_pages=on a short reservation makes postgres REFUSE TO START, so resizing shared_buffers upward could have left the postmaster unable to come back. It now reads the count back and falls back to huge_pages=try on any shortfall, clearing the override again once a reservation succeeds. shared_buffers and its hugepage reservation are now derived from one place (config::shared_buffers_mb / nr_hugepages_for) instead of three, so they cannot drift apart. Verified end-to-end on a live VM, reproduced twice: visible RAM 752,920 kB -> 1,801,496 kB shared_buffers 183MB -> 503MB postmaster restarted client @ 1/sec 75 successes, 0 errors across the restart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…estart
The retune in the previous commit restarts the postmaster, which drops the
buffer pool cold. pg_prewarm.so was already in the image; we just never
loaded it.
Preload pg_prewarm and turn its autoprewarm worker on. It periodically dumps
the block list currently in shared_buffers to PGDATA/autoprewarm.blocks, and
reloads those blocks at startup. It stores block identities, not data, so the
dump is small.
This image restarts the postmaster more often than a hand-run Postgres does —
a retune when visible RAM moves, plus reboots, image swaps, forks and
restores — and every one of those would otherwise come back to a stone-cold
pool and a latency cliff while it refills.
Dump interval is 60s rather than the 300s default: a retune or a crash can
land at any time, and a 5-minute-stale block list can miss most of a working
set that turned over in between.
Also exposes pg_prewarm as an optional extension (the manual pg_prewarm('tbl')
function); the autoprewarm worker itself comes from the preload, not from
CREATE EXTENSION.
Verified on a live VM: warmed the pool, then forced a retune 14s later —
inside the dump interval, so no periodic dump had fired and only the
shutdown dump could save it.
shared_preload_libraries ...,pg_prewarm
buffers holding the table 2316 -> 2316 across the restart (100%)
shared_buffers 183MB -> 503MB
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.
The bug
The VM's visible RAM changes at runtime — the guest boots with only part of its memory online and the rest is hot-plugged in later. But
beyond-pgonly read RAM at boot, derivedshared_buffersfrom whatever was visible then, and never revisited it.So an instance that grows stays sized for its smallest moment, for the life of the postmaster. Measured on a live VM:
MemTotal: 752920 kBat boot →shared_buffers = 183MB, held there permanently even after the VM grew to 1.7 GiB. Nothing anywhere complained.The fix
The memory watcher now follows RAM across both tuning halves:
02-memory.conf(sighup-context:effective_cache_size,work_mem,maintenance_work_mem, per-gather knobs) — rewritten and applied withpg_reload_conf(), as before.01-tuning.conf(postmaster-context:shared_buffers,max_connections,wal_buffers, worker counts) — Postgres fixes these at postmaster start and cannot be talked out of them. The watcher asks the supervisor loop for a retune cycle: rewrite the conf → resize the hugepage reservation →PAUSEevery pgbouncer worker (SIGUSR1) → fast-shutdown + respawn the postmaster →RESUME(SIGUSR2) on every path out, including failure (a pooler left paused is an outage).PAUSEwaits for in-flight transactions to be released and then queues new clients rather than dropping them, so the restart is a stall, not an error.Gated by hysteresis:
shared_buffersmust move ≥25% (RETUNE_MIN_RATIO), at most once perRETUNE_MIN_INTERVAL— so a flapping balloon can't put the postmaster into a restart loop.autoprewarm — the restart doesn't cost you a cold pool
A restart would normally drop the buffer pool cold.
pg_prewarm.sowas already in the image; we just never loaded it. It's now preloaded with its autoprewarm worker on: it periodically dumps the block list inshared_bufferstoPGDATA/autoprewarm.blocksand reloads those blocks at startup (block identities, not data — the dump is small). Interval is 60s rather than the 300s default, since a retune or crash can land at any time.This pays for every postmaster restart, not just retunes: reboots, image swaps, forks, restores.
Latent brick, also fixed
apply_kernel_settingsonly checked whether thenr_hugepageswrite succeeded, never whether the kernel actually granted the pages. Writingnr_hugepagesis a request, not a guarantee — hugetlb pages must be physically contiguous, so the kernel reserves what it can and silently clamps the rest.With
huge_pages = on, a short reservation makes postgres refuse to start. Resizingshared_buffersupward could therefore have left the postmaster unable to come back. It now reads the count back and falls back tohuge_pages = tryon any shortfall, clearing the override again once a reservation succeeds.shared_buffersand its hugepage reservation are now derived from one place (config::shared_buffers_mb/nr_hugepages_for) instead of three, so they can't drift apart.Verification — on a live VM, not just tests
Retune (memory hot-plugged up underneath a running database, reproduced twice):
shared_buffersautoprewarm — warmed the pool, then forced a retune 14s later, inside the 60s dump interval so no periodic dump had fired and only the shutdown dump could save it:
Plus 106 bin + 41 lib tests green. Five new tests pin the retune boundary — including that a single small hotplug step is correctly declined as not worth bouncing the postmaster, and that a shrink retunes too.
🤖 Generated with Claude Code