Skip to content

Commit 0d04c6f

Browse files
authored
ringhash: don't recreate subConns when update doesn't change address information (#5431)
1 parent a6dcb71 commit 0d04c6f

File tree

4 files changed

+112
-82
lines changed

4 files changed

+112
-82
lines changed

xds/internal/balancer/ringhash/ring.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ type ringEntry struct {
4343
sc *subConn
4444
}
4545

46-
// newRing creates a ring from the subConns. The ring size is limited by the
47-
// passed in max/min.
46+
// newRing creates a ring from the subConns stored in the AddressMap. The ring
47+
// size is limited by the passed in max/min.
4848
//
4949
// ring entries will be created for each subConn, and subConn with high weight
5050
// (specified by the address) may have multiple entries.
@@ -64,7 +64,7 @@ type ringEntry struct {
6464
//
6565
// To pick from a ring, a binary search will be done for the given target hash,
6666
// and first item with hash >= given hash will be returned.
67-
func newRing(subConns map[resolver.Address]*subConn, minRingSize, maxRingSize uint64) (*ring, error) {
67+
func newRing(subConns *resolver.AddressMap, minRingSize, maxRingSize uint64) (*ring, error) {
6868
// https://github.com/envoyproxy/envoy/blob/765c970f06a4c962961a0e03a467e165b276d50f/source/common/upstream/ring_hash_lb.cc#L114
6969
normalizedWeights, minWeight, err := normalizeWeights(subConns)
7070
if err != nil {
@@ -95,7 +95,7 @@ func newRing(subConns map[resolver.Address]*subConn, minRingSize, maxRingSize ui
9595
for _, scw := range normalizedWeights {
9696
targetIdx += scale * scw.weight
9797
for float64(idx) < targetIdx {
98-
h := xxhash.Sum64String(scw.sc.addr + strconv.Itoa(len(items)))
98+
h := xxhash.Sum64String(scw.sc.addr + strconv.Itoa(idx))
9999
items = append(items, &ringEntry{idx: idx, hash: h, sc: scw.sc})
100100
idx++
101101
}
@@ -111,26 +111,26 @@ func newRing(subConns map[resolver.Address]*subConn, minRingSize, maxRingSize ui
111111

112112
// normalizeWeights divides all the weights by the sum, so that the total weight
113113
// is 1.
114-
func normalizeWeights(subConns map[resolver.Address]*subConn) (_ []subConnWithWeight, min float64, _ error) {
115-
if len(subConns) == 0 {
114+
func normalizeWeights(subConns *resolver.AddressMap) ([]subConnWithWeight, float64, error) {
115+
keys := subConns.Keys()
116+
if len(keys) == 0 {
116117
return nil, 0, fmt.Errorf("number of subconns is 0")
117118
}
118119
var weightSum uint32
119-
for a := range subConns {
120-
// The address weight was moved from attributes to the Metadata field.
121-
// This is necessary (all the attributes need to be stripped) for the
122-
// balancer to detect identical {address+weight} combination.
123-
weightSum += a.Metadata.(uint32)
120+
for _, a := range keys {
121+
weightSum += getWeightAttribute(a)
124122
}
125123
if weightSum == 0 {
126124
return nil, 0, fmt.Errorf("total weight of all subconns is 0")
127125
}
128126
weightSumF := float64(weightSum)
129-
ret := make([]subConnWithWeight, 0, len(subConns))
130-
min = math.MaxFloat64
131-
for a, sc := range subConns {
132-
nw := float64(a.Metadata.(uint32)) / weightSumF
133-
ret = append(ret, subConnWithWeight{sc: sc, weight: nw})
127+
ret := make([]subConnWithWeight, 0, len(keys))
128+
min := float64(1.0)
129+
for _, a := range keys {
130+
v, _ := subConns.Get(a)
131+
scInfo := v.(*subConn)
132+
nw := float64(getWeightAttribute(a)) / weightSumF
133+
ret = append(ret, subConnWithWeight{sc: scInfo, weight: nw})
134134
if nw < min {
135135
min = nw
136136
}

xds/internal/balancer/ringhash/ring_test.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,31 @@ import (
2424
"testing"
2525

2626
xxhash "github.com/cespare/xxhash/v2"
27+
"google.golang.org/grpc/balancer/weightedroundrobin"
2728
"google.golang.org/grpc/resolver"
2829
)
2930

30-
func testAddr(addr string, weight uint32) resolver.Address {
31-
return resolver.Address{Addr: addr, Metadata: weight}
32-
}
31+
var testAddrs []resolver.Address
32+
var testSubConnMap *resolver.AddressMap
3333

34-
func (s) TestRingNew(t *testing.T) {
35-
testAddrs := []resolver.Address{
34+
func init() {
35+
testAddrs = []resolver.Address{
3636
testAddr("a", 3),
3737
testAddr("b", 3),
3838
testAddr("c", 4),
3939
}
40+
testSubConnMap = resolver.NewAddressMap()
41+
testSubConnMap.Set(testAddrs[0], &subConn{addr: "a"})
42+
testSubConnMap.Set(testAddrs[1], &subConn{addr: "b"})
43+
testSubConnMap.Set(testAddrs[2], &subConn{addr: "c"})
44+
}
45+
46+
func testAddr(addr string, weight uint32) resolver.Address {
47+
return weightedroundrobin.SetAddrInfo(resolver.Address{Addr: addr}, weightedroundrobin.AddrInfo{Weight: weight})
48+
}
49+
50+
func (s) TestRingNew(t *testing.T) {
4051
var totalWeight float64 = 10
41-
testSubConnMap := map[resolver.Address]*subConn{
42-
testAddr("a", 3): {addr: "a"},
43-
testAddr("b", 3): {addr: "b"},
44-
testAddr("c", 4): {addr: "c"},
45-
}
4652
for _, min := range []uint64{3, 4, 6, 8} {
4753
for _, max := range []uint64{20, 8} {
4854
t.Run(fmt.Sprintf("size-min-%v-max-%v", min, max), func(t *testing.T) {
@@ -59,7 +65,7 @@ func (s) TestRingNew(t *testing.T) {
5965
}
6066
}
6167
got := float64(count) / float64(totalCount)
62-
want := float64(a.Metadata.(uint32)) / totalWeight
68+
want := float64(getWeightAttribute(a)) / totalWeight
6369
if !equalApproximately(got, want) {
6470
t.Fatalf("unexpected item weight in ring: %v != %v", got, want)
6571
}
@@ -76,11 +82,7 @@ func equalApproximately(x, y float64) bool {
7682
}
7783

7884
func (s) TestRingPick(t *testing.T) {
79-
r, _ := newRing(map[resolver.Address]*subConn{
80-
{Addr: "a", Metadata: uint32(3)}: {addr: "a"},
81-
{Addr: "b", Metadata: uint32(3)}: {addr: "b"},
82-
{Addr: "c", Metadata: uint32(4)}: {addr: "c"},
83-
}, 10, 20)
85+
r, _ := newRing(testSubConnMap, 10, 20)
8486
for _, h := range []uint64{xxhash.Sum64String("1"), xxhash.Sum64String("2"), xxhash.Sum64String("3"), xxhash.Sum64String("4")} {
8587
t.Run(fmt.Sprintf("picking-hash-%v", h), func(t *testing.T) {
8688
e := r.pick(h)
@@ -98,11 +100,7 @@ func (s) TestRingPick(t *testing.T) {
98100
}
99101

100102
func (s) TestRingNext(t *testing.T) {
101-
r, _ := newRing(map[resolver.Address]*subConn{
102-
{Addr: "a", Metadata: uint32(3)}: {addr: "a"},
103-
{Addr: "b", Metadata: uint32(3)}: {addr: "b"},
104-
{Addr: "c", Metadata: uint32(4)}: {addr: "c"},
105-
}, 10, 20)
103+
r, _ := newRing(testSubConnMap, 10, 20)
106104

107105
for _, e := range r.items {
108106
ne := r.next(e)

xds/internal/balancer/ringhash/ringhash.go

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type bb struct{}
4747
func (bb) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
4848
b := &ringhashBalancer{
4949
cc: cc,
50-
subConns: make(map[resolver.Address]*subConn),
50+
subConns: resolver.NewAddressMap(),
5151
scStates: make(map[balancer.SubConn]*subConn),
5252
csEvltr: &connectivityStateEvaluator{},
5353
}
@@ -65,8 +65,9 @@ func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, err
6565
}
6666

6767
type subConn struct {
68-
addr string
69-
sc balancer.SubConn
68+
addr string
69+
weight uint32
70+
sc balancer.SubConn
7071

7172
mu sync.RWMutex
7273
// This is the actual state of this SubConn (as updated by the ClientConn).
@@ -178,9 +179,8 @@ type ringhashBalancer struct {
178179
cc balancer.ClientConn
179180
logger *grpclog.PrefixLogger
180181

181-
config *LBConfig
182-
183-
subConns map[resolver.Address]*subConn // `attributes` is stripped from the keys of this map (the addresses)
182+
config *LBConfig
183+
subConns *resolver.AddressMap // Map from resolver.Address to `*subConn`.
184184
scStates map[balancer.SubConn]*subConn
185185

186186
// ring is always in sync with subConns. When subConns change, a new ring is
@@ -208,55 +208,47 @@ type ringhashBalancer struct {
208208
// SubConn states are Idle.
209209
func (b *ringhashBalancer) updateAddresses(addrs []resolver.Address) bool {
210210
var addrsUpdated bool
211-
// addrsSet is the set converted from addrs, it's used for quick lookup of
212-
// an address.
213-
//
214-
// Addresses in this map all have attributes stripped, but metadata set to
215-
// the weight. So that weight change can be detected.
216-
//
217-
// TODO: this won't be necessary if there are ways to compare address
218-
// attributes.
219-
addrsSet := make(map[resolver.Address]struct{})
220-
for _, a := range addrs {
221-
aNoAttrs := a
222-
// Strip attributes but set Metadata to the weight.
223-
aNoAttrs.Attributes = nil
224-
w := weightedroundrobin.GetAddrInfo(a).Weight
225-
if w == 0 {
226-
// If weight is not set, use 1.
227-
w = 1
228-
}
229-
aNoAttrs.Metadata = w
230-
addrsSet[aNoAttrs] = struct{}{}
231-
if scInfo, ok := b.subConns[aNoAttrs]; !ok {
232-
// When creating SubConn, the original address with attributes is
233-
// passed through. So that connection configurations in attributes
234-
// (like creds) will be used.
235-
sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: true})
211+
// addrsSet is the set converted from addrs, used for quick lookup.
212+
addrsSet := resolver.NewAddressMap()
213+
for _, addr := range addrs {
214+
addrsSet.Set(addr, true)
215+
newWeight := getWeightAttribute(addr)
216+
if val, ok := b.subConns.Get(addr); !ok {
217+
sc, err := b.cc.NewSubConn([]resolver.Address{addr}, balancer.NewSubConnOptions{HealthCheckEnabled: true})
236218
if err != nil {
237219
logger.Warningf("base.baseBalancer: failed to create new SubConn: %v", err)
238220
continue
239221
}
240-
scs := &subConn{addr: a.Addr, sc: sc}
222+
scs := &subConn{addr: addr.Addr, weight: newWeight, sc: sc}
241223
scs.setState(connectivity.Idle)
242224
b.state = b.csEvltr.recordTransition(connectivity.Shutdown, connectivity.Idle)
243-
b.subConns[aNoAttrs] = scs
225+
b.subConns.Set(addr, scs)
244226
b.scStates[sc] = scs
245227
addrsUpdated = true
246228
} else {
247-
// Always update the subconn's address in case the attributes
248-
// changed. The SubConn does a reflect.DeepEqual of the new and old
249-
// addresses. So this is a noop if the current address is the same
250-
// as the old one (including attributes).
251-
b.subConns[aNoAttrs] = scInfo
252-
b.cc.UpdateAddresses(scInfo.sc, []resolver.Address{a})
229+
// We have seen this address before and created a subConn for it. If the
230+
// weight associated with the address has changed, update the subConns map
231+
// with the new weight. This will be used when a new ring is created.
232+
//
233+
// There is no need to call UpdateAddresses on the subConn at this point
234+
// since *only* the weight attribute has changed, and that does not affect
235+
// subConn uniqueness.
236+
scInfo := val.(*subConn)
237+
if oldWeight := scInfo.weight; oldWeight != newWeight {
238+
scInfo.weight = newWeight
239+
b.subConns.Set(addr, scInfo)
240+
// Return true to force recreation of the ring.
241+
addrsUpdated = true
242+
}
253243
}
254244
}
255-
for a, scInfo := range b.subConns {
256-
// a was removed by resolver.
257-
if _, ok := addrsSet[a]; !ok {
245+
for _, addr := range b.subConns.Keys() {
246+
// addr was removed by resolver.
247+
if _, ok := addrsSet.Get(addr); !ok {
248+
v, _ := b.subConns.Get(addr)
249+
scInfo := v.(*subConn)
258250
b.cc.RemoveSubConn(scInfo.sc)
259-
delete(b.subConns, a)
251+
b.subConns.Delete(addr)
260252
addrsUpdated = true
261253
// Keep the state of this sc in b.scStates until sc's state becomes Shutdown.
262254
// The entry will be deleted in UpdateSubConnState.
@@ -304,7 +296,7 @@ func (b *ringhashBalancer) UpdateClientConnState(s balancer.ClientConnState) err
304296

305297
func (b *ringhashBalancer) ResolverError(err error) {
306298
b.resolverErr = err
307-
if len(b.subConns) == 0 {
299+
if b.subConns.Len() == 0 {
308300
b.state = connectivity.TransientFailure
309301
}
310302

@@ -392,7 +384,8 @@ func (b *ringhashBalancer) UpdateSubConnState(sc balancer.SubConn, state balance
392384
// attempting to connect, we need to trigger one. But since the deleted
393385
// SubConn will eventually send a shutdown update, this code will run
394386
// and trigger the next SubConn to connect.
395-
for _, sc := range b.subConns {
387+
for _, v := range b.subConns.Values() {
388+
sc := v.(*subConn)
396389
if sc.isAttemptingToConnect() {
397390
return
398391
}
@@ -485,3 +478,18 @@ func (cse *connectivityStateEvaluator) recordTransition(oldState, newState conne
485478
}
486479
return connectivity.TransientFailure
487480
}
481+
482+
// getWeightAttribute is a convenience function which returns the value of the
483+
// weight attribute stored in the BalancerAttributes field of addr, using the
484+
// weightedroundrobin package.
485+
//
486+
// When used in the xDS context, the weight attribute is guaranteed to be
487+
// non-zero. But, when used in a non-xDS context, the weight attribute could be
488+
// unset. A Default of 1 is used in the latter case.
489+
func getWeightAttribute(addr resolver.Address) uint32 {
490+
w := weightedroundrobin.GetAddrInfo(addr).Weight
491+
if w == 0 {
492+
return 1
493+
}
494+
return w
495+
}

xds/internal/balancer/ringhash/ringhash_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"google.golang.org/grpc/internal/grpctest"
3434
"google.golang.org/grpc/internal/testutils"
3535
"google.golang.org/grpc/resolver"
36+
"google.golang.org/grpc/xds/internal"
3637
)
3738

3839
var (
@@ -491,3 +492,26 @@ func (s) TestConnectivityStateEvaluatorRecordTransition(t *testing.T) {
491492
})
492493
}
493494
}
495+
496+
// TestAddrBalancerAttributesChange tests the case where the ringhash balancer
497+
// receives a ClientConnUpdate with the same config and addresses as received in
498+
// the previous update. Although the `BalancerAttributes` contents are the same,
499+
// the pointer is different. This test verifies that subConns are not recreated
500+
// in this scenario.
501+
func (s) TestAddrBalancerAttributesChange(t *testing.T) {
502+
addrs1 := []resolver.Address{internal.SetLocalityID(resolver.Address{Addr: testBackendAddrStrs[0]}, internal.LocalityID{Region: "americas"})}
503+
cc, b, _ := setupTest(t, addrs1)
504+
505+
addrs2 := []resolver.Address{internal.SetLocalityID(resolver.Address{Addr: testBackendAddrStrs[0]}, internal.LocalityID{Region: "americas"})}
506+
if err := b.UpdateClientConnState(balancer.ClientConnState{
507+
ResolverState: resolver.State{Addresses: addrs2},
508+
BalancerConfig: nil,
509+
}); err != nil {
510+
t.Fatalf("UpdateClientConnState returned err: %v", err)
511+
}
512+
select {
513+
case <-cc.NewSubConnCh:
514+
t.Fatal("new subConn created for an update with the same addresses")
515+
case <-time.After(defaultTestShortTimeout):
516+
}
517+
}

0 commit comments

Comments
 (0)