xds/resolver: drop field references on Close to prevent retention across ClientConn recycle - #9301
xds/resolver: drop field references on Close to prevent retention across ClientConn recycle#9301sushanb wants to merge 2 commits into
Conversation
…oss ClientConn recycle When callers repeatedly Close a ClientConn dialed with an xds:/// target and immediately redial a fresh one (as bigtable's ConnectionRecycler does every few minutes), the closed xdsResolver instances are pinned in memory across recycles. Over hours this accumulates significant heap: in the standalone reproduction at github.com/sushanb/bigtable-recycle-repro (50 DirectPath channels, 10s recycle interval, 8h run), retained heap grew from ~90 MiB to 6.3 GiB on grpc-go v1.81.1 baseline. The retention is via multiple field references off *xdsResolver that survive Close: r.cc (-> ClientConn's ServiceConfig chain), r.curConfigSelector (-> ServiceConfig via UpdateState), and transitively r.dm / r.xdsClient watcher metadata held by the shared xdsclient.DefaultPool. Individually each field is a benign back-reference; together they form multiple retention paths through externally-held state. Go's GC handles cycles, but these are external-anchored chains, not cycles. Explicitly dropping the field references at Close leaves the resolver struct with no outbound edges and allows the entire per-channel resolver + CDS balancer subtree (~40 CallbackSerializers, JSON-parsed configs, attribute chains, cloned address slices per channel) to be collected. In the same 8h A/B, retained heap growth dropped 86%-91%: HeapAlloc growth baseline +5487 MiB -> with fix +475 MiB (91% less) HeapSys growth baseline +6243 MiB -> with fix +891 MiB (86% less) HeapObjects baseline +61.5M -> with fix +5.9M (90% less) This is a distinct, orthogonal bug from grpc#9140 (activeClusters refcount on early stream failure). Both should ship. Includes a regression test that reproduces the "shared xdsClient pool + close/redial one of many channels" pattern in-process. The synthetic setup is much smaller than production DirectPath, so per-iter numbers are within GC noise regardless of the fix; the test guards against a future regression that would grow retained heap by >=20 KiB per close+redial. Full evidence including hourly heap snapshots and pprof composition diff lives at github.com/sushanb/bigtable-recycle-repro.
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9301 +/- ##
==========================================
+ Coverage 83.11% 83.14% +0.02%
==========================================
Files 423 423
Lines 35231 35246 +15
==========================================
+ Hits 29284 29305 +21
+ Misses 4432 4427 -5
+ Partials 1515 1514 -1
🚀 New features to boost your workflow:
|
|
@mbissa : Could you please prioritize this review. GCS is affected by this memory leak. |
|
Hi @sushanb , thank you for your contribution and for opening this PR! Before we can move forward, could you please sign the CLA? |
|
I signed the cla. @eshitachandwani |
|
For
Can we instead try to free up the service config , maybe by doing something like |
Per review feedback on grpc#9301: release the resolver-provided ConfigSelector at ClientConn.Close by swapping to a defaultConfigSelector{nil}. The old ConfigSelector transitively retains resolver-owned state (parsed service configs, cluster/plugin maps for xds; equivalents for other resolvers), so this helps every resolver, not just xds. The existing xdsResolver.Close field cleanup remains necessary because the resolver itself is still pinned by the shared xdsClient pool's watcher metadata.
|
Thanks @eshitachandwani — good call, pushed as Kept the Build + |
Summary
When callers repeatedly
Close()aClientConndialed with anxds:///target and immediately redial a fresh one (as Bigtable'sConnectionRecyclerdoes every few minutes), the closed*xdsResolverinstances are pinned in memory across recycles. Over hours this accumulates significant heap. On grpc-go v1.81.1 baseline, an 8-hour run of the recycle reproduction (50 DirectPath channels, 10s recycle interval) grew retained heap from ~90 MiB → 6.3 GiB.Full reproduction: https://github.com/sushanb/bigtable-recycle-repro
Root cause
Retention is via multiple field references off
*xdsResolverthat surviveClose:r.cc→ theClientConn, whoseServiceConfigstill transitively references the resolverr.curConfigSelector→ captured by theClientConn'sServiceConfigviaUpdateStater.dm/r.xdsClient→ watcher metadata held by the sharedxdsclient.DefaultPoolsingleton, which survives every ClientConn lifetimeIndividually each field is a benign back-reference; together they form multiple independent retention chains through externally-anchored state. Go's tracing GC handles cycles fine, but these aren't cycles — they're chains rooted at package-global state (the shared xdsClient pool) and other ClientConns that stay alive.
Explicitly dropping the field references at the end of
Closeleaves the resolver struct with no outbound edges. The entire per-channel resolver + CDS balancer subtree (roughly 40CallbackSerializers, JSON-parsed service configs,attributes.Attributeschains, cloned[]resolver.Addressslices per channel) becomes GC-collectible.Impact (measured)
Same repro, same workload, 8-hour A/B, 50 channels, 10s recycle interval, DirectPath xDS engaged:
Hourly heap snapshots and
pprof -basecomposition diffs are in the linked repro.Related but distinct
This is orthogonal to #9140 (activeClusters refcount on early stream failure). Both bugs exist in v1.83; both should ship. Applying #9140 alone does NOT fix this leak — verified with an 8-hour run against grpc-go master @ the #9140 merge commit
89d4d61e.Test
Included
internal/xds/balancer/cdsbalancer/e2e_test/close_redial_leak_test.go— reproduces the "sharedxdsclient.Pool+ close-and-redial one of many channels" pattern in-process:NewXDSResolverWithPoolForTestingwith a shared pool so every dial sub/unsubscribes against the same refcounted xdsClient (matches production wiring;NewXDSResolverWithConfigForTestingwould build a fresh pool per iteration and hide the leak surface).ClientConnpins the pool refcount above zero across the run, mirroring the "N-1 other channels stay open" invariant from Bigtable.HeapInusegrowth < 20 KiB.Caveat: the synthetic in-process xDS server is much smaller than production DirectPath (a handful of clusters vs many, minimal endpoint set), so per-iter numbers are within GC noise regardless of whether the fix is applied. The test's primary purpose is a future-regression guard at 20 KiB/iter — any change that reintroduces a retention chain of the observed magnitude (~60 KB/iter in the standalone repro) will exceed that threshold. The primary evidence for the fix's magnitude is the standalone-repro A/B linked above.
Test plan
go test -run "Test/CloseRedialDoesNotRetainXDSState" ./internal/xds/balancer/cdsbalancer/e2e_test/passes with the fix./internal/xds/resolver/...build cleanRELEASE NOTES: None