You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(balancer): keep least_conn load state across upstream scaling
When least_conn proxies long-lived connections (WebSocket) and the upstream
is scaled, the load stays skewed on the original nodes: the newly added ones
are not preferred and least_conn degrades to round-robin.
The picker is cached by the upstream version, so it is rebuilt whenever the
upstream changes - and scaling changes it. The binary heap holding the
per-server scores lives inside the picker, so every score is reset to the base
weight on rebuild and the connections already established are forgotten. The
requests that are still in flight keep the picker they were routed with and
release their server on it in the log phase, so their releases land on a heap
nobody reads anymore, while the rebuilt heap never learns about them.
Move both the heap and the in-flight connection counts out of the picker into
a per-worker state keyed by the upstream resource key, which is stable across
scaling (and across health status flips) unlike the picker version. The picker
now reconciles that heap with the current node set instead of rebuilding it:
surviving nodes keep their load, a freshly added node starts empty and is
preferred right away, and a node that leaves keeps its count so its score is
restored if it comes back. Every generation of pickers shares one view of the
load, so a connection established before a rebuild is released against the
heap that is actually in use.
The score is now derived from the connection count instead of being
accumulated with +/- effect_weight, which keeps it exact over time.
Because that state now outlives the picker, releasing a server the request no
longer holds is no longer self-healing: it used to be washed away by the next
rebuild, now it is written into shared state for good. pick_server released
the current server when it entered a retry but left ctx.balancer_server set,
so a request that then ran out of servers to try released it a second time in
the log phase. It only takes an active health check marking a node unhealthy
to reach that path, since the retry count is derived from all nodes while only
the healthy ones are picked from. Clear ctx.balancer_server on release, so it
always names the server the request currently holds, and skip the release when
it names none.
The priority is part of the state key, since node sets of different priorities
are disjoint and must not share a heap. A node that moves between priorities
leaves its count behind in the old level, where it drains normally.
Also call after_balance in the stream log phase, which it never did: for L4
the count was only ever incremented, so least_conn could not balance TCP long
connections at all and ctx.balancer_tried_servers was leaked.
Fixes#12217
0 commit comments