Files
im_server/server/cluster_leader.go
T
2026-08-11 15:59:57 +08:00

348 lines
9.9 KiB
Go

package main
import (
"math/rand"
"net/rpc"
"sync"
"time"
"github.com/tinode/chat/server/logs"
)
// Cluster methods related to leader node election. Based on ideas from Raft protocol.
// The leader node issues heartbeats to follower nodes. If the follower node fails enough
// times, the leader node annouces it dead and initiates rehashing: it regenerates ring hash with
// only live nodes and communicates the new list of nodes to followers. They in turn do their
// rehashing using the new list. When the dead node is revived, rehashing happens again.
// Failover config.
type clusterFailover struct {
// Current leader
leader string
// Current election term
term int
// Hearbeat interval
heartBeat time.Duration
// Vote timeout: the number of missed heartbeats before a new election is initiated.
voteTimeout int
// The list of nodes the leader considers active
activeNodes []string
activeNodesLock sync.RWMutex
// The number of heartbeats a node can fail before being declared dead
nodeFailCountLimit int
// Channel for processing leader health checks.
healthCheck chan *ClusterHealth
// Channel for processing election votes.
electionVote chan *ClusterVote
// Channel for stopping the failover runner.
done chan bool
}
type clusterFailoverConfig struct {
// Failover is enabled
Enabled bool `json:"enabled"`
// Time in milliseconds between heartbeats
Heartbeat int `json:"heartbeat"`
// Number of failed heartbeats before a leader election is initiated.
VoteAfter int `json:"vote_after"`
// Number of failures before a node is considered dead
NodeFailAfter int `json:"node_fail_after"`
}
// ClusterHealth is content of a leader's health check of a follower node.
type ClusterHealth struct {
// Name of the leader node
Leader string
// Election term
Term int
// Ring hash signature that represents the cluster
Signature string
// Names of nodes currently active in the cluster
Nodes []string
}
// ClusterVoteRequest is a request from a leader candidate to a node to vote for the candidate.
type ClusterVoteRequest struct {
// Candidate node which issued this request
Node string
// Election term
Term int
}
// ClusterVoteResponse is a vote from a node.
type ClusterVoteResponse struct {
// Actual vote
Result bool
// Node's term after the vote
Term int
}
// ClusterVote is a vote request and a response in leader election.
type ClusterVote struct {
req *ClusterVoteRequest
resp chan ClusterVoteResponse
}
func (c *Cluster) failoverInit(config *clusterFailoverConfig) bool {
if config == nil || !config.Enabled {
return false
}
if len(c.nodes) < 2 {
logs.Err.Printf("cluster: failover disabled; need at least 3 nodes, got %d", len(c.nodes)+1)
return false
}
// Generate ring hash on the assumption that all nodes are alive and well.
// This minimizes rehashing during normal operations.
var activeNodes []string
for _, node := range c.nodes {
activeNodes = append(activeNodes, node.name)
}
activeNodes = append(activeNodes, c.thisNodeName)
c.rehash(activeNodes)
// Random heartbeat ticker: 0.75 * config.HeartBeat + random(0, 0.5 * config.HeartBeat).
// The PRNG is initialized in main.go.
hb := time.Duration(config.Heartbeat) * time.Millisecond
hb = (hb >> 1) + (hb >> 2) + time.Duration(rand.Intn(int(hb>>1)))
c.fo = &clusterFailover{
activeNodes: activeNodes,
heartBeat: hb,
voteTimeout: config.VoteAfter,
nodeFailCountLimit: config.NodeFailAfter,
healthCheck: make(chan *ClusterHealth, config.VoteAfter),
electionVote: make(chan *ClusterVote, len(c.nodes)),
done: make(chan bool, 1),
}
logs.Info.Println("cluster: failover mode enabled")
return true
}
// Health is called by the leader node to assert leadership and check status
// of the followers.
func (c *Cluster) Health(health *ClusterHealth, unused *bool) error {
select {
case c.fo.healthCheck <- health:
default:
}
return nil
}
// Vote processes request for a vote from a candidate.
func (c *Cluster) Vote(vreq *ClusterVoteRequest, response *ClusterVoteResponse) error {
respChan := make(chan ClusterVoteResponse, 1)
c.fo.electionVote <- &ClusterVote{
req: vreq,
resp: respChan,
}
*response = <-respChan
return nil
}
// Cluster leader checks health of follower nodes.
func (c *Cluster) sendHealthChecks() {
rehash := false
for _, node := range c.nodes {
unused := false
err := node.call("Cluster.Health",
&ClusterHealth{
Leader: c.thisNodeName,
Term: c.fo.term,
Signature: c.ring.Signature(),
Nodes: c.fo.activeNodes,
}, &unused)
if err != nil {
node.failCount++
if node.failCount == c.fo.nodeFailCountLimit {
// Node failed too many times
rehash = true
}
} else {
if node.failCount >= c.fo.nodeFailCountLimit {
// Node has recovered
rehash = true
}
node.failCount = 0
}
}
if rehash {
activeNodes := []string{c.thisNodeName}
for _, node := range c.nodes {
if node.failCount < c.fo.nodeFailCountLimit {
activeNodes = append(activeNodes, node.name)
}
}
c.fo.activeNodesLock.Lock()
c.fo.activeNodes = activeNodes
c.fo.activeNodesLock.Unlock()
c.rehash(activeNodes)
c.invalidateProxySubs("")
c.gcProxySessions(activeNodes)
logs.Info.Println("cluster: initiating failover rehash for nodes", activeNodes)
globals.hub.rehash <- true
}
}
func (c *Cluster) electLeader() {
// Increment the term (voting for myself in this term) and clear the leader
c.fo.term++
c.fo.leader = ""
// Make sure the current node does not report itself as a leader.
statsSet("ClusterLeader", 0)
logs.Info.Println("cluster: leading new election for term", c.fo.term)
nodeCount := len(c.nodes)
// Number of votes needed to elect the leader
expectVotes := (nodeCount+1)>>1 + 1
done := make(chan *rpc.Call, nodeCount)
// Send async requests for votes to other nodes
for _, node := range c.nodes {
response := ClusterVoteResponse{}
node.callAsync("Cluster.Vote",
&ClusterVoteRequest{
Node: c.thisNodeName,
Term: c.fo.term,
}, &response, done)
}
// Number of votes received (1 vote for self)
voteCount := 1
timeout := time.NewTimer(c.fo.heartBeat>>1 + c.fo.heartBeat)
// Wait for one of the following
// 1. More than half of the nodes voting in favor
// 2. All nodes responded.
// 3. Timeout.
for i := 0; i < nodeCount && voteCount < expectVotes; {
select {
case call := <-done:
if call.Error == nil {
if call.Reply.(*ClusterVoteResponse).Result {
// Vote in my favor
voteCount++
} else if c.fo.term < call.Reply.(*ClusterVoteResponse).Term {
// Vote against me. Abandon vote: this node's term is behind the cluster
i = nodeCount
voteCount = 0
}
}
i++
case <-timeout.C:
// break the loop
i = nodeCount
}
}
if voteCount >= expectVotes {
// Current node elected as the leader.
c.fo.leader = c.thisNodeName
statsSet("ClusterLeader", 1)
logs.Info.Printf("'%s' elected self as a new leader", c.thisNodeName)
}
}
// Go routine that processes calls related to leader election and maintenance.
func (c *Cluster) run() {
ticker := time.NewTicker(c.fo.heartBeat)
// Count of missed health checks from the leader.
missed := 0
// Don't rehash immediately on the first missed health check. If this node just came online, leader will
// account it on the next check. Otherwise it will be rehashing twice.
rehashSkipped := false
for {
select {
case <-ticker.C:
if c.fo.leader == c.thisNodeName {
// I'm the leader, send the health checks to followers.
c.sendHealthChecks()
} else {
// Increment the number of missed health checks from the leader.
// The counter will be reset to zero when a health check is received.
missed++
if missed >= c.fo.voteTimeout {
// Leader is gone, initiate election of a new leader.
missed = 0
c.electLeader()
}
}
case health := <-c.fo.healthCheck:
// Health check from the leader.
if health.Term < c.fo.term {
// This is a health check from a stale leader. Ignore.
logs.Warn.Println("cluster: health check from a stale leader", health.Term, c.fo.term, health.Leader, c.fo.leader)
continue
}
if health.Term > c.fo.term {
c.fo.term = health.Term
c.fo.leader = health.Leader
logs.Info.Printf("cluster: leader '%s' elected", c.fo.leader)
} else if health.Leader != c.fo.leader {
if c.fo.leader != "" {
// Wrong leader. It's a bug, should never happen!
logs.Err.Printf("cluster: wrong leader '%s' while expecting '%s'; term %d",
health.Leader, c.fo.leader, health.Term)
} else {
logs.Info.Printf("cluster: leader set to '%s'", health.Leader)
}
c.fo.leader = health.Leader
}
// This is a health check from a leader, consequently this node is not the leader.
statsSet("ClusterLeader", 0)
missed = 0
if health.Signature != c.ring.Signature() {
if rehashSkipped {
logs.Info.Println("cluster: rehashing at a request of",
health.Leader, health.Nodes, health.Signature, c.ring.Signature())
c.rehash(health.Nodes)
c.invalidateProxySubs("")
c.gcProxySessions(health.Nodes)
rehashSkipped = false
globals.hub.rehash <- true
} else {
rehashSkipped = true
}
}
case vreq := <-c.fo.electionVote:
if c.fo.term < vreq.req.Term {
// This is a new election. This node has not voted yet. Vote for the requestor and
// clear the current leader.
logs.Info.Printf("Voting YES for %s, my term %d, vote term %d", vreq.req.Node, c.fo.term, vreq.req.Term)
c.fo.term = vreq.req.Term
c.fo.leader = ""
// Election means these is no leader yet.
statsSet("ClusterLeader", 0)
vreq.resp <- ClusterVoteResponse{Result: true, Term: c.fo.term}
} else {
// This node has voted already or stale election, reject.
logs.Info.Printf("Voting NO for %s, my term %d, vote term %d", vreq.req.Node, c.fo.term, vreq.req.Term)
vreq.resp <- ClusterVoteResponse{Result: false, Term: c.fo.term}
}
case <-c.fo.done:
return
}
}
}