mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-30 14:56:19 +00:00
## What changed? Added a configurable `ReplicaPoints` variable to the ringpop service resolver. Instead of a hardcoded constant (`replicaPoints = 100`), the number of virtual nodes per host is now controlled by a `var ReplicaPoints func()` int that can be overridden. When the value returned by `ReplicaPoints()` changes, the hash ring is rebuilt and a `ChangedEvent` is emitted -- even if membership itself hasn't changed. ## Why? The default of 100 replica points is not optimal for all combinations of shard and history node counts and may lead to uneven shard distribution across history nodes. For example, having 4096 shards with 9 history nodes with 100 replica points gives us std dev around 50 shards. Increasing replica points to 500 lowers std dev to around 30 shards. The optimal replica points is roughly `shards / nodes`, which equates the number of "shard points" and "node points" on the ring. For 4096 shards and 9 history nodes, distribution got better as follows * with 100 replica points: 370, 380, 408, 442, 452, 488, 510, 520, 526 (shards per node); * with 500 replica points: 426, 444, 446, 446, 450, 452, 458, 474, 500. This patch allows setting custom `ringpop.ReplicaPoints` (e.g. to dynamic config value getter) before starting the server, and gradually (to avoid reshuffling too many shards at once) increasing replica points to the desired value. ## How did you test it? - [x] built - [x] run locally and tested manually - [x] covered by existing tests - [ ] added new unit test(s) - [ ] added new functional test(s) ## Potential risks None if `var ReplicaPoints func()` not modified. If the server is built with overridden `ReplicaPoints`, it may cause ring inconsistency across cluster nodes during the deployment process.
218 lines
6.2 KiB
Go
218 lines
6.2 KiB
Go
package ringpop
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/temporalio/ringpop-go"
|
|
"github.com/temporalio/tchannel-go"
|
|
"go.temporal.io/server/common/config"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/membership"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/primitives"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
// testCluster is a type that represents a test ringpop cluster
|
|
type testCluster struct {
|
|
hostUUIDs []string
|
|
hostAddrs []string
|
|
hostInfoList []membership.HostInfo
|
|
rings []*monitor
|
|
channels []*tchannel.Channel
|
|
seedNode string
|
|
}
|
|
|
|
// newTestCluster creates a new test cluster with the given name and cluster size
|
|
// All the nodes in the test cluster will register themselves in Ringpop
|
|
// with the specified name. This is only intended for unit tests.
|
|
func newTestCluster(
|
|
t *testing.T,
|
|
ringPopApp string,
|
|
size int,
|
|
listenIPAddr string,
|
|
seed string,
|
|
serviceName primitives.ServiceName,
|
|
broadcastAddress string,
|
|
joinTimes []time.Time,
|
|
testInitialBootstrapFailure bool,
|
|
) *testCluster {
|
|
logger := log.NewTestLogger()
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
mockMgr := persistence.NewMockClusterMetadataManager(ctrl)
|
|
mockMgr.EXPECT().PruneClusterMembership(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
mockMgr.EXPECT().UpsertClusterMembership(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
|
|
cluster := &testCluster{
|
|
hostUUIDs: make([]string, size),
|
|
hostAddrs: make([]string, size),
|
|
hostInfoList: make([]membership.HostInfo, size),
|
|
rings: make([]*monitor, size),
|
|
channels: make([]*tchannel.Channel, size),
|
|
seedNode: seed,
|
|
}
|
|
|
|
for i := range size {
|
|
var err error
|
|
cluster.channels[i], err = tchannel.NewChannel(ringPopApp, nil)
|
|
if err != nil {
|
|
logger.Error("Failed to create tchannel", tag.Error(err))
|
|
return nil
|
|
}
|
|
listenAddr := listenIPAddr + ":0"
|
|
err = cluster.channels[i].ListenAndServe(listenAddr)
|
|
if err != nil {
|
|
logger.Error("tchannel listen failed", tag.Error(err))
|
|
return nil
|
|
}
|
|
cluster.hostUUIDs[i] = uuid.NewString()
|
|
cluster.hostAddrs[i], err = buildBroadcastHostPort(cluster.channels[i].PeerInfo(), broadcastAddress)
|
|
if err != nil {
|
|
logger.Error("Failed to build broadcast hostport", tag.Error(err))
|
|
return nil
|
|
}
|
|
cluster.hostInfoList[i] = newHostInfo(cluster.hostAddrs[i], nil)
|
|
}
|
|
|
|
// if seed node is already supplied, use it; if not, set it
|
|
if cluster.seedNode == "" {
|
|
cluster.seedNode = cluster.hostAddrs[0]
|
|
}
|
|
logger.Info("seedNode", tag.Name(cluster.seedNode))
|
|
|
|
seedAddress, seedPort, err := splitHostPortTyped(cluster.seedNode)
|
|
if err != nil {
|
|
logger.Error("unable to split host port", tag.Error(err))
|
|
return nil
|
|
}
|
|
// MarshalBinary never fails for UUIDs
|
|
hostID, _ := uuid.New().MarshalBinary()
|
|
|
|
seedMember := &persistence.ClusterMember{
|
|
HostID: hostID,
|
|
RPCAddress: seedAddress,
|
|
RPCPort: seedPort,
|
|
SessionStart: time.Now().UTC(),
|
|
LastHeartbeat: time.Now().UTC(),
|
|
}
|
|
|
|
firstGetClusterMemberCall := testInitialBootstrapFailure
|
|
mockMgr.EXPECT().GetClusterMembers(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(_ context.Context, _ *persistence.GetClusterMembersRequest) (*persistence.GetClusterMembersResponse, error) {
|
|
res := &persistence.GetClusterMembersResponse{ActiveMembers: []*persistence.ClusterMember{seedMember}}
|
|
|
|
hostID, _ := uuid.New().MarshalBinary()
|
|
if firstGetClusterMemberCall {
|
|
// The first time GetClusterMembers is invoked, we simulate returning a stale/bad heartbeat.
|
|
// All subsequent calls only return the single "good" seed member
|
|
// This ensures that we exercise the retry path in bootstrap properly.
|
|
badSeedMember := &persistence.ClusterMember{
|
|
HostID: hostID,
|
|
RPCAddress: seedAddress,
|
|
RPCPort: seedPort + 1,
|
|
SessionStart: time.Now().UTC(),
|
|
LastHeartbeat: time.Now().UTC(),
|
|
}
|
|
res = &persistence.GetClusterMembersResponse{ActiveMembers: []*persistence.ClusterMember{seedMember, badSeedMember}}
|
|
}
|
|
|
|
firstGetClusterMemberCall = false
|
|
return res, nil
|
|
}).AnyTimes()
|
|
|
|
for i := range size {
|
|
node := i
|
|
resolver := func() (string, error) {
|
|
return buildBroadcastHostPort(cluster.channels[node].PeerInfo(), broadcastAddress)
|
|
}
|
|
|
|
ringPop, err := ringpop.New(ringPopApp, ringpop.Channel(cluster.channels[i]), ringpop.AddressResolverFunc(resolver))
|
|
if err != nil {
|
|
logger.Error("failed to create ringpop instance", tag.Error(err))
|
|
return nil
|
|
}
|
|
_, port, _ := splitHostPortTyped(cluster.hostAddrs[i])
|
|
var joinTime time.Time
|
|
if i < len(joinTimes) {
|
|
joinTime = joinTimes[i]
|
|
}
|
|
cluster.rings[i] = newMonitor(
|
|
serviceName,
|
|
config.ServicePortMap{serviceName: int(port)}, // use same port for "grpc" port
|
|
ringPop,
|
|
logger,
|
|
mockMgr,
|
|
resolver,
|
|
2*time.Second,
|
|
3*time.Second,
|
|
joinTime,
|
|
100,
|
|
)
|
|
cluster.rings[i].Start()
|
|
}
|
|
return cluster
|
|
}
|
|
|
|
// GetSeedNode returns the seedNode for this cluster
|
|
func (c *testCluster) GetSeedNode() string {
|
|
return c.seedNode
|
|
}
|
|
|
|
// DrainHost marks the given host draining
|
|
func (c *testCluster) DrainHost(hostID string) {
|
|
for i, uuid := range c.hostUUIDs {
|
|
if uuid == hostID {
|
|
c.rings[i].SetDraining(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// KillHost kills the given host within the cluster
|
|
func (c *testCluster) KillHost(hostID string) {
|
|
for i := 0; i < len(c.hostUUIDs); i++ {
|
|
if c.hostUUIDs[i] == hostID {
|
|
c.rings[i].Stop()
|
|
c.channels[i].Close()
|
|
c.rings[i] = nil
|
|
c.channels[i] = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the cluster
|
|
func (c *testCluster) Stop() {
|
|
for i := 0; i < len(c.hostAddrs); i++ {
|
|
if c.rings[i] != nil {
|
|
c.rings[i].Stop()
|
|
c.channels[i].Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetHostInfoList returns the list of all hosts within the cluster
|
|
func (c *testCluster) GetHostInfoList() []membership.HostInfo {
|
|
return c.hostInfoList
|
|
}
|
|
|
|
// GetHostAddrs returns all host addrs within the cluster
|
|
func (c *testCluster) GetHostAddrs() []string {
|
|
return c.hostAddrs
|
|
}
|
|
|
|
// FindHostByAddr returns the host info corresponding to
|
|
// the given addr, if it exists
|
|
func (c *testCluster) FindHostByAddr(addr string) (membership.HostInfo, bool) {
|
|
for _, hi := range c.hostInfoList {
|
|
if hi.GetAddress() == addr {
|
|
return hi, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|