feat(prometheus_scrape source): add scrape_delay to stagger scrapes - #26110
Open
taloric wants to merge 1 commit into
Open
feat(prometheus_scrape source): add scrape_delay to stagger scrapes#26110taloric wants to merge 1 commit into
scrape_delay to stagger scrapes#26110taloric wants to merge 1 commit into
Conversation
prometheus_scrape issues its first scrape the moment the source starts and then scrapes on a fixed cadence, so every source sharing a scrape_interval_secs fires at the same instant. A Vector instance running many of them concentrates all scrape load into a single spike each interval. Add a scrape_delay option with three forms: - none (default) keeps today's behavior exactly. - A duration such as 30s holds the first scrape back by that amount and then keeps the same fixed cadence, so sources can be staggered by hand. - auto picks a position inside each interval derived from a hash of the host name, the component ID and the scrape number, so the source no longer sits at one fixed phase relative to other periodic work. Scheduling lives in the shared GenericHttpClientInputs as two new fields, initial_delay and jitter_seed. Only jitter_seed: Some(..) (that is, scrape_delay: auto) uses the new grid-anchored scheduler, which skips intervals that elapse while the stream is not polled. Every other caller, including the http_client source and the two non-auto scrape_delay settings, still goes through tokio::time::interval_at and keeps MissedTickBehavior::Burst catch-up unchanged.
Contributor
|
All contributors have signed the CLA ✍️ ✅ |
Author
|
I have read the CLA Document and I hereby sign the CLA |
Author
|
recheck |
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.
Summary
The problem
prometheus_scrapestarts scraping the moment the source is built and then scrapes everyscrape_interval_secsexactly.tokio::time::intervalfires its first tick immediately, so several sources built in the same topology with the same interval start together and stay in lockstep for the lifetime of the process. There is no per-source offset, delay, or jitter available to break that up.That turns a Vector instance running N
prometheus_scrapesources into one that does all of its scrape work in a single burst every interval and nothing in between. The burst is not only network I/O: the full HTTP body is read, parsed into metric events, and pushed through the transform and encode path before anything downstream can drop it, so afilterlater in the topology does not avoid the cost that was already paid.Why it is worth fixing
This came out of a root-cause investigation on a production Kubernetes node. The node ran an observability agent with an embedded Vector scraping cadvisor, kubelet and kube-state-metrics, all three on a 10s interval, so all three fired together:
Roughly 10.6 MB and 34,800 samples read and parsed in one burst, once every 10 seconds, then idle. Under a cgroup CPU quota that burst does not show up as CPU usage — it shows up as threads that are runnable but waiting for the next quota period. Node
load1peaked at 22.87 while CPU sat at about 80% idle, and the averaged workload counters were flat across peak and trough:The scrape phase was directly measurable. Over one hour, every one of the 182 samples where the agent had 8 or more runnable threads landed on
second mod 10of 7 or 8 — the same second-of-interval Vector had started on, with nothing on any other second.What made this hard to diagnose is that a fixed phase aliases against any other periodic observer. On Linux 4.19 the load average is recomputed every
LOAD_FREQ = 5*HZ+1jiffies, which atCONFIG_HZ=1000is 5.001s, not 5s. Sampled against a 10.000s burst, the phase walks by 2ms per pair of samples, so it takes 25,005s — 6h56m45s — to drift into the burst and back out. Fitting 103 peaks across 29 days ofsar -qhistory gave a measured envelope of 25,004.485s against a theoretical 25,005s. The node looked like it had a mysterious 7-hour workload; it had a 10-second one observed through a 5.001-second sampler.The aliasing is a property of the observer rather than of Vector, and it is only one way this surfaces — the general point is that a deterministic, permanently in-phase burst concentrates load in a way that is both avoidable and hard to attribute. Spreading the sources removes the concentration at the source, and setting the three sources above to
autoresolved the envelope on the affected fleet.How the option maps onto that
A fixed delay is the minimal, single-variable fix: it shifts phase only, leaving the interval, the endpoints, the timeout and the total work per interval untouched, which makes it straightforward to A/B against the unmodified configuration. Giving the three sources above
scrape_delayvalues of0s,3sand6sstaggers them without changing anything else about what they do.autois for the case where hand-assigning offsets across many sources or many hosts is not practical. It trades the fixed spacing between consecutive scrapes for not needing coordination, so it is a different trade-off rather than a strictly better one — the option docs spell out what it gives up.Three properties the investigation asked for, and which the implementation provides:
Implementation
The scheduling is added to the shared
GenericHttpClientInputsas two fields,initial_delayandjitter_seed. A newticks()seam picks the tick stream:jitter_seed: Some(..)— onlyscrape_delay: auto— uses the new grid-anchoredschedule(), which drops grid points that go by while the stream is not polled rather than firing them late back to back.jitter_seed: None— thehttp_clientsource, andscrape_delay: noneor a fixed duration — goes throughtokio::time::interval_atverbatim, keepingMissedTickBehavior::Burstcatch-up.That split is deliberate: it keeps this PR's behavior change scoped to the opt-in setting, so the unrelated
http_clientsource and the defaultprometheus_scrapeconfiguration are untouched under backpressure. Two tests pin the contract in both directions —ticks_without_a_seed_keep_the_tokio_catch_up_burstasserts the burst is still replayed,ticks_with_a_seed_skip_missed_callsasserts it is not.References
Closes: <#26109>
Vector configuration
How did you test this PR?
Unit tests, all new:
util/http_client.rs— 9 tests overschedule(),window_offset()andticks(), using#[tokio::test(start_paused = true)]withtokio::time::advance()so the timing assertions are deterministic rather than wall-clock dependent. They cover the fixed cadence with a zero jitter window, rejection of a zero interval, honoring the start instant, varying gaps between calls, missed calls being skipped, jitter not accumulating across intervals, and offsets covering the window without leaving it.prometheus/scrape.rs— 6 tests overScrapeDelayparsing and string round-trips (none/auto/ durations), rejection of malformed and out-of-range values, the derivedfirst_scrape_delay(), and an end-to-endscrape_delay_defers_the_first_requestagainst a local server.Commands run locally, all clean:
The generated component docs were regenerated with
cargo vdev build component-docs; the only website change is thescrape_delayblock in theprometheus_scrapesource reference.Field verification
The staggering is deployed on the fleet where the investigation above was done. All three sources were set to
scrape_delay: auto, with the scrape interval, endpoints, timeout, filter rules and CPU quota left unchanged, so scrape phase was the only variable. The three sources share a host name and differ only by component ID, which is what gives them different positions inside the interval.Observed over
<N>hours, covering<M>full cycles of the original envelope:load1envelope no longer forms at the predicted peak centers.This is an internal cluster, so I can't attach the raw
sar -qand scheduler captures, but I'm happy to answer questions about the setup or how any of it was measured.Is this a breaking change?
scrape_delaydefaults tonone, which is the existing behavior. The shared HTTP client scheduler change is gated on the opt-inautovalue, so thehttp_clientsource is unaffected.Does this PR include user facing changes?
no-changeloglabel to this PR.