PMM-15050 Hi-load performance improvements - #5338
Conversation
When PMM Server restarts (e.g. during a version migration), all agents lose their long-lived gRPC Connect streams and reconnect at roughly the same time. Each reconnect goes through nginx auth_request -> pmm-managed AuthServer, which on cache miss calls Grafana /api/auth/serviceaccount. With the previous defaults a fleet of 800 agents would issue 800 simultaneous Grafana lookups, exhaust Grafana, and time out at the 3-second auth deadline. This change reduces the load on Grafana along three independent axes: - Singleflight around the cache miss in retrieveRole: concurrent calls for the same hashed credentials now collapse into a single Grafana request; followers wait for the leader and pick up the cached entry. - Longer auth cache TTL (3s -> 60s) and longer auth timeout (3s -> 15s), so a fresh agent's role no longer needs to be re-fetched every few seconds and a temporarily slow Grafana does not immediately translate to mass 401s. - Larger HTTP transport pool for the Grafana client (MaxIdleConns 50 -> 200, explicit MaxIdleConnsPerHost 100 instead of the Go default of 2), so reconnect bursts no longer force a fresh TCP/TLS handshake for almost every request. Drive-by: replaces interface{} with any in the Grafana client to clear pre-existing lint warnings in the files touched here.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5338 +/- ##
==========================================
+ Coverage 43.59% 44.16% +0.56%
==========================================
Files 415 416 +1
Lines 43134 43185 +51
==========================================
+ Hits 18804 19071 +267
+ Misses 22454 22231 -223
- Partials 1876 1883 +7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
errors.Cause from github.com/pkg/errors does not walk standard Unwrap chains. errors.As is the supported replacement, also used elsewhere in this file, and removes the need for the //nolint:errorlint suppression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each PMM Client authenticates with its own per-node service token, so the cache key (a hash of the Authorization/Cookie headers) is unique per client. Singleflight only coalesces concurrent calls sharing the same key, so for the migration scenario that motivated this PR — hundreds of clients reconnecting at the same moment — it provided essentially no deduplication. The longer cache TTL and bigger HTTP transport pool do the actual load-shedding. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When PMM Server restarts, all clients lose their gRPC streams at the same instant and start reconnecting against a small randomized window. With min=1s / max=15s and ±25% jitter, the first retry of a fleet of 800 agents was concentrated in roughly a 0.5-second window — a worst case of ~1600 reconnects/sec hitting auth_request and Grafana. Doubling the jitter (±25% -> ±50%) and raising the cap from 15s to 60s spreads later retries over a wider interval, so a server outage longer than 15s no longer keeps every agent hammering at the cap simultaneously. The jitter constant lives in the shared backoff package, so the change also applies to slowlog and process restart loops; wider jitter is strictly more decorrelating and has no downside for those local loops. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reverting the cap bump from the previous commit. With a 60s cap, an agent in repeated failure can hold up to a minute of metrics in its local buffer between reconnect attempts, which is a worse trade-off than the server-side benefit of spreading retries further apart. The wider jitter (±50%) introduced in the previous commit is kept; it still meaningfully spreads later retries within the 15s cap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Grafana 11 defaults max_open_conn to 0 (unlimited), so a burst of concurrent auth lookups - e.g. a fleet of agents reconnecting at the same time - can ask Grafana to open hundreds of fresh Postgres backends in parallel and saturate Postgres' max_connections, returning "too many clients" and tipping the reconnect loop into a livelock. Cap the pool at 100 open / 25 idle. Token validation is a fast query, so 100 concurrent backends is plenty even for thousands of agents while staying well within PMM Server's max_connections=2000 ceiling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The pool was hard-coded to 5 idle / 10 open. Under a reconnect storm from a fleet of agents, every DB-bound auth path (LBAC role lookup, settings read, agent-state write) queues at 10, and that queue sits on the same Postgres backends that Grafana's auth flow is competing for. Bump to 20 idle / 50 open. Stays well within PMM Server's max_connections=2000 and gives auth paths enough headroom to drain the burst instead of stalling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
14400s (4h) is already Grafana's default for conn_max_lifetime, so setting it explicitly is just clutter. The values that actually change behaviour are max_open_conn and max_idle_conn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A fleet of agents reconnecting after a server outage flushes their 1 GiB on-disk buffers to VictoriaMetrics in parallel, with no rate limit anywhere in the path. VM can be overwhelmed, return 503s, and feed the same reconnect loop that the auth-side fixes in this PR already address. Add --maxIngestionRate to vmsingle's command line with a default of 3M samples/sec (~3x steady state of an 800-agent fleet, well within PMM Server sizing). vmagent tolerates throttling natively by holding data in its 1 GiB disk queue and retrying, so a brief burst is spread over time instead of crashing VM. Tunable via the VM_maxIngestionRate environment variable, following the same pattern as the existing VM_search_* / VM_promscrape_* overrides plumbed through marshalConfig. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| db.SetMaxOpenConns(10) //nolint:mnd | ||
| // Sized to give DB-bound auth/role/settings paths enough headroom during | ||
| // a reconnect storm from a fleet of agents, while staying well within | ||
| // Postgres max_connections (set to 2000 by PMM Server). |
There was a problem hiding this comment.
This comment assumes Postgres max_connections=2000, which is only true for bundled Postgres. HA/external Postgres deployments may have much lower limits, and with Grafana 100 + pmm-managed 50 per node we can exceed them. Should we point this out in our docs?
There was a problem hiding this comment.
Good question! Yes, I think we need to make sure our Kubernetes distros configure the same parameter by default, then we can skip mentioning it explicitly in the docs. Otherwise we need to mention.
maxkondr
left a comment
There was a problem hiding this comment.
In general: this solution is very questionable. On one hand it increases the number of allowed incoming PG connections. On the other hand it increases the load on PG itself because there will be more requests from pmm-managed that shifts the initial problem described in ticket from pmm-managed+Grafana to Grafana+PG.
Here is the list of issues:
- auth cache in
grafana/auth_server.godoesn't hold negative responses from Grafana -> wrong credentials or DDOS-attack will always hit Grafana.
1.1. There is one common lock (mutex) on cache that creates contention lock on high load.
Good solution here would be using sharded cache with local shard's lock instead.
- there is no protection on pmm-managed side agains "Thundering herd" problem during massive agents registration and receiving massive incoming API requests - it just tries to swallow everything it receives (by increasing the PG connections and Grafana client connections limits) and shifts the problem to backing services (Grafana, PG).
Resilient systems are built using the following approaches.
On pmm-managed side:
- introduce rate-limiter + circuit breaker + re-tries backoff with increasing jitter in Grafana client. Each retry to Grafana has a local timeout (let's say 5s) and general time for processing request (let's say 15s).
- the same as above in
managed/services/agents/registry.go:register func. - cache positive and negative responses from backing services
- during pmm-agent registration pmm-managed may calculate some jitter/coefficient that client will use later for during re-registration (and other logics like sending QAN packets). This jitter may be based on number of pmm-agents registered in DB.
On pmm-agent side the same approaches:
- for connection attempts - implement circuit breaker + re-tries backoff increasing with jitter (some sort of that is already implemented)
Regarding DB migration issue on pmm-managed side.
DB migration is part of pmm-managed component startup logic. It shall not accept incoming connections/requests from agents/API until the migration is finished. the finished migration procedure shall be included into "healthy condition" to accept incoming requests.
a5cd1ea to
dd6e380
Compare
I'm not sure we can go ahead with such massive refactoring given that it's very difficult to reliably test the changes. I think we should consider the refactoring in a series of separate tickets. |
…r Grafana client
After a fleet-wide reconnect, vmagents replay their disk queues oldest-first, so VM can have no fresh *_up samples for many minutes while data is flowing and services are healthy. The Inventory Services page showed Unknown for ~20 minutes after a reconnect storm of 600 agents. ListServices now falls back to the most recent sample within a 30-minute lookback, gated on the service's pmm-agent being currently connected, so a dead client still reports Unknown on the same timeline as before. The stale query only runs when a supported service lacks a fresh sample. Also fix the supported-service check that read the zero-value metric's service type, which made the intended UNKNOWN branch unreachable and reported UNSPECIFIED for every service without metrics.
A 30m lookback only covers short outages: after PMM Server was down for an hour, the newest samples in VM were ~60 minutes old, last_over_time found nothing, and services showed Unknown again until the vmagent replay caught up. The window must cover the outage duration plus the replay backlog, so widen it to 24h. The connected-pmm-agent gate still keeps dead clients at Unknown, and fresh samples always take precedence, so the wider window only affects the lag period. Basing the fallback on agent status instead was considered and rejected: exporters serve *_up=0 and keep running indefinitely while their database is unreachable (verified with mongodb_exporter, mysqld_exporter and postgres_exporter), so agents.status=RUNNING cannot distinguish an up database from a down one and would report Up for a down database.
The Services page recovered after the stale-status fallback, but the
Nodes page kept showing Unknown during post-outage ingestion lag:
ListNodes and GetNode derive node status from a fresh up{job=~".*_hr$"}
sample and had no fallback.
Apply the same logic as ListServices: when a node has no fresh sample,
use the most recent one within staleStatusWindow, gated on a pmm-agent
providing that node's metrics being currently connected. Dead clients
keep reporting Unknown.
PMM-15050
SUBMODULES-4350
Summary
When PMM Server is restarted (for example during a version migration), all PMM Clients lose their long-lived gRPC
Connectstreams and reconnect at roughly the same instant. With the previous defaults a fleet of 800 agents would issue 800 simultaneous Grafana lookups, exhaust Grafana's request capacity, fan out into hundreds of Postgres backends, and trip Postgres'max_connections— turning the migration into a reconnect storm and a "too many clients" livelock. On top of that, each agent'svmagentholds up to 1 GiB of buffered metrics that flush to VictoriaMetrics in parallel as soon as the connection is re-established, which can swamp VM and re-feed the same loop.This PR addresses the storm across several layers.
pmm-managedauth cacheAuthServer. Once an agent's credentials have been validated, the next reconnect within a minute is a free cache hit instead of another Grafana round-trip. Entries are now also age-checked on read, so a hit never serves credentials older than the TTL — previously a stale entry could survive until the next background sweep (up to ~2× the interval).auth_request.MaxIdleConns50 → 100 with explicitMaxIdleConnsPerHost100 (Go default is 2). pmm-managed only ever talks to a single Grafana host, so the per-host cap is the real bound and both are set to the same value — aligned with Grafana'smax_open_conn = 100, the ceiling on auth lookups it can process in parallel. Reconnect bursts no longer force a fresh TCP connection for almost every request.Postgres connection pressure
grafana.ini:max_open_conn = 100,max_idle_conn = 25. Grafana 11's default formax_open_connis0(unlimited), so a synchronized burst of token validations can ask Grafana to open hundreds of Postgres backends and exhaustmax_connections.VictoriaMetrics ingestion
VM_maxIngestionRateenv var (following the existingVM_search_*/VM_promscrape_*pattern), wired to VM's--maxIngestionRateflag. The default is0— VM's native "no limit" — so out-of-the-box ingestion behavior is unchanged. A hard default cap was deliberately avoided:--maxIngestionRatepauses ingestion when the limit is exceeded rather than shedding load, so a fixed cap would throttle legitimate ingestion on large fleets — penalizing exactly the big deployments this work targets. Operators who need to smooth a reconnect burst can opt in by settingVM_maxIngestionRateto a value suited to their fleet; vmagent tolerates throttling natively by holding data in its 1 GiB disk queue and retrying.pmm-agentreconnect±25 %→±50 %) in the sharedagent/utils/backoffpackage. Spreads the first retry of 800 agents across a 1 s window instead of 0.5 s. The cap (backoffMaxDelay) stays at 15 s so an agent in repeated failure does not hold up to a minute of metrics in its local buffer.Drive-bys
errors.Causewitherrors.Asinauth_server.go.Notes
delayJitterconstant lives in the sharedagent/utils/backoffpackage, so wider jitter also applies to the slowlog reader retry and the exporter restart loop. Both are local to a single agent process; wider jitter is strictly more decorrelating there and has no downside.singleflightaround the cache miss. It was removed: each PMM Client authenticates with its own per-node service token, so cache keys are unique per agent and singleflight provides essentially no deduplication for the migration scenario.