Repository navigation
fix(loadbalancer): clean up NFQLB targets before unlinking shm - #290
Merged
Merged
Conversation
zolug
approved these changes
Oct 14, 2026
DeleteInstance unlinked the shm segment before deactivating the instance's targets. `nfqlb deactivate` has to mmap that segment, so every deactivate call then failed with "FAILED mapSharedData" — and because deleteTargetNoLock returns early on that error, before its doDeletePolicyRoute loop, an ip rule and a routing table leaked for every target on every deletion of a DistributionGroup that had active targets. Not a race and not restart-dependent: it was the always-taken path on ordinary teardown. Reproduced on a kind cluster, one failed cleanup per DG deletion, 30 out of 30 rounds. The same code also removed the instance from nfqlb.instances up front, releasing its fwmark offset for immediate reuse. Combined with the leaked routes that is worse than a leak: getOffset can hand the same range to the next DistributionGroup, which then inherits live policy routes pointing at the previous DG's Pod IPs. Reorder to targets/routes -> flows -> shm unlink, and release the offset only once every step has succeeded, so a partial failure is retried instead of silently losing the instance while its kernel state survives. Also drops a stale comment that justified an early nfqlb.mu unlock by reference to a heal() function which does not exist in this repo. DeleteInstance now holds nfqlb.mu for its full duration, matching AddInstance, which already did. cleanupDistributionGroup previously logged the DeleteInstance error and dropped it, while clearing its tracking maps unconditionally. That made the failure unrecoverable: reconcileNFQLBInstance only acts on DGs absent from c.instances, and a deleted DG never reappears to re-trigger it, so nothing would ever call DeleteInstance again. The error is now returned for requeue and the maps are cleared only on success. The readiness-file removal is attempted independently of the NFQLB cleanup. Serialising them would let a stuck DeleteInstance keep a deleted DG's VIP advertised over BGP indefinitely, since readiness.Manager.IsReady() keys off file presence. Tests, all three verified to fail against the previous code: - internal/nfqlb: one spec asserts policy routes are still cleaned up and the offset stays reserved when the shm unlink fails; a second observes the real call order via a logging stub binary, since the unlink and flowList bypass the injectable execCmd and cannot otherwise be placed relative to the deactivate. - internal/controller/loadbalancer: a spec asserting a failed DeleteInstance is returned for requeue, keeps the DG tracked, and still removes the readiness file. The mock gains a deleteInstanceErr injection point. Split out of the original combined branch at review request. The NFQLB shm garbage collection (which needs rework so it cannot delete the segments nfqlb creates for itself), the Start() clean-exit handling, the maxTargets validation and the DistributionGroup churn e2e test follow as separate PRs. Signed-off-by: Botond Szirtes <botond.szirtes@est.tech>
bszirtes
force-pushed
the
fix-nfqlb-deleteinstance-ordering
branch
from
October 15, 2026 09:16
4a19e13 to
eb3df1d
Compare
This was referenced Oct 16, 2026
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.
fix(loadbalancer): clean up NFQLB targets and flows before unlinking shm
Split out of #281 at review request — this is 1a + 1d only. See
Relationship to #281 at the bottom for what follows separately.
The bug
DeleteInstanceunlinked the shm segment before deactivating theinstance's targets:
delete(nfqlb.instances, name)— release the fwmark offset immediatelynfqlb.mu.Unlock()nfqlb delete --shm=<name>— unlink the shm segmentdeleteTargetNoLock→nfqlb deactivate --shm=<name>Step 4 cannot work after step 3.
nfqlb deactivatehas tommapthe segment,which no longer exists, so it fails with
FAILED mapSharedData. AnddeleteTargetNoLockreturns early on that error — before itsdoDeletePolicyRouteloop — so theip ruleand routing table for everytarget leak.
This is not a race and not restart-dependent. It is the always-taken path on
ordinary teardown of any DistributionGroup that has active targets. Reproduced
on a kind cluster at 30/30 rounds: one failed cleanup per DG deletion.
Releasing the offset in step 1 compounds it.
getOffsetpacks ranges andreuses freed ones, so the next DistributionGroup can be allocated the same
fwmark range and inherit live policy routes pointing at the previous DG's Pod
IPs.
Why it matters beyond the leak
Policy rules and routing tables accumulate in the LB Pod's netns on every DG
deletion. Nothing reclaims them within the process lifetime —
CleanupStaleRulesonly runs at startup. Under steady-state DG churn the tables grow without bound,
and the offset reuse means a new DG can silently route to a dead Pod.
The fix
Reorder to targets/routes → flows → shm unlink, and release the offset only
once every step has succeeded, so a partial failure is retried instead of
silently losing the instance while its kernel state survives.
Also drops a stale comment that justified an early
nfqlb.muunlock byreference to a
heal()function that does not exist in this repo.DeleteInstancenow holdsnfqlb.mufor its full duration, matchingAddInstance, which already did.cleanupDistributionGrouperror handlingPreviously the
DeleteInstanceerror was logged and dropped, and the trackingmaps were cleared unconditionally. That made the failure unrecoverable:
reconcileNFQLBInstanceonly acts on DGs absent fromc.instances, and adeleted DG never reappears to re-trigger it, so nothing would ever call
DeleteInstanceagain. The error is now returned for requeue and the maps arecleared only on success.
The readiness-file removal is attempted independently of the NFQLB cleanup.
Serialising them would let a stuck
DeleteInstancekeep a deleted DG's VIPadvertised over BGP indefinitely, since
readiness.Manager.IsReady()keys offfile presence.
Background: why the ordering is load-bearing
nfqlb does not forward packets; it only marks them, and the kernel forwards
via policy routing. The fwmark value is the routing table ID (
routing.gosets
rule.Table = fwMarkandrule.Mark = uint32(fwMark)).So every fwmark nfqlb can emit needs a matching
ip rule+ routing table,created by
AddTargetand removed byDeleteTarget. Removing them requires asuccessful
deactivate, which requires the segment to still exist.Tests
All three were verified to fail against the previous code before being
committed — they are regression tests, not coverage padding.
internal/nfqlb— two specs:cleans up policy routes and keeps the offset reserved when the shm unlink
fails: injects a failing unlink, asserts routes were still deleted and the
offset was not handed to the next instance.
unlinks the shm segment only after targets and flows are cleaned up:
observes the real call order. The unlink and
flowListboth shell out viaexec.CommandContextand bypass the injectableexecCmd, so the only way toplace the unlink relative to the
deactivateis to have the fake binary logits own invocations —
execCmdis deliberately not injected here sodeactivatelands in the same ordered log.internal/controller/loadbalancer— one spec asserting a failedDeleteInstanceis returned for requeue, keeps the DG tracked, and stillremoves the readiness file. The mock gains a
deleteInstanceErrinjection point.Relationship to #281
#281 bundled five changes found in one investigation. Per review, they are being
split. This PR is 1a (
DeleteInstanceordering) + 1d(
cleanupDistributionGrouperror propagation), which the reviewer agreed couldgo together.