Repository navigation
Pace the read-model backfills so they cannot starve a small server's disk - #257
Merged
Merged
Conversation
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.
Pace the one-off backfills so they cannot starve a small server's disk
Why
On the dev box (8 vCPU, 3.8 GB RAM, a rotational QEMU disk) the first
packet_sitesfill drove I/O pressure tofull avg10=78%. The whole VM stalled twice: sshd, journald and the app all froze, and the box had to be rebooted.Until now the backfills paused a fixed 100 ms between slices, whatever each slice cost. A
packet_sitesslice of 500,000 observation ids takes about 14 s of solid I/O on a laptop SSD, and far longer on a slow disk, so 100 ms of rest is nothing.What changes
Every one-off fill now paces itself by duty cycle: the
packet_sitesbackfill, thechannel_message_iatas(cmi) backfill and the observation payload-type backfill. After each slice or batch it sleepswhere
elapsedis how long that slice took. No single pause is longer than 10 minutes, and the arithmetic cannot overflow. Withduty: 0.5the fill is busy at most half the time. A slow disk makes each slice slower, so the pauses stretch on their own. A stop request still ends the pause at once.The new settings sit under
readmodels.fill:duty0.5min_pause100ms0sto1mpacket_sites_slice500000cmi_batch10000config: loadedline (fillDuty,fillMinPause,fillPacketSitesSlice,fillCmiBatch).config.yaml.example.Progress logging. Each backfill logs at most once a minute with
ids_done,ids_remainingand anetacomputed from the rate so far. Forpacket_sitesand cmi this replaces the old line written after every 30 s step. The payload-type line gains the same three fields. The ETA comes from the rate the fill's position moves, applied to the current remaining count, so it holds while ingest adds ids. Progress resets when a fill starts over or completes.Nothing else changes. Pacing changes timing only. The fills run the same statements over the same ranges, the trust and verification rules are untouched, and progress is still saved after every slice. Every existing read-model test passes without changes.
Reconcile and audit are left as they were.
A second cause: generic plans on a new, unanalyzed table
The stall. While measuring, the 6th 500,000-id
packet_sitesslice ran past a 2-minute statement timeout. The five slices before it took 4 to 7 s each.The cause. I reproduced it with autovacuum off on
packet_sites, which stands in for a fill that outruns autoanalyze.The fix (second commit):
packet_sitesandchannel_message_iatas) setsplan_cache_mode = force_custom_planfor itself and resets it on close.SET LOCAL plan_cache_mode = force_custom_plan.ANALYZEruns only on a table that has never been analyzed (reltuples = -1). It is checked at session start, where it runs only if the table already holds rows, and again after the session's first slice or batch.ANALYZEburst on a large table.ANALYZEruns with no statement timeout and a 10 s lock timeout. A failure is logged and the fill goes on.How it relates to the dev-box freezes. This is the likely second cause of the stalls, alongside the slow disk:
There is one caveat. The
maintpool already sendsplan_cache_mode=force_custom_planas a startup parameter.ignore_startup_parameters), or when the store runs on another pool.The session-level setting makes custom plans hold either way.
Tests for this fix.
TestFillSessionsPlanCustomPostgresuses a pool that forces generic plans, standing in for the old path.$1).plan_cache_mode = force_custom_plan, and its plan has the slice bounds as constants.Close.fill session plan_cache_mode force_generic_plan.TestFillAnalyzeOnlyNeverAnalyzedPostgrescovers whenANALYZEruns.ANALYZEacross two rebuild sessions.The payload-type step checks that
current_setting('plan_cache_mode')isforce_custom_planinside the backfill transaction, that every untyped row was typed, and that the pool's own setting is back afterwards.TestFillAnalyzeFailureIsIgnoredPostgresholds a lock that blocksANALYZE. TheANALYZEwas tried and failed (reltuples still -1), and the backfill still completed with an exact table.Measurements
Measured on a scratch Postgres on a laptop SSD with 3,000,000 synthetic observations and the
packet_sitesbackfill. "Busy" is the time spent in fill statements. These runs had custom plans forced on the session by hand, which the second commit now does itself.dutyRe-measured with the fix in the stall's own conditions: autovacuum off on
packet_sites, a freshreltuples = -1table, a pool with automatic plan caching, and no manual setting.dutyBefore the fix, the 6th slice ran past the 2-minute timeout in the same conditions.
Operator note
On a slow disk (rotational, network or shared storage), set
duty: 0.25. Considerpacket_sites_slice: 100000too, for shorter bursts. While a fill runs, watchfull avg10in/proc/pressure/io, and lowerdutyfurther if it climbs toward 50 %.Tests
TestPacedBackfillSlicesPostgreschecks that the slice, batch and floor come from the store's pacing and that the table ends exact.TestPacedFillsEarnTrustPostgresboots both read models over data with no read-model rows, usingduty: 0.25and tiny slices. Both fills complete, every page matches the observations, and trust is earned.-runlists.