fix: reduce `SetAddrs` shards lock contention

Introduces a new lock to make `SetAddrs` calls exclusive.
This allows release of shards lock for the duration of potentially long `newRingShards` call.

`TestRingSetAddrsContention` observes number of pings increased from <1000 to ~40_000.

See https://github.com/go-redis/redis/pull/2190#discussion_r953040289

Updates #2077
This commit is contained in:
Alexander Yastrebov 2022-11-21 15:59:43 +01:00
parent a31c1d6ff0
commit 6c05a9f6b1
2 changed files with 46 additions and 32 deletions

66
ring.go
View File

@ -219,6 +219,10 @@ type ringSharding struct {
hash ConsistentHash hash ConsistentHash
numShard int numShard int
onNewNode []func(rdb *Client) onNewNode []func(rdb *Client)
// ensures exclusive access to SetAddrs so there is no need
// to hold mu for the duration of potentially long shard creation
setAddrsMu sync.Mutex
} }
type ringShards struct { type ringShards struct {
@ -245,46 +249,62 @@ func (c *ringSharding) OnNewNode(fn func(rdb *Client)) {
// decrease number of shards, that you use. It will reuse shards that // decrease number of shards, that you use. It will reuse shards that
// existed before and close the ones that will not be used anymore. // existed before and close the ones that will not be used anymore.
func (c *ringSharding) SetAddrs(addrs map[string]string) { func (c *ringSharding) SetAddrs(addrs map[string]string) {
c.mu.Lock() c.setAddrsMu.Lock()
defer c.setAddrsMu.Unlock()
cleanup := func(shards map[string]*ringShard) {
for addr, shard := range shards {
if err := shard.Client.Close(); err != nil {
internal.Logger.Printf(context.Background(), "shard.Close %s failed: %s", addr, err)
}
}
}
c.mu.RLock()
if c.closed { if c.closed {
c.mu.RUnlock()
return
}
existing := c.shards
c.mu.RUnlock()
shards, created, unused := c.newRingShards(addrs, existing)
c.mu.Lock()
if c.closed {
cleanup(created)
c.mu.Unlock() c.mu.Unlock()
return return
} }
shards, cleanup := c.newRingShards(addrs, c.shards)
c.shards = shards c.shards = shards
c.rebalanceLocked() c.rebalanceLocked()
c.mu.Unlock() c.mu.Unlock()
cleanup() cleanup(unused)
} }
func (c *ringSharding) newRingShards( func (c *ringSharding) newRingShards(
addrs map[string]string, existingShards *ringShards, addrs map[string]string, existing *ringShards,
) (*ringShards, func()) { ) (shards *ringShards, created, unused map[string]*ringShard) {
shardMap := make(map[string]*ringShard) // indexed by addr
unusedShards := make(map[string]*ringShard) // indexed by addr
if existingShards != nil { shards = &ringShards{m: make(map[string]*ringShard, len(addrs))}
for _, shard := range existingShards.list { created = make(map[string]*ringShard) // indexed by addr
addr := shard.Client.opt.Addr unused = make(map[string]*ringShard) // indexed by addr
shardMap[addr] = shard
unusedShards[addr] = shard
}
}
shards := &ringShards{ if existing != nil {
m: make(map[string]*ringShard), for _, shard := range existing.list {
unused[shard.addr] = shard
}
} }
for name, addr := range addrs { for name, addr := range addrs {
if shard, ok := shardMap[addr]; ok { if shard, ok := unused[addr]; ok {
shards.m[name] = shard shards.m[name] = shard
delete(unusedShards, addr) delete(unused, addr)
} else { } else {
shard := newRingShard(c.opt, addr) shard := newRingShard(c.opt, addr)
shards.m[name] = shard shards.m[name] = shard
created[addr] = shard
for _, fn := range c.onNewNode { for _, fn := range c.onNewNode {
fn(shard.Client) fn(shard.Client)
@ -296,13 +316,7 @@ func (c *ringSharding) newRingShards(
shards.list = append(shards.list, shard) shards.list = append(shards.list, shard)
} }
return shards, func() { return
for addr, shard := range unusedShards {
if err := shard.Client.Close(); err != nil {
internal.Logger.Printf(context.Background(), "shard.Close %s failed: %s", addr, err)
}
}
}
} }
func (c *ringSharding) List() []*ringShard { func (c *ringSharding) List() []*ringShard {

View File

@ -124,7 +124,7 @@ var _ = Describe("Redis Ring", func() {
}) })
Expect(ring.Len(), 1) Expect(ring.Len(), 1)
gotShard := ring.ShardByName("ringShardOne") gotShard := ring.ShardByName("ringShardOne")
Expect(gotShard).To(Equal(wantShard)) Expect(gotShard).To(BeIdenticalTo(wantShard))
ring.SetAddrs(map[string]string{ ring.SetAddrs(map[string]string{
"ringShardOne": ":" + ringShard1Port, "ringShardOne": ":" + ringShard1Port,
@ -132,7 +132,7 @@ var _ = Describe("Redis Ring", func() {
}) })
Expect(ring.Len(), 2) Expect(ring.Len(), 2)
gotShard = ring.ShardByName("ringShardOne") gotShard = ring.ShardByName("ringShardOne")
Expect(gotShard).To(Equal(wantShard)) Expect(gotShard).To(BeIdenticalTo(wantShard))
}) })
It("uses 3 shards after setting it to 3 shards", func() { It("uses 3 shards after setting it to 3 shards", func() {
@ -156,8 +156,8 @@ var _ = Describe("Redis Ring", func() {
gotShard1 := ring.ShardByName(shardName1) gotShard1 := ring.ShardByName(shardName1)
gotShard2 := ring.ShardByName(shardName2) gotShard2 := ring.ShardByName(shardName2)
gotShard3 := ring.ShardByName(shardName3) gotShard3 := ring.ShardByName(shardName3)
Expect(gotShard1).To(Equal(wantShard1)) Expect(gotShard1).To(BeIdenticalTo(wantShard1))
Expect(gotShard2).To(Equal(wantShard2)) Expect(gotShard2).To(BeIdenticalTo(wantShard2))
Expect(gotShard3).ToNot(BeNil()) Expect(gotShard3).ToNot(BeNil())
ring.SetAddrs(map[string]string{ ring.SetAddrs(map[string]string{
@ -168,8 +168,8 @@ var _ = Describe("Redis Ring", func() {
gotShard1 = ring.ShardByName(shardName1) gotShard1 = ring.ShardByName(shardName1)
gotShard2 = ring.ShardByName(shardName2) gotShard2 = ring.ShardByName(shardName2)
gotShard3 = ring.ShardByName(shardName3) gotShard3 = ring.ShardByName(shardName3)
Expect(gotShard1).To(Equal(wantShard1)) Expect(gotShard1).To(BeIdenticalTo(wantShard1))
Expect(gotShard2).To(Equal(wantShard2)) Expect(gotShard2).To(BeIdenticalTo(wantShard2))
Expect(gotShard3).To(BeNil()) Expect(gotShard3).To(BeNil())
}) })
}) })