mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-28 20:06:14 +00:00
* master: answer with the leader raft already knows Topo.Leader() backs off for up to 20 seconds waiting for an election. Callers that a health probe or a client is blocked on cannot afford that: /cluster/status, /cluster/healthz and /readyz all sit past the probe timeout of both the helm chart and the operator, so a master that is still joining looks dead rather than joining, and the kubelet restarts it. informNewLeader and SendHeartbeat hold the client on a master that cannot serve it, exactly when it should move on to find the one that can. Answer these from MaybeLeader instead, which reports what raft knows right now. MaybeLeader takes over the "am I the leader myself" fallback that Leader() used to apply on top of it, so one non-blocking call is still correct; Leader() keeps the backoff for callers that must wait. * master: let the leader admit a master that starts with no raft state Neither raft implementation lets a server outside the configuration campaign: goraft's promotable() requires a non-empty log, and hashicorp rejects vote requests from a candidate that is not in its configuration. A master that comes up with fresh state therefore cannot elect itself in — the leader has to pull it in. Nothing did. The peer list is static, rendered from the replica count, so scaling it up leaves the sitting leader running the old list with no idea the new masters exist. Under goraft they wait forever. Under hashicorp they are worse off: each bootstraps a cluster of its own from the new list, and two of them form a quorum next to the live leader, with their own TopologyId. That is the split brain SetTopologyId kills a master over. Admit the peer where it registers instead. Only the leader gets past the IsLeader check in KeepConnected, and a joining master's client lands there, so that is the moment it joins. The broadcast OnPeerUpdate rides on is not enough on its own: it only reaches masters already connected, which is why a leader that came up first missed both newcomers. RaftAddServer grew a goraft branch on the way, so cluster.raft.add stops silently doing nothing on the default raft, and RaftRemoveServer with it. Bootstrapping is now one call for both implementations, made only after the peers confirm nobody has a leader, and retried until this master is in rather than checked once and dropped. * master: do not evict a peer that is still in -peers The hashicorp leader drops a master from the raft configuration as soon as it stops answering pings. A master that is merely restarting answers nothing, so an ordinary bounce shrinks the quorum behind the operator's back — and then races its own return: the master comes back, registers, gets re-admitted, and the eviction lands after it. A randomized start/stop walk lands on it. Two of three masters running, the leader evicts the one that just went down, the restart re-adds it, the removal commits late and takes the leader's own leadership with it. What is left is a two-server configuration whose other half is down, and a running master that nobody will ask for a vote — no quorum, no way back until the third master returns. -peers is what declares membership. updatePeers already reconciles the configuration against it on every leadership change, and an operator who really means to drop a master can say so with cluster.raft.remove, so keep the eviction for masters that are no longer listed at all. * test: bounce masters at random and hold the election to it Twelve rounds of stopping or starting a random master, on both raft implementations, checking the two things an election must never get wrong: two masters claiming leadership at once, and a quorum that comes back without agreeing on one. The cluster's identity has to survive the whole walk, since a master that re-mints a TopologyId is the split brain SetTopologyId kills its peers over. The seed is random and logged, so a failure names the walk that reproduces it. Below a quorum the walk moves straight on. A master that has lost its quorum cannot commit anything, and goraft only checks whether it still has one on an election-timeout ticker, after its peers have been quiet for a full timeout — measured taking over 30 seconds to step down. That direction belongs to TestTwoMastersDownAndRestart, which was giving it ten seconds and would have started failing on a slower machine; it now waits on that behaviour explicitly rather than sleeping twice and hoping. WaitForTopologyId returns the id it waited for. Reading it separately raced the leader applying the raft entry that carries it, which shows up as an empty id right after an election rather than as a wrong one.
310 lines
9.3 KiB
Go
310 lines
9.3 KiB
Go
package multi_master
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
)
|
|
|
|
const (
|
|
// Election timeout is 3s in our cluster config; allow generous margin.
|
|
leaderElectionTimeout = 30 * time.Second
|
|
|
|
// Losing a quorum is much slower to show than winning one. goraft only
|
|
// notices on an election-timeout ticker, after its peers have been quiet
|
|
// for a full timeout, and has been measured taking over 30s to step down.
|
|
leaderStepDownTimeout = 60 * time.Second
|
|
)
|
|
|
|
// TestLeaderDownAndRecoverQuickly verifies that when the leader is stopped and
|
|
// restarted quickly, the cluster re-elects a leader and the restarted node
|
|
// rejoins as a follower. TopologyId must be consistent across all nodes.
|
|
func TestLeaderDownAndRecoverQuickly(t *testing.T) {
|
|
mc := StartMasterCluster(t)
|
|
|
|
// Record initial state.
|
|
leaderIdx, leaderAddr := mc.FindLeader()
|
|
if leaderIdx < 0 {
|
|
t.Fatal("no leader found after cluster start")
|
|
}
|
|
t.Logf("initial leader: node %d at %s", leaderIdx, leaderAddr)
|
|
|
|
topologyId, err := mc.GetTopologyId(leaderIdx)
|
|
if err != nil || topologyId == "" {
|
|
t.Fatalf("failed to get initial TopologyId: %v", err)
|
|
}
|
|
t.Logf("initial TopologyId: %s", topologyId)
|
|
|
|
// Stop the leader.
|
|
mc.StopNode(leaderIdx)
|
|
t.Logf("stopped leader node %d", leaderIdx)
|
|
|
|
// Wait for a new leader from the remaining 2 nodes.
|
|
newLeaderIdx, newLeaderAddr, err := mc.WaitForNewLeader(leaderAddr, leaderElectionTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("new leader not elected after stopping old leader: %v", err)
|
|
}
|
|
t.Logf("new leader: node %d at %s", newLeaderIdx, newLeaderAddr)
|
|
|
|
// Restart the old leader quickly.
|
|
mc.StartNode(leaderIdx)
|
|
if err := mc.WaitForNodeReady(leaderIdx, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("restarted node %d not ready: %v", leaderIdx, err)
|
|
}
|
|
t.Logf("restarted node %d", leaderIdx)
|
|
|
|
// Give raft time to settle.
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Verify leader is stable.
|
|
finalLeaderIdx, _ := mc.FindLeader()
|
|
if finalLeaderIdx < 0 {
|
|
mc.DumpLogs()
|
|
t.Fatal("no leader after restarting old leader node")
|
|
}
|
|
|
|
// Verify TopologyId is consistent across all nodes.
|
|
assertTopologyIdConsistent(t, mc, topologyId)
|
|
}
|
|
|
|
// TestLeaderDownSlowRecover verifies that when the leader goes down and takes
|
|
// a long time to come back, the remaining 2 nodes elect a new leader and the
|
|
// cluster continues to function. When the slow node returns, it rejoins.
|
|
func TestLeaderDownSlowRecover(t *testing.T) {
|
|
mc := StartMasterCluster(t)
|
|
|
|
leaderIdx, leaderAddr := mc.FindLeader()
|
|
if leaderIdx < 0 {
|
|
t.Fatal("no leader found")
|
|
}
|
|
topologyId, err := mc.GetTopologyId(leaderIdx)
|
|
if err != nil || topologyId == "" {
|
|
t.Fatalf("failed to get initial TopologyId: %v", err)
|
|
}
|
|
t.Logf("initial leader: node %d, TopologyId: %s", leaderIdx, topologyId)
|
|
|
|
// Stop the leader.
|
|
mc.StopNode(leaderIdx)
|
|
|
|
// Wait for a new leader.
|
|
newLeaderIdx, _, err := mc.WaitForNewLeader(leaderAddr, leaderElectionTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("new leader not elected: %v", err)
|
|
}
|
|
t.Logf("new leader: node %d", newLeaderIdx)
|
|
|
|
// Verify cluster functions with only 2 nodes (quorum is 2/3).
|
|
cs, err := mc.GetClusterStatus(newLeaderIdx)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("cannot get cluster status from new leader: %v", err)
|
|
}
|
|
if !cs.IsLeader {
|
|
t.Fatalf("node %d claims not to be leader", newLeaderIdx)
|
|
}
|
|
|
|
// Simulate slow recovery: wait significantly longer than election timeout.
|
|
t.Log("simulating slow recovery (10 seconds)...")
|
|
time.Sleep(10 * time.Second)
|
|
|
|
// Verify leader is still stable during the outage.
|
|
stableLeaderIdx, _ := mc.FindLeader()
|
|
if stableLeaderIdx < 0 {
|
|
mc.DumpLogs()
|
|
t.Fatal("leader lost during extended outage of one node")
|
|
}
|
|
|
|
// Restart the downed node.
|
|
mc.StartNode(leaderIdx)
|
|
if err := mc.WaitForNodeReady(leaderIdx, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("slow-recovered node %d not ready: %v", leaderIdx, err)
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
assertTopologyIdConsistent(t, mc, topologyId)
|
|
}
|
|
|
|
// TestTwoMastersDownAndRestart verifies that when 2 of 3 masters go down
|
|
// (losing quorum), the cluster cannot elect a leader. When both restart,
|
|
// a leader is elected and TopologyId is preserved.
|
|
func TestTwoMastersDownAndRestart(t *testing.T) {
|
|
mc := StartMasterCluster(t)
|
|
|
|
leaderIdx, _ := mc.FindLeader()
|
|
if leaderIdx < 0 {
|
|
t.Fatal("no leader found")
|
|
}
|
|
topologyId, err := mc.GetTopologyId(leaderIdx)
|
|
if err != nil || topologyId == "" {
|
|
t.Fatalf("failed to get initial TopologyId: %v", err)
|
|
}
|
|
t.Logf("initial TopologyId: %s", topologyId)
|
|
|
|
// Determine which 2 nodes to stop (stop the leader + one follower).
|
|
down1 := leaderIdx
|
|
down2 := (leaderIdx + 1) % 3
|
|
survivor := (leaderIdx + 2) % 3
|
|
t.Logf("stopping nodes %d and %d, keeping node %d", down1, down2, survivor)
|
|
|
|
mc.StopNode(down1)
|
|
mc.StopNode(down2)
|
|
|
|
// The surviving node alone cannot form a quorum, so it has to give up the
|
|
// leadership it is still claiming.
|
|
if err := mc.WaitForNoLeader(leaderStepDownTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("expected no leader with only 1 of 3 nodes: %v", err)
|
|
}
|
|
|
|
// Restart both downed nodes.
|
|
mc.StartNode(down1)
|
|
mc.StartNode(down2)
|
|
for _, i := range []int{down1, down2} {
|
|
if err := mc.WaitForNodeReady(i, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("restarted node %d not ready: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Wait for leader election.
|
|
if err := mc.WaitForLeader(leaderElectionTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("no leader after restarting 2 downed nodes: %v", err)
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
assertTopologyIdConsistent(t, mc, topologyId)
|
|
}
|
|
|
|
// TestAllMastersDownAndRestart verifies that when all 3 masters are stopped
|
|
// and restarted, the cluster elects a leader and all nodes agree on a
|
|
// TopologyId. With RaftResumeState=false (default), raft state is cleared on
|
|
// restart. The TopologyId is recovered from snapshots when available; on a
|
|
// short-lived cluster that hasn't taken snapshots on all nodes, a new
|
|
// TopologyId may be generated — but all nodes must still agree.
|
|
func TestAllMastersDownAndRestart(t *testing.T) {
|
|
mc := StartMasterCluster(t)
|
|
|
|
leaderIdx, _ := mc.FindLeader()
|
|
if leaderIdx < 0 {
|
|
t.Fatal("no leader found")
|
|
}
|
|
topologyId, _ := mc.GetTopologyId(leaderIdx)
|
|
if topologyId == "" {
|
|
t.Fatal("no TopologyId on initial leader")
|
|
}
|
|
t.Logf("initial TopologyId: %s", topologyId)
|
|
|
|
// Stop all nodes.
|
|
for i := range 3 {
|
|
mc.StopNode(i)
|
|
}
|
|
t.Log("all nodes stopped")
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Restart all nodes.
|
|
for i := range 3 {
|
|
mc.StartNode(i)
|
|
}
|
|
for i := range 3 {
|
|
if err := mc.WaitForNodeReady(i, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("node %d not ready after full restart: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Wait for leader.
|
|
if err := mc.WaitForLeader(leaderElectionTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("no leader after full cluster restart: %v", err)
|
|
}
|
|
|
|
newLeaderIdx, _ := mc.FindLeader()
|
|
t.Logf("leader after full restart: node %d", newLeaderIdx)
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// All nodes must agree on a TopologyId (may differ from original if
|
|
// snapshots were not yet taken on all nodes before shutdown).
|
|
newTopologyId, err := mc.GetTopologyId(newLeaderIdx)
|
|
if err != nil || newTopologyId == "" {
|
|
mc.DumpLogs()
|
|
t.Fatal("no TopologyId after full restart")
|
|
}
|
|
if newTopologyId == topologyId {
|
|
t.Logf("TopologyId preserved across full restart: %s", topologyId)
|
|
} else {
|
|
t.Logf("TopologyId changed (expected for short-lived cluster without snapshots): %s -> %s", topologyId, newTopologyId)
|
|
}
|
|
assertTopologyIdConsistent(t, mc, newTopologyId)
|
|
}
|
|
|
|
// TestLeaderConsistencyAcrossNodes verifies that all nodes agree on who the
|
|
// leader is and report the same TopologyId.
|
|
func TestLeaderConsistencyAcrossNodes(t *testing.T) {
|
|
mc := StartMasterCluster(t)
|
|
|
|
// Allow cluster to stabilize.
|
|
time.Sleep(3 * time.Second)
|
|
|
|
leaderIdx, leaderAddr := mc.FindLeader()
|
|
if leaderIdx < 0 {
|
|
t.Fatal("no leader found")
|
|
}
|
|
t.Logf("leader: node %d at %s", leaderIdx, leaderAddr)
|
|
|
|
// Every node should agree on the leader.
|
|
for i := range 3 {
|
|
cs, err := mc.GetClusterStatus(i)
|
|
if err != nil {
|
|
t.Fatalf("node %d cluster/status error: %v", i, err)
|
|
}
|
|
if i == leaderIdx {
|
|
if !cs.IsLeader {
|
|
t.Errorf("node %d should be leader but IsLeader=false", i)
|
|
}
|
|
} else {
|
|
if cs.IsLeader {
|
|
t.Errorf("node %d should not be leader but IsLeader=true", i)
|
|
}
|
|
// cs.Leader is a ServerAddress like "127.0.0.1:10000.20000";
|
|
// convert to HTTP address for comparison with leaderAddr.
|
|
leaderHttp := pb.ServerAddress(cs.Leader).ToHttpAddress()
|
|
if leaderHttp != leaderAddr {
|
|
t.Errorf("node %d reports leader %q (http: %s), expected %q", i, cs.Leader, leaderHttp, leaderAddr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// All nodes should have the same TopologyId.
|
|
topologyId, _ := mc.GetTopologyId(leaderIdx)
|
|
if topologyId == "" {
|
|
t.Fatal("leader has no TopologyId")
|
|
}
|
|
assertTopologyIdConsistent(t, mc, topologyId)
|
|
}
|
|
|
|
// assertTopologyIdConsistent verifies that all running nodes report the expected TopologyId.
|
|
func assertTopologyIdConsistent(t *testing.T, mc *MasterCluster, expectedId string) {
|
|
t.Helper()
|
|
for i := range 3 {
|
|
if !mc.IsNodeRunning(i) {
|
|
continue
|
|
}
|
|
id, err := mc.GetTopologyId(i)
|
|
if err != nil {
|
|
t.Errorf("node %d: failed to get TopologyId: %v", i, err)
|
|
continue
|
|
}
|
|
if id != expectedId {
|
|
t.Errorf("node %d: TopologyId=%q, expected %q", i, id, expectedId)
|
|
}
|
|
}
|
|
}
|