348 lines
9.9 KiB
Go
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
|
|
}
|
|
}
|
|
}
|