tonic-xds/xds-client: client permanently stuck on stale endpoints after backend pod rotation — no keepalives or connect timeouts anywhere in the xDS path#2746
Open
aryehlev wants to merge 2 commits into
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.
Labels: bug, tonic-xds, xds-client
Environment
Symptom
The client works fine until the backend server deployment does a rolling restart. Once the old pods are replaced, the client never routes to the new pods: every request hangs until the caller's deadline and times out. The client never recovers without a restart.
Root cause analysis
The xDS protocol machinery itself looks correct (ACK/NACK, version/nonce, reconnect-with-resubscribe, EDS-not-full-state per gRFC A53, endpoint diffing, health-status filtering). The failure is that dead connections are never detected, in two places:
TonicTransportBuilder::build (xds-client/src/transport/tonic.rs) constructs the channel with just Endpoint::from_shared(...).connect() — no http2_keep_alive_interval, no keep_alive_timeout, no keep_alive_while_idle, no connect_timeout. The worker reads the stream with a bare stream.recv() inside tokio::select! with no timeout arm (xds-client/src/client/worker.rs, run_connected).
The ADS stream is mostly idle from the client's perspective, so if the connection goes half-open during cluster churn (istiod restart where the GOAWAY/RST is lost, dropped conntrack/NAT entry, LB reset) — tonic gets no error and no bytes, recv() pends forever, and the otherwise-correct reconnect + re-subscribe logic never fires. The client then serves its last-known EDS endpoints indefinitely. Combined with (2), that's a permanent outage.
Per-endpoint channels are created with connect_lazy() and no connect timeout or keepalive (PlaintextConnector / TlsConnector in tonic-xds/src/xds/cluster_discovery.rs). The production balancer is raw tower::balance::p2c::Balance (tonic-xds/src/client/cluster.rs), and a lazy tonic channel's poll_ready reflects buffer capacity, not TCP connectivity — so every endpoint is "ready" the moment it's inserted and stays ready forever. Kubernetes black-holes deleted pod IPs (SYNs dropped, no RST), so a request picked onto a dead endpoint hangs until the caller's deadline. Nothing ejects the endpoint.
Notably, tonic-xds/src/client/loadbalance/ contains a full connectivity-aware load balancer with outlier detection (gRFC A50), but the module is #[allow(dead_code)] and never constructed by the production stack.
With insecure channel creds no TLS is configured, but ensure_secure_server_uri (xds-client/src/transport/tonic.rs) only adds an https:// scheme on the secure path — it never strips one on the insecure path. So the bootstrap above hands tonic an https URI with no TLS backend. Per xDS bootstrap semantics, channel_creds should decide transport security, not the URI scheme.
Failure sequence
Deployment rolls → old pod IPs become black holes → requests picked onto them hang (no connect timeout, no ejection) → the ADS connection is also disturbed by the churn and goes half-open → no EDS update ever arrives to replace the endpoint set (no keepalive to detect the dead stream) → client permanently routes 100% of traffic to dead IPs.