fix: harden multi-pod delivery and load verification
This commit is contained in:
+167
-34
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"sort"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/concurrency"
|
||||
"github.com/tinode/chat/server/logs"
|
||||
immetrics "github.com/tinode/chat/server/metrics"
|
||||
"github.com/tinode/chat/server/push"
|
||||
rh "github.com/tinode/chat/server/ringhash"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
@@ -25,18 +27,21 @@ const (
|
||||
// Default timeout before attempting to reconnect to a node.
|
||||
clusterDefaultReconnectTime = 200 * time.Millisecond
|
||||
// Number of replicas in ringhash.
|
||||
clusterHashReplicas = 20
|
||||
// Buffer size for sending requests from proxy to master.
|
||||
clusterProxyToMasterBuffer = 64
|
||||
// Expand buffer size by this value for nodes over the basic 3-node setup.
|
||||
clusterProxyToMasterBufferPerNode = 16
|
||||
// Timeout for attempting to enqueue a proxy-to-master request when the buffer is full.
|
||||
clusterP2MTimeout = 20 * time.Millisecond
|
||||
clusterHashReplicas = 20
|
||||
clusterDefaultP2MBuffer = 4096
|
||||
clusterDefaultP2MWorkers = 16
|
||||
clusterDefaultP2MTimeout = 300 * time.Millisecond
|
||||
clusterDefaultRouteRetries = 3
|
||||
clusterDefaultRouteRetryBackoff = 25 * time.Millisecond
|
||||
// Buffer size for receiving responses from other nodes, per node.
|
||||
clusterRpcCompletionBuffer = 64
|
||||
)
|
||||
|
||||
var errClusterTopicOwnerUnavailable = errors.New("cluster topic owner unavailable")
|
||||
var (
|
||||
errClusterTopicOwnerUnavailable = errors.New("cluster topic owner unavailable")
|
||||
errClusterOverloaded = errors.New("cluster overloaded")
|
||||
errClusterLoadExceeded = errors.New("cluster load exceeded")
|
||||
)
|
||||
|
||||
// ProxyReqType is the type of proxy requests.
|
||||
type ProxyReqType int
|
||||
@@ -64,7 +69,12 @@ type clusterConfig struct {
|
||||
// Name of this cluster node
|
||||
ThisName string `json:"self"`
|
||||
// Deprecated: this field is no longer used.
|
||||
NumProxyEventGoRoutines int `json:"-"`
|
||||
NumProxyEventGoRoutines int `json:"-"`
|
||||
ProxyToMasterBuffer int `json:"proxy_to_master_buffer"`
|
||||
ProxyToMasterWorkers int `json:"proxy_to_master_workers"`
|
||||
ProxyToMasterEnqueueTimeoutMS int `json:"proxy_to_master_enqueue_timeout_ms"`
|
||||
RouteRetries int `json:"route_retries"`
|
||||
RouteRetryBackoffMS int `json:"route_retry_backoff_ms"`
|
||||
// Failover configuration
|
||||
Failover *clusterFailoverConfig
|
||||
}
|
||||
@@ -99,8 +109,13 @@ type ClusterNode struct {
|
||||
// Buffered, clusterRpcCompletionBuffer * number_of_nodes.
|
||||
rpcDone chan *rpc.Call
|
||||
|
||||
// Channel for sending proxy to master requests; buffered, clusterProxyToMasterBuffer.
|
||||
p2mSender chan *ClusterReq
|
||||
// Sharded queues preserve ordering for the same tenant and topic while allowing parallel RPCs.
|
||||
p2mSenders []chan *ClusterReq
|
||||
p2mEnqueueTimeout time.Duration
|
||||
routeRetries int
|
||||
routeRetryBackoff time.Duration
|
||||
overloadLogAt atomic.Int64
|
||||
overloadSuppressed atomic.Int64
|
||||
}
|
||||
|
||||
func (n *ClusterNode) asyncRpcLoop() {
|
||||
@@ -109,19 +124,51 @@ func (n *ClusterNode) asyncRpcLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *ClusterNode) p2mSenderLoop() {
|
||||
for req := range n.p2mSender {
|
||||
func (n *ClusterNode) p2mSenderLoop(sender <-chan *ClusterReq) {
|
||||
for req := range sender {
|
||||
if req == nil {
|
||||
// Stop
|
||||
return
|
||||
}
|
||||
|
||||
if err := n.proxyToMaster(req); err != nil {
|
||||
logs.Warn.Println("p2mSenderLoop: call failed", n.name, err)
|
||||
if errors.Is(err, errClusterOverloaded) {
|
||||
n.logOverload("proxy_to_master", req.TenantID, req.RcptTo, 0)
|
||||
} else {
|
||||
logs.Warn.Println("p2mSenderLoop: call failed", n.name, err)
|
||||
}
|
||||
}
|
||||
n.updateP2MQueueMetric()
|
||||
}
|
||||
}
|
||||
|
||||
func (n *ClusterNode) logOverload(path string, tenantID types.TenantID, topic string, seq int64) {
|
||||
now := time.Now().Unix()
|
||||
previous := n.overloadLogAt.Load()
|
||||
if previous == now || !n.overloadLogAt.CompareAndSwap(previous, now) {
|
||||
n.overloadSuppressed.Add(1)
|
||||
return
|
||||
}
|
||||
suppressed := n.overloadSuppressed.Swap(0)
|
||||
logs.Warn.Printf("cluster overload: path=%s tenant=%d topic=%s seq=%d peer=%s suppressed=%d",
|
||||
path, tenantID, topic, seq, n.name, suppressed)
|
||||
}
|
||||
|
||||
func (n *ClusterNode) updateP2MQueueMetric() {
|
||||
depth := 0
|
||||
for _, sender := range n.p2mSenders {
|
||||
depth += len(sender)
|
||||
}
|
||||
immetrics.ClusterP2MQueueLength.WithLabelValues(clusterPodName(), n.name).Set(float64(depth))
|
||||
}
|
||||
|
||||
func clusterPodName() string {
|
||||
if globals.cluster != nil && globals.cluster.thisNodeName != "" {
|
||||
return globals.cluster.thisNodeName
|
||||
}
|
||||
return "standalone"
|
||||
}
|
||||
|
||||
// ClusterSess is a basic info on a remote session where the message was created.
|
||||
type ClusterSess struct {
|
||||
TenantID types.TenantID
|
||||
@@ -318,7 +365,12 @@ func (n *ClusterNode) call(proc string, req, resp any) error {
|
||||
return errors.New("cluster: node '" + n.name + "' not connected")
|
||||
}
|
||||
|
||||
method := proc
|
||||
started := time.Now()
|
||||
immetrics.ClusterRPCInflight.WithLabelValues(clusterPodName(), n.name, method).Inc()
|
||||
defer immetrics.ClusterRPCInflight.WithLabelValues(clusterPodName(), n.name, method).Dec()
|
||||
if err := n.endpoint.Call(proc, req, resp); err != nil {
|
||||
immetrics.ClusterRPCDuration.WithLabelValues(clusterPodName(), n.name, method, "error").Observe(time.Since(started).Seconds())
|
||||
logs.Warn.Println("cluster: call failed", n.name, err)
|
||||
|
||||
n.lock.Lock()
|
||||
@@ -331,6 +383,7 @@ func (n *ClusterNode) call(proc string, req, resp any) error {
|
||||
n.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
immetrics.ClusterRPCDuration.WithLabelValues(clusterPodName(), n.name, method, "ok").Observe(time.Since(started).Seconds())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -395,29 +448,47 @@ func (n *ClusterNode) proxyToMaster(msg *ClusterReq) error {
|
||||
var rejected bool
|
||||
err := n.call("Cluster.TopicMaster", msg, &rejected)
|
||||
if err == nil && rejected {
|
||||
err = errors.New("cluster: topic master node out of sync")
|
||||
err = errClusterOverloaded
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// proxyToMaster forwards request from topic proxy to topic master.
|
||||
func (n *ClusterNode) proxyToMasterAsync(msg *ClusterReq) error {
|
||||
if len(n.p2mSenders) == 0 {
|
||||
return errClusterTopicOwnerUnavailable
|
||||
}
|
||||
sender := n.p2mSenders[clusterReqShard(msg, len(n.p2mSenders))]
|
||||
select {
|
||||
case n.p2mSender <- msg:
|
||||
case sender <- msg:
|
||||
n.updateP2MQueueMetric()
|
||||
immetrics.ClusterP2MEnqueueTotal.WithLabelValues(clusterPodName(), n.name, "ok").Inc()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
// Buffer is full. Wait briefly before giving up.
|
||||
timer := time.NewTimer(clusterP2MTimeout)
|
||||
timer := time.NewTimer(n.p2mEnqueueTimeout)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case n.p2mSender <- msg:
|
||||
case sender <- msg:
|
||||
n.updateP2MQueueMetric()
|
||||
immetrics.ClusterP2MEnqueueTotal.WithLabelValues(clusterPodName(), n.name, "ok").Inc()
|
||||
return nil
|
||||
case <-timer.C:
|
||||
return errors.New("cluster: load exceeded")
|
||||
immetrics.ClusterP2MEnqueueTotal.WithLabelValues(clusterPodName(), n.name, "timeout").Inc()
|
||||
return errClusterLoadExceeded
|
||||
}
|
||||
}
|
||||
|
||||
func clusterReqShard(msg *ClusterReq, count int) int {
|
||||
if count <= 1 || msg == nil {
|
||||
return 0
|
||||
}
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(types.TopicKey{TenantID: msg.TenantID, Topic: msg.RcptTo}.RoutingKey()))
|
||||
return int(h.Sum32() % uint32(count))
|
||||
}
|
||||
|
||||
// masterToProxyAsync forwards response from topic master to topic proxy
|
||||
// in a fire-and-forget manner.
|
||||
func (n *ClusterNode) masterToProxyAsync(msg *ClusterResp) error {
|
||||
@@ -430,8 +501,32 @@ func (n *ClusterNode) masterToProxyAsync(msg *ClusterResp) error {
|
||||
|
||||
// route routes server message within the cluster.
|
||||
func (n *ClusterNode) route(msg *ClusterRoute) error {
|
||||
var unused bool
|
||||
return n.call("Cluster.Route", msg, &unused)
|
||||
for attempt := 0; attempt <= n.routeRetries; attempt++ {
|
||||
var rejected bool
|
||||
err := n.call("Cluster.Route", msg, &rejected)
|
||||
if err != nil {
|
||||
immetrics.ClusterRouteTotal.WithLabelValues(clusterPodName(), n.name, "disconnected").Inc()
|
||||
return err
|
||||
}
|
||||
if !rejected {
|
||||
immetrics.ClusterRouteTotal.WithLabelValues(clusterPodName(), n.name, "ok").Inc()
|
||||
return nil
|
||||
}
|
||||
immetrics.ClusterRouteTotal.WithLabelValues(clusterPodName(), n.name, "peer_rejected").Inc()
|
||||
if attempt < n.routeRetries {
|
||||
time.Sleep(n.routeRetryBackoff * time.Duration(1<<attempt))
|
||||
}
|
||||
}
|
||||
seq := int64(0)
|
||||
topic := ""
|
||||
if msg != nil && msg.SrvMsg != nil && msg.SrvMsg.Data != nil {
|
||||
seq = msg.SrvMsg.Data.SeqId
|
||||
}
|
||||
if msg != nil && msg.SrvMsg != nil {
|
||||
topic = msg.SrvMsg.RcptTo
|
||||
}
|
||||
n.logOverload("route", msg.TenantID, topic, seq)
|
||||
return errClusterOverloaded
|
||||
}
|
||||
|
||||
// Cluster is the representation of the cluster.
|
||||
@@ -460,6 +555,8 @@ type Cluster struct {
|
||||
// running a separate event processing goroutine for each proxy session
|
||||
// leads to a rather large memory usage and excessive scheduling overhead.
|
||||
proxyEventQueue *concurrency.GoRoutinePool
|
||||
p2mBuffer int
|
||||
p2mWorkers int
|
||||
}
|
||||
|
||||
func (n *ClusterNode) stopMultiplexingSession(msess *Session) {
|
||||
@@ -600,7 +697,13 @@ func (c *Cluster) TopicMaster(msg *ClusterReq, rejected *bool) error {
|
||||
select {
|
||||
case globals.hub.routeCli <- msg.CliMsg:
|
||||
default:
|
||||
logs.Err.Println("cluster: route req failed - hub.route queue full")
|
||||
*rejected = true
|
||||
if sess != nil {
|
||||
reply := ErrServiceUnavailableReply(msg.CliMsg, msg.CliMsg.Timestamp)
|
||||
reply.Ctrl.Text = "cluster overloaded"
|
||||
sess.queueOut(reply)
|
||||
}
|
||||
node.logOverload("topic_master", msg.TenantID, msg.RcptTo, 0)
|
||||
}
|
||||
|
||||
case ProxyReqBgSession, ProxyReqMeUserAgent:
|
||||
@@ -681,7 +784,9 @@ func (c *Cluster) Route(msg *ClusterRoute, rejected *bool) error {
|
||||
|
||||
select {
|
||||
case globals.hub.routeSrv <- msg.SrvMsg:
|
||||
immetrics.ClusterRouteTotal.WithLabelValues(clusterPodName(), msg.Node, "ok").Inc()
|
||||
default:
|
||||
immetrics.ClusterRouteTotal.WithLabelValues(clusterPodName(), msg.Node, "hub_full").Inc()
|
||||
logError("cluster Route: server busy")
|
||||
}
|
||||
return nil
|
||||
@@ -1016,6 +1121,26 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
if err := json.Unmarshal(configString, &config); err != nil {
|
||||
logs.Err.Fatal(err)
|
||||
}
|
||||
if config.ProxyToMasterBuffer <= 0 {
|
||||
config.ProxyToMasterBuffer = clusterDefaultP2MBuffer
|
||||
}
|
||||
if config.ProxyToMasterWorkers <= 0 {
|
||||
config.ProxyToMasterWorkers = clusterDefaultP2MWorkers
|
||||
}
|
||||
if config.ProxyToMasterWorkers > config.ProxyToMasterBuffer {
|
||||
config.ProxyToMasterWorkers = config.ProxyToMasterBuffer
|
||||
}
|
||||
if config.ProxyToMasterEnqueueTimeoutMS <= 0 {
|
||||
config.ProxyToMasterEnqueueTimeoutMS = int(clusterDefaultP2MTimeout / time.Millisecond)
|
||||
}
|
||||
if config.RouteRetries < 0 {
|
||||
config.RouteRetries = 0
|
||||
} else if config.RouteRetries == 0 {
|
||||
config.RouteRetries = clusterDefaultRouteRetries
|
||||
}
|
||||
if config.RouteRetryBackoffMS <= 0 {
|
||||
config.RouteRetryBackoffMS = int(clusterDefaultRouteRetryBackoff / time.Millisecond)
|
||||
}
|
||||
|
||||
thisName := *self
|
||||
if thisName == "" {
|
||||
@@ -1039,6 +1164,8 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
fingerprint: time.Now().Unix(),
|
||||
nodes: make(map[string]*ClusterNode),
|
||||
proxyEventQueue: concurrency.NewGoRoutinePool(len(config.Nodes) * 5),
|
||||
p2mBuffer: config.ProxyToMasterBuffer,
|
||||
p2mWorkers: config.ProxyToMasterWorkers,
|
||||
}
|
||||
|
||||
var nodeNames []string
|
||||
@@ -1052,10 +1179,13 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
}
|
||||
|
||||
globals.cluster.nodes[host.Name] = &ClusterNode{
|
||||
address: host.Addr,
|
||||
name: host.Name,
|
||||
done: make(chan bool, 1),
|
||||
msess: make(map[string]struct{}),
|
||||
address: host.Addr,
|
||||
name: host.Name,
|
||||
done: make(chan bool, 1),
|
||||
msess: make(map[string]struct{}),
|
||||
p2mEnqueueTimeout: time.Duration(config.ProxyToMasterEnqueueTimeoutMS) * time.Millisecond,
|
||||
routeRetries: config.RouteRetries,
|
||||
routeRetryBackoff: time.Duration(config.RouteRetryBackoffMS) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,17 +1243,18 @@ func (c *Cluster) start() {
|
||||
logs.Err.Fatal(err)
|
||||
}
|
||||
|
||||
var bufferSize = clusterProxyToMasterBuffer
|
||||
if len(c.nodes) > 2 {
|
||||
// Expand the buffer for larger (>3 node) clusters.
|
||||
bufferSize += clusterProxyToMasterBufferPerNode * (len(c.nodes) - 2)
|
||||
}
|
||||
for _, n := range c.nodes {
|
||||
go n.reconnect()
|
||||
n.rpcDone = make(chan *rpc.Call, len(c.nodes)*clusterRpcCompletionBuffer)
|
||||
n.p2mSender = make(chan *ClusterReq, bufferSize)
|
||||
workers := c.p2mWorkers
|
||||
perWorker := (c.p2mBuffer + workers - 1) / workers
|
||||
n.p2mSenders = make([]chan *ClusterReq, workers)
|
||||
go n.asyncRpcLoop()
|
||||
go n.p2mSenderLoop()
|
||||
for i := range n.p2mSenders {
|
||||
n.p2mSenders[i] = make(chan *ClusterReq, perWorker)
|
||||
go n.p2mSenderLoop(n.p2mSenders[i])
|
||||
}
|
||||
immetrics.ClusterP2MQueueCapacity.WithLabelValues(clusterPodName(), n.name).Set(float64(perWorker * workers))
|
||||
}
|
||||
|
||||
if c.fo != nil {
|
||||
@@ -1147,7 +1278,9 @@ func (c *Cluster) shutdown() {
|
||||
}
|
||||
for _, n := range c.nodes {
|
||||
close(n.rpcDone)
|
||||
close(n.p2mSender)
|
||||
for _, sender := range n.p2mSenders {
|
||||
close(sender)
|
||||
}
|
||||
}
|
||||
|
||||
globals.cluster.proxyEventQueue.Stop()
|
||||
|
||||
Reference in New Issue
Block a user