This is an automated email from the ASF dual-hosted git repository.
Similarityoung pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git
The following commit(s) were added to refs/heads/develop by this push:
new f49ac46a7 perf(loadbalancer): O(1) snapshot pick recheck via
healthy-by-ID index (#970)
f49ac46a7 is described below
commit f49ac46a7fcfa09bf5d87ef147f9225d7116a80d
Author: 承潜 <[email protected]>
AuthorDate: Sun Jul 26 13:36:13 2026 +0800
perf(loadbalancer): O(1) snapshot pick recheck via healthy-by-ID index
(#970)
* perf(loadbalancer): O(1) snapshot pick recheck via healthy-by-ID index
healthyEndpointFromSnapshot validated a balancer's pick by scanning the
healthy slice twice: a pointer-equality pass, then a sameEndpointIdentity
pass (with a per-element SocketAddress.Equal). Zero-copy balancers hit the
pointer pass early, but non-zero-copy snapshot balancers return an element
of the cloned HealthyEndpoints slice, whose pointer never matches the
context slice — so every pick fell through to the full O(N) identity scan.
Expose the snapshot's existing healthyEndpointByID index as an O(1),
allocation-free accessor (HealthyEndpointByIDForPick) and thread it into
PickContext via a small HealthyEndpointByIDLookup interface (declared in
loadbalancer so it keeps no dependency on pkg/cluster). The recheck now
resolves the single candidate by ID and applies the unchanged
sameEndpointIdentity rules to it; it falls back to the original scan when
no index is present (hand-built contexts) or the pick has no ID (the
empty-ID / placeholder-address path), preserving every existing rule
including the blank-domain wildcard and the reject-on-not-found defense.
BenchmarkHealthyEndpointFromSnapshot, 1024 healthy endpoints, non-zero-copy
return (ID at the far end):
identity-scan-without-index 1368 ns/op
identity-recheck-with-index 36 ns/op
The result clone (1 alloc) is unchanged; the win is the removed O(N) walk,
which matters for large clusters under high QPS.
Closes #955
* fix(loadbalancer): clear HealthyByID in defensive snapshot context
defensiveSnapshotPickContext clones the endpoint slices to isolate
non-zero-copy balancers from live snapshot pointers, but it did not clear
the newly-added HealthyByID field. This allowed untrusted balancers to call
HealthyByIDForPick and obtain un-cloned snapshot-owned endpoints, breaking
the memory-isolation guarantee that the defensive copy exists to enforce.
The recheck (healthyEndpointFromSnapshot) still works correctly because both
call sites pass the original context, whose HealthyByID remains intact. Only
the balancer-facing defensive copy is affected.
Current impact: zero (all in-tree balancers are zero-copy and none read
HealthyByID). This change closes a latent trust-boundary leak before
external
plugins can exploit it.
* perf(loadbalancer): keep by-id zero-copy pointer fast path
* chore(loadbalancer): address sonar maintainability issues
---
pkg/cluster/cluster.go | 14 +++
pkg/cluster/cluster_test.go | 25 ++++
pkg/cluster/loadbalancer/load_balancer.go | 58 ++++++++-
pkg/cluster/loadbalancer/load_balancer_test.go | 163 ++++++++++++++++++++++---
pkg/server/cluster_manager.go | 1 +
5 files changed, 242 insertions(+), 19 deletions(-)
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index 4868107e6..4dc783820 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -479,6 +479,20 @@ func (s *EndpointSnapshot) HealthyEndpointByID(endpointID
string) *model.Endpoin
return model.CloneEndpoint(s.healthyEndpointByID[endpointID])
}
+// HealthyEndpointByIDForPick returns the snapshot-internal healthy endpoint
for
+// endpointID without cloning, or nil when no healthy endpoint carries that ID.
+// Like HealthyEndpointsForPick, the returned endpoint is owned by the
snapshot:
+// callers MUST NOT mutate or retain it past the current pick. This O(1) lookup
+// backs the request-path recheck that validates a balancer's pick by ID
instead
+// of scanning the healthy slice; external callers should use
HealthyEndpointByID
+// (defensive clone).
+func (s *EndpointSnapshot) HealthyEndpointByIDForPick(endpointID string)
*model.Endpoint {
+ if s == nil {
+ return nil
+ }
+ return s.healthyEndpointByID[endpointID]
+}
+
func (s *EndpointSnapshot) withEndpointHealth(
endpointID, endpointAddress string,
healthy bool,
diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go
index 196c9da4a..bd0b03f3a 100644
--- a/pkg/cluster/cluster_test.go
+++ b/pkg/cluster/cluster_test.go
@@ -52,6 +52,31 @@ func TestClusterEndpointSnapshotSeedsFromEndpointHealth(t
*testing.T) {
assert.Nil(t, snapshot.HealthyEndpointByID(unhealthy.ID))
}
+// TestClusterEndpointSnapshotHealthyEndpointByIDForPick locks the zero-copy
+// O(1) recheck accessor: it returns the snapshot-owned healthy endpoint
pointer
+// (the same one HealthyEndpointsForPick exposes, not a clone) and nil for
+// unhealthy or unknown IDs. The request-path recheck (issue #955) relies on
+// this being allocation-free and identity-stable.
+func TestClusterEndpointSnapshotHealthyEndpointByIDForPick(t *testing.T) {
+ healthy := testEndpoint("ep-1", "127.0.0.1", 18080)
+ unhealthy := testEndpoint("ep-2", "127.0.0.1", 18081)
+ unhealthy.UnHealthy = true
+
+ runtimeCluster :=
NewCluster(testCluster("snapshot-healthy-by-id-for-pick", healthy, unhealthy))
+ snapshot := runtimeCluster.EndpointSnapshot()
+
+ forPick := snapshot.HealthyEndpointByIDForPick(healthy.ID)
+ if assert.NotNil(t, forPick) {
+ // Same pointer the no-clone pick slice exposes.
+ assert.Same(t, snapshot.HealthyEndpointsForPick()[0], forPick)
+ }
+ assert.Nil(t, snapshot.HealthyEndpointByIDForPick(unhealthy.ID),
"unhealthy ID must not resolve")
+ assert.Nil(t, snapshot.HealthyEndpointByIDForPick("missing"), "unknown
ID must not resolve")
+
+ var nilSnapshot *EndpointSnapshot
+ assert.Nil(t, nilSnapshot.HealthyEndpointByIDForPick(healthy.ID), "nil
snapshot must be safe")
+}
+
func TestClusterEndpointSnapshotReturnsDefensiveEndpointSlices(t *testing.T) {
first := testEndpoint("ep-1", "127.0.0.1", 18080)
second := testEndpoint("ep-2", "127.0.0.1", 18081)
diff --git a/pkg/cluster/loadbalancer/load_balancer.go
b/pkg/cluster/loadbalancer/load_balancer.go
index fb1d3339c..295de13c7 100644
--- a/pkg/cluster/loadbalancer/load_balancer.go
+++ b/pkg/cluster/loadbalancer/load_balancer.go
@@ -49,6 +49,20 @@ type PickContext struct {
// Snapshot-aware balancers must treat endpoints as read-only and return
// the chosen endpoint without mutating or retaining it.
HealthyEndpoints []*model.Endpoint
+ // HealthyByID resolves a healthy snapshot endpoint by ID in O(1) for
the
+ // post-pick identity recheck. It is set by the snapshot-published pick
path;
+ // when nil (e.g. a hand-built context in a test), the recheck falls
back to
+ // scanning HealthyEndpoints. The returned endpoint is snapshot-owned
and
+ // must not be mutated or retained.
+ HealthyByID HealthyEndpointByIDForPicker
+}
+
+// HealthyEndpointByIDForPicker is the O(1) healthy-by-ID accessor the request
path
+// uses to recheck a balancer's pick without scanning the healthy slice. It is
+// satisfied by *cluster.EndpointSnapshot (HealthyEndpointByIDForPick);
declaring
+// it here keeps loadbalancer free of a dependency on the cluster package.
+type HealthyEndpointByIDForPicker interface {
+ HealthyEndpointByIDForPick(endpointID string) *model.Endpoint
}
type LoadBalancer interface {
@@ -134,7 +148,7 @@ func pickEndpoint(balancer LoadBalancer, context
PickContext, policy model.LbPol
snapshotContext = defensiveSnapshotPickContext(context)
}
endpoint :=
snapshotBalancer.HandlerWithSnapshot(snapshotContext, policy)
- return healthyEndpointFromSnapshot(endpoint,
context.HealthyEndpoints)
+ return healthyEndpointFromSnapshot(endpoint, context)
}
// Legacy balancers only understand ClusterConfig. Serialize this
@@ -192,26 +206,60 @@ func pickEndpoint(balancer LoadBalancer, context
PickContext, policy model.LbPol
if cursorAfter != cursorBefore {
atomic.AddUint32(&context.Config.PrePickEndpointIndex,
cursorAfter-cursorBefore)
}
- return healthyEndpointFromSnapshot(endpoint, context.HealthyEndpoints)
+ return healthyEndpointFromSnapshot(endpoint, context)
}
func defensiveSnapshotPickContext(context PickContext) PickContext {
defensive := context
defensive.AllEndpoints = model.CloneEndpoints(context.AllEndpoints)
defensive.HealthyEndpoints =
model.CloneEndpoints(context.HealthyEndpoints)
+ defensive.HealthyByID = nil
return defensive
}
-func healthyEndpointFromSnapshot(endpoint *model.Endpoint, healthyEndpoints
[]*model.Endpoint) *model.Endpoint {
+// healthyEndpointFromSnapshot validates that the balancer's pick is still a
+// member of the snapshot's healthy set and returns a defensive clone of the
+// snapshot-owned endpoint (never the balancer's possibly-cloned return).
+//
+// Resolution order:
+//
+// 1. O(1) by-ID recheck: when the pick carries an ID and the context exposes
+// the snapshot's healthy-by-ID index, resolve the single indexed
candidate.
+// If the indexed candidate is the exact balancer return, keep the
zero-copy
+// pointer fast path; otherwise apply sameEndpointIdentity to it. Every
+// snapshot endpoint carries a unique non-empty ID, so for an ID-bearing
pick
+// this is exactly the candidate the scan below would have found — without
+// the O(N) walk.
+// 2. Fallback scan: used when there is no ID index (e.g. a hand-built
context)
+// or the pick has no ID. Keeps the pointer-equality fast path for
zero-copy
+// balancers and the sameEndpointIdentity slow path (including the
+// empty-ID / placeholder-address rules) unchanged.
+//
+// Returns nil when the pick is not present in the healthy set; the validation
+// is deliberate and must not be skipped by blindly cloning the balancer
return.
+func healthyEndpointFromSnapshot(endpoint *model.Endpoint, context
PickContext) *model.Endpoint {
if endpoint == nil {
return nil
}
- for _, candidate := range healthyEndpoints {
+ if endpoint.ID != "" && context.HealthyByID != nil {
+ candidate :=
context.HealthyByID.HealthyEndpointByIDForPick(endpoint.ID)
+ if candidate == nil {
+ return nil
+ }
+ if candidate == endpoint {
+ return model.CloneEndpoint(candidate)
+ }
+ if sameEndpointIdentity(candidate, endpoint) {
+ return model.CloneEndpoint(candidate)
+ }
+ return nil
+ }
+ for _, candidate := range context.HealthyEndpoints {
if candidate == endpoint {
return model.CloneEndpoint(candidate)
}
}
- for _, candidate := range healthyEndpoints {
+ for _, candidate := range context.HealthyEndpoints {
if sameEndpointIdentity(candidate, endpoint) {
return model.CloneEndpoint(candidate)
}
diff --git a/pkg/cluster/loadbalancer/load_balancer_test.go
b/pkg/cluster/loadbalancer/load_balancer_test.go
index 496f19790..88b0eb4f9 100644
--- a/pkg/cluster/loadbalancer/load_balancer_test.go
+++ b/pkg/cluster/loadbalancer/load_balancer_test.go
@@ -46,6 +46,36 @@ type healthyOnlySnapshotLoadBalancer struct{}
var _ LoadBalancer = (*legacyLoadBalancer)(nil)
+// healthyByIDIndex is a test double for the snapshot's O(1) healthy-by-ID
+// lookup (satisfied in production by *cluster.EndpointSnapshot). It maps the
+// supplied healthy endpoints by ID, mirroring the snapshot's
healthyEndpointByID
+// index so the recheck fast path can be exercised without importing
pkg/cluster.
+type healthyByIDIndex map[string]*model.Endpoint
+
+func (h healthyByIDIndex) HealthyEndpointByIDForPick(endpointID string)
*model.Endpoint {
+ return h[endpointID]
+}
+
+func newHealthyByIDIndex(endpoints []*model.Endpoint) healthyByIDIndex {
+ index := make(healthyByIDIndex, len(endpoints))
+ for _, endpoint := range endpoints {
+ if endpoint != nil && endpoint.ID != "" {
+ index[endpoint.ID] = endpoint
+ }
+ }
+ return index
+}
+
+// snapshotRecheckContext builds the PickContext the request path passes to
+// healthyEndpointFromSnapshot: the healthy slice plus the O(1) by-ID index
over
+// the same endpoints, matching how cluster_manager wires a real snapshot.
+func snapshotRecheckContext(healthy []*model.Endpoint) PickContext {
+ return PickContext{
+ HealthyEndpoints: healthy,
+ HealthyByID: newHealthyByIDIndex(healthy),
+ }
+}
+
type blockingLegacyLoadBalancer struct {
entered chan int
release chan struct{}
@@ -327,6 +357,21 @@ func
TestNeedsAllEndpointsKeepsCompatibilityForUnmarkedSnapshotLoadBalancer(t *t
assert.False(t, NeedsAllEndpoints(healthyOnlySnapshotLoadBalancer{}))
}
+func TestDefensiveSnapshotPickContextClearsHealthyByID(t *testing.T) {
+ healthy := []*model.Endpoint{
+ {ID: "ep-1", Address: model.SocketAddress{Address: "127.0.0.1",
Port: 8080}},
+ }
+ originalContext := PickContext{
+ HealthyEndpoints: healthy,
+ HealthyByID: newHealthyByIDIndex(healthy),
+ }
+
+ defensive := defensiveSnapshotPickContext(originalContext)
+
+ assert.Nil(t, defensive.HealthyByID, "defensive context must not expose
live snapshot pointers")
+ assert.NotNil(t, originalContext.HealthyByID, "original context must be
unchanged")
+}
+
func TestPickEndpointSerializesLegacyLoadBalancerHandlers(t *testing.T) {
harness := newLegacyPickHarness(t)
pickContext := newLegacyPickContext("blocking-legacy-load-balancer",
"first")
@@ -529,7 +574,7 @@ func
TestHealthyEndpointFromSnapshotAcceptsResolvedAddressForBlankPlaceholder(t
Address: model.SocketAddress{Address: "127.0.0.1", Port: 8080},
}
- got := healthyEndpointFromSnapshot(balancerReturn, healthyEndpoints)
+ got := healthyEndpointFromSnapshot(balancerReturn,
snapshotRecheckContext(healthyEndpoints))
if !assert.NotNil(t, got, "balancer return with same ID as snapshot
placeholder must match via wildcard") {
return
}
@@ -546,7 +591,27 @@ func
TestHealthyEndpointFromSnapshotPointerFastPathReturnsClone(t *testing.T) {
},
}
- got := healthyEndpointFromSnapshot(endpoint,
[]*model.Endpoint{endpoint})
+ // No HealthyByID index: exercise the fallback scan's pointer-equality
fast
+ // path (the branch a zero-copy balancer hits when no ID index is
present).
+ got := healthyEndpointFromSnapshot(endpoint,
PickContext{HealthyEndpoints: []*model.Endpoint{endpoint}})
+
+ if !assert.NotNil(t, got) {
+ return
+ }
+ assert.Equal(t, endpoint, got)
+ assert.NotSame(t, endpoint, got, "request path must not return the
snapshot-owned endpoint pointer")
+}
+
+func TestHealthyEndpointFromSnapshotByIDPointerFastPathReturnsClone(t
*testing.T) {
+ endpoint := &model.Endpoint{
+ ID: "zero-copy-id",
+ Address: model.SocketAddress{
+ Address: "127.0.0.1",
+ Port: 8080,
+ },
+ }
+
+ got := healthyEndpointFromSnapshot(endpoint,
snapshotRecheckContext([]*model.Endpoint{endpoint}))
if !assert.NotNil(t, got) {
return
@@ -572,10 +637,68 @@ func
TestHealthyEndpointFromSnapshotRejectsMismatchedRealAddress(t *testing.T) {
Address: model.SocketAddress{Address: "10.0.0.1", Port: 9090},
}
- got := healthyEndpointFromSnapshot(balancerReturn, healthyEndpoints)
+ got := healthyEndpointFromSnapshot(balancerReturn,
snapshotRecheckContext(healthyEndpoints))
assert.Nil(t, got, "real-address mismatch must not match even when IDs
agree")
}
+// TestHealthyEndpointFromSnapshotByIDMatchesScan locks the O(1) by-ID recheck
+// to the fallback scan: for every case, resolving through the HealthyByID
index
+// must produce the same accept/reject decision as scanning HealthyEndpoints,
so
+// the optimization cannot silently change which picks are admitted.
+func TestHealthyEndpointFromSnapshotByIDMatchesScan(t *testing.T) {
+ realAddr := model.SocketAddress{Address: "127.0.0.1", Port: 8080}
+ healthy := []*model.Endpoint{
+ {ID: "ep-real", Address: realAddr},
+ {ID: "ep-placeholder", Address: model.SocketAddress{Domains:
[]string{""}}},
+ }
+
+ cases := []struct {
+ name string
+ ret *model.Endpoint
+ wantMatchID string // "" means expect nil
+ }{
+ {
+ name: "defensive copy of real endpoint matches
by ID",
+ ret: &model.Endpoint{ID: "ep-real", Address:
realAddr},
+ wantMatchID: "ep-real",
+ },
+ {
+ name: "placeholder wildcard accepts resolved
address",
+ ret: &model.Endpoint{ID: "ep-placeholder",
Address: model.SocketAddress{Address: "10.0.0.9", Port: 9090}},
+ wantMatchID: "ep-placeholder",
+ },
+ {
+ name: "same ID different real address rejected",
+ ret: &model.Endpoint{ID: "ep-real", Address:
model.SocketAddress{Address: "10.0.0.1", Port: 9090}},
+ wantMatchID: "",
+ },
+ {
+ name: "unknown ID rejected",
+ ret: &model.Endpoint{ID: "ep-missing", Address:
realAddr},
+ wantMatchID: "",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ withIndex := healthyEndpointFromSnapshot(tc.ret,
snapshotRecheckContext(healthy))
+ scanOnly := healthyEndpointFromSnapshot(tc.ret,
PickContext{HealthyEndpoints: healthy})
+
+ if tc.wantMatchID == "" {
+ assert.Nil(t, withIndex, "by-ID recheck must
reject")
+ assert.Nil(t, scanOnly, "scan must reject")
+ return
+ }
+ if assert.NotNil(t, withIndex, "by-ID recheck must
accept") {
+ assert.Equal(t, tc.wantMatchID, withIndex.ID)
+ }
+ if assert.NotNil(t, scanOnly, "scan must accept") {
+ assert.Equal(t, tc.wantMatchID, scanOnly.ID)
+ }
+ })
+ }
+}
+
func BenchmarkHealthyEndpointFromSnapshot(b *testing.B) {
const endpointCount = 1024
healthyEndpoints := make([]*model.Endpoint, endpointCount)
@@ -590,20 +713,32 @@ func BenchmarkHealthyEndpointFromSnapshot(b *testing.B) {
}
zeroCopyEndpoint := healthyEndpoints[endpointCount-1]
defensiveCopyEndpoint := model.CloneEndpoint(zeroCopyEndpoint)
+ scanContext := PickContext{HealthyEndpoints: healthyEndpoints}
+ indexContext := snapshotRecheckContext(healthyEndpoints)
+ // Zero-copy balancers hit the pointer-equality fast path; the index
does
+ // not change that branch, but measure it to confirm no regression.
b.Run("pointer-eq-fast-path", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- if healthyEndpointFromSnapshot(zeroCopyEndpoint,
healthyEndpoints) == nil {
- b.Fatal("expected match")
- }
- }
+ benchmarkHealthyEndpointFromSnapshot(b, zeroCopyEndpoint,
scanContext)
})
- b.Run("identity-scan", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- if healthyEndpointFromSnapshot(defensiveCopyEndpoint,
healthyEndpoints) == nil {
- b.Fatal("expected match")
- }
- }
+ // Non-zero-copy balancers return a cloned endpoint whose pointer is
not in
+ // the snapshot slice. Without an ID index this falls through to the
full
+ // O(N) sameEndpointIdentity scan; the index turns it into an O(1)
lookup.
+ b.Run("identity-scan-without-index", func(b *testing.B) {
+ benchmarkHealthyEndpointFromSnapshot(b, defensiveCopyEndpoint,
scanContext)
+ })
+
+ b.Run("identity-recheck-with-index", func(b *testing.B) {
+ benchmarkHealthyEndpointFromSnapshot(b, defensiveCopyEndpoint,
indexContext)
})
}
+
+func benchmarkHealthyEndpointFromSnapshot(b *testing.B, endpoint
*model.Endpoint, context PickContext) {
+ b.Helper()
+ for i := 0; i < b.N; i++ {
+ if healthyEndpointFromSnapshot(endpoint, context) == nil {
+ b.Fatal("expected match")
+ }
+ }
+}
diff --git a/pkg/server/cluster_manager.go b/pkg/server/cluster_manager.go
index f7284e9c7..fb3afb12b 100644
--- a/pkg/server/cluster_manager.go
+++ b/pkg/server/cluster_manager.go
@@ -308,6 +308,7 @@ func (cm *ClusterManager) pickOneEndpoint(runtimeCluster
*cluster.Cluster, polic
HealthyConsistentHash: snapshot.HealthyConsistentHash(),
AllEndpoints: allEndpoints,
HealthyEndpoints: healthyEndpoints,
+ HealthyByID: snapshot,
}, policy)
}