feat(http): add max connection age for inbound HTTP/2 server connections - #4618
Open
ergdevops wants to merge 2 commits into
Open
feat(http): add max connection age for inbound HTTP/2 server connections#4618ergdevops wants to merge 2 commits into
ergdevops wants to merge 2 commits into
Conversation
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>
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>
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.
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 (checkedlinkerd/app/src/env.rsonmainand back throughv2.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_totalaccounting on the admin endpoint — the pooling itself works as designed; the cost is the unbounded buffer lifetime.Change
Adds
LINKERD2_PROXY_INBOUND_SERVER_HTTP2_MAX_CONNECTION_AGE(duration). Unset (the default) preserves current behavior exactly.When set, the inbound HTTP/2 serve loop arms a timer as an additional
select!branch alongside the existing drain/teardown triggers, reusing the samegraceful_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.
Results from our environment (15m age, production-like mesh)
Similar in spirit to gRPC's
MAX_CONNECTION_AGE; follows the same params/env plumbing pattern as #4542.