Skip to content

feat(http): add max connection age for HTTP/2 server connections - #4618

Open
ergdevops wants to merge 4 commits into
linkerd:mainfrom
ergdevops:inbound-h2-max-connection-age
Open

feat(http): add max connection age for HTTP/2 server connections#4618
ergdevops wants to merge 4 commits into
linkerd:mainfrom
ergdevops:inbound-h2-max-connection-age

Conversation

@ergdevops

@ergdevops ergdevops commented Aug 31, 2026

Copy link
Copy Markdown

Addresses linkerd/linkerd2#15623.

Problem

Pooled proxy-to-proxy HTTP/2 connections currently live forever: TCP and HTTP/2 keepalives keep them healthy indefinitely, MAX_IDLE_CONNS_PER_ENDPOINT (default 10,000) only caps the count, the HTTP/1 pool idle timeout doesn't apply to them, and the outbound discovery idle timeout only fires when a service receives no traffic at all. As far as we can tell there is no setting that closes a pooled connection for being idle or bounds its lifetime (checked linkerd/app/src/env.rs on main and back through v2.311.0).

This matters for memory: each held server-side connection retains its receive-buffer high-water mark — bounded by the connection window (1MB default) — for its entire lifetime. In our mesh (arm64 EKS), a large caller fleet holds ~300 pooled connections into each server pod; after ~300 rps load bursts we measured ~700KB retained per connection, i.e. ~200MB of proxy RSS per pod that was only ever released when peer pods restarted. Verified via tcp_open_total/tcp_close_total accounting on the admin endpoint — the pooling itself works as designed; the cost is the unbounded buffer lifetime.

Change

Adds a max_connection_age setting to the shared HTTP/2 ServerParams, configurable per server prefix like every other SERVER_HTTP2 setting:

  • LINKERD2_PROXY_INBOUND_SERVER_HTTP2_MAX_CONNECTION_AGE (duration) — the inbound proxy-to-proxy listener, where pooled peer connections accumulate;
  • LINKERD2_PROXY_OUTBOUND_SERVER_HTTP2_MAX_CONNECTION_AGE (duration) — the outbound (app-facing) listener, for symmetry with the other paired settings.

Unset (the default) preserves current behavior exactly. The admin server builds its params with Default::default() and is unaffected regardless of environment.

When set, the HTTP/2 serve loop arms a timer as an additional select! branch alongside the existing drain/teardown triggers, reusing the same graceful_shutdown() (GOAWAY) path: no new streams are accepted, in-flight streams run to completion, then the connection closes and the client reconnects transparently — the identical sequence every pod drain already exercises. A deterministic per-connection jitter of up to +10% (derived from the client's ephemeral port; no new dependency) prevents synchronized shutdown storms. Each shutdown is logged at DEBUG with the client address.

Includes an integration test covering: requests served normally before the age elapses, an in-flight stream completing across the age boundary, and the connection closing once drained — plus an env test asserting the setting parses independently under both server prefixes.

Results from our environment (15m age, production-like mesh)

  • Proxy RSS became a bounded ~15-minute sawtooth instead of a monotonic plateau — buffers released every cycle with no pod restarts. (Observed on a build with jemalloc enabled on aarch64 — see feat(allocator): enable jemalloc on aarch64-unknown-linux-gnu #4613 — so freed memory was actually returned to the OS.)
  • Zero failed requests across many recycle cycles; p50/p99 unchanged through recycle waves.
  • Cost: one TLS handshake per connection per age period, spread over ~90s by the jitter — below measurement noise in-cluster.

Similar in spirit to gRPC's MAX_CONNECTION_AGE; follows the same params/env plumbing pattern as #4542.

Pooled proxy-to-proxy HTTP/2 connections have no idle timeout or age
bound: peers keep them alive indefinitely (keepalives prevent closure;
MAX_IDLE_CONNS_PER_ENDPOINT only caps the count), and each held
connection retains its receive-buffer high-water mark - up to the
connection window - for its entire lifetime. In a mesh where a large
caller fleet held ~300 connections into each server pod, we measured
~700KB retained per connection after ~300 rps load bursts (~200MB of
proxy RSS per pod), released only when peer pods restarted.

This adds LINKERD2_PROXY_INBOUND_SERVER_HTTP2_MAX_CONNECTION_AGE
(duration; unset preserves current behavior). When set, the inbound
HTTP/2 serve loop arms a timer as an additional select branch alongside
the existing drain/teardown triggers and reuses the same graceful
shutdown (GOAWAY) path: in-flight streams complete and clients
reconnect transparently. A deterministic per-connection jitter of up to
+10% (derived from the client's ephemeral port, avoiding a new
dependency) prevents synchronized shutdown storms. Shutdowns are logged
at INFO with the client address.

Signed-off-by: ergdevops <115262164+ergdevops@users.noreply.github.com>
@ergdevops
ergdevops requested a review from a team as a code owner August 31, 2026 10:02
At scale this fires once per connection per age period, which is too
chatty for INFO; operators who want per-shutdown visibility can enable
it via the proxy log filter.

Signed-off-by: ergdevops <115262164+ergdevops@users.noreply.github.com>
Comment thread linkerd/http/h2/src/lib.rs Outdated
Comment thread linkerd/proxy/http/src/server.rs Outdated
Comment thread linkerd/proxy/http/src/server.rs Outdated
Comment thread linkerd/proxy/http/src/server.rs
- Drop the doc comment on ServerParams::max_connection_age to match the
  struct's style.
- Rename pa/peer to peer_addr for clarity and consistency.
- Deduplicate the H2 serve loop's shutdown handling with a Teardown enum
  classifying the completion branches (ClientClosed / Drain / Graceful),
  as suggested in review.

Signed-off-by: ergdevops <115262164+ergdevops@users.noreply.github.com>
@ergdevops
ergdevops force-pushed the inbound-h2-max-connection-age branch from 12a80c6 to a758233 Compare September 3, 2026 08:46
@ergdevops
ergdevops requested a review from thesw4rm September 3, 2026 10:31
Comment thread linkerd/app/src/env/http2.rs
Like every other SERVER_HTTP2 setting, max connection age parses under
both the inbound and outbound server prefixes via the shared
parse_server; make that symmetry explicit with a test exercising the
real variable names.

Signed-off-by: ergdevops <115262164+ergdevops@users.noreply.github.com>
@ergdevops ergdevops changed the title feat(http): add max connection age for inbound HTTP/2 server connections feat(http): add max connection age for HTTP/2 server connections Sep 4, 2026
@ergdevops
ergdevops requested a review from thesw4rm September 4, 2026 09:42
@ergdevops

Copy link
Copy Markdown
Author

@thesw4rm With the symmetry addressed and tests covering both prefixes, I believe the feature is complete and the PR title now matches the actual scope of the change — but happy to take any further feedback.

Separately, since you've contributed here before: could you help me understand the cadence the linkerd maintainers typically follow for reviewing and merging feature PRs like this? I have this PR and one other (#4613) waiting on maintainer review, and I'd like to set my expectations right — and do whatever I can from my side to make them easy to review.

@thesw4rm

thesw4rm commented Sep 4, 2026

Copy link
Copy Markdown

@ergdevops I have never contributed here before haha. I'm just going around reviewing PRs

@ergdevops

Copy link
Copy Markdown
Author

@thesw4rm With the symmetry addressed and tests covering both prefixes, I believe the feature is complete and the PR title now matches the actual scope of the change — but happy to take any further feedback.

Separately, since you've contributed here before: could you help me understand the cadence the linkerd maintainers typically follow for reviewing and merging feature PRs like this? I have this PR and one other (#4613) waiting on maintainer review, and I'd like to set my expectations right — and do whatever I can from my side to make them easy to review.

@adleong apologies for the tag but could you please help me with this request, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants