mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-28 20:06:14 +00:00
* master: never re-seed a raft cluster over committed state -raftBootstrap deleted logs.dat, stable.dat and snapshots on every start and then bootstrapped a fresh cluster. Since hashicorp raft only snapshots after 8192 log entries, the TopologyId lives in the log, not in a snapshot, so the pre-wipe snapshot recovery found nothing and each restart minted a new cluster identity. A master that came up while it could not reach its peers seeded a rival cluster; when the two logs met, SetTopologyId's split-brain guard fatally stopped every master holding the other id, and the master layer crash-looped with no quorum. Bootstrapping is genesis. Drop the wipe and the inline bootstrap. The first master in -peers already mints a cluster once it has confirmed no peer has a leader, so the flag has nothing left to do and is now ignored; keeping that one master the sole bootstrap authority is what stops a partition from minting two clusters, so the flag must not widen it either. A master with state rejoins its peers, and one whose data dir was reset is admitted by the sitting leader instead of forking again. * test: cover -raftBootstrap restarts in the multi-master suite Three masters start with -raftBootstrap, the way the helm chart renders it on every master on every roll, and the cluster has to hold one TopologyId after they all restart. /dir/status is proxied to the leader, so each master's own view of the identity is read out of its log, which is where a fork shows up. Before the fix the hashicorp case minted a new id on each restart.
213 lines
5.9 KiB
Go
213 lines
5.9 KiB
Go
package multi_master
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
)
|
|
|
|
var raftImplementations = []struct {
|
|
name string
|
|
raftHashicorp bool
|
|
}{
|
|
{"goraft", false},
|
|
{"hashicorp", true},
|
|
}
|
|
|
|
// TestFreshClusterFormsWithoutALeader covers the other half of the bootstrap
|
|
// decision: with no leader anywhere, three masters starting together still have
|
|
// to mint a cluster between them.
|
|
func TestFreshClusterFormsWithoutALeader(t *testing.T) {
|
|
for _, impl := range raftImplementations {
|
|
t.Run(impl.name, func(t *testing.T) {
|
|
mc := NewMasterCluster(t, impl.raftHashicorp)
|
|
for i := range 3 {
|
|
mc.StartNode(i)
|
|
}
|
|
|
|
if _, err := waitForCommonLeader(mc, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("fresh cluster did not converge: %v", err)
|
|
}
|
|
for i := range 3 {
|
|
if err := waitForPeerCount(mc, i, 2, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("master %d does not see the full cluster: %v", i, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestScaleUpOntoExistingLeader mirrors a Kubernetes master StatefulSet whose
|
|
// replica count goes back from one to three: master 0 keeps running as the
|
|
// leader of a single-peer cluster while two fresh masters come up pointing at
|
|
// all three. The newcomers start with an empty raft log, so neither raft
|
|
// implementation lets them campaign — the sitting leader has to admit them.
|
|
func TestScaleUpOntoExistingLeader(t *testing.T) {
|
|
for _, impl := range raftImplementations {
|
|
t.Run(impl.name, func(t *testing.T) {
|
|
mc := NewMasterCluster(t, impl.raftHashicorp)
|
|
|
|
// Master 0 is alone in its peer list, the way the operator renders
|
|
// -peers when spec.master.replicas is 1.
|
|
mc.SetNodePeers(0, mc.NodeAddress(0))
|
|
mc.StartNode(0)
|
|
if err := mc.WaitForLeader(waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("single master did not become leader: %v", err)
|
|
}
|
|
|
|
// Scale up. These two carry the full peer list; master 0 still
|
|
// runs with the old one and has never heard of them.
|
|
mc.StartNode(1)
|
|
mc.StartNode(2)
|
|
|
|
leader, err := waitForCommonLeader(mc, waitTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("masters did not converge after scaling up: %v", err)
|
|
}
|
|
if leader != mc.NodeAddress(0) {
|
|
t.Fatalf("leader moved to %s, want the sitting leader %s", leader, mc.NodeAddress(0))
|
|
}
|
|
|
|
if err := waitForPeerCount(mc, 0, 2, waitTimeout); err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("leader did not admit both new masters: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// waitForCommonLeader waits until every running master names the same leader,
|
|
// and returns it.
|
|
func waitForCommonLeader(mc *MasterCluster, timeout time.Duration) (string, error) {
|
|
var lastErr error
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
leader, err := commonLeader(mc)
|
|
if err == nil {
|
|
return leader, nil
|
|
}
|
|
lastErr = err
|
|
time.Sleep(waitTick)
|
|
}
|
|
return "", lastErr
|
|
}
|
|
|
|
func commonLeader(mc *MasterCluster) (string, error) {
|
|
agreed := ""
|
|
for i := range 3 {
|
|
if !mc.IsNodeRunning(i) {
|
|
continue
|
|
}
|
|
cs, err := mc.GetClusterStatus(i)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
leader := pb.ServerAddress(cs.Leader).ToHttpAddress()
|
|
if leader == "" {
|
|
return "", fmt.Errorf("master %d has no leader", i)
|
|
}
|
|
if agreed == "" {
|
|
agreed = leader
|
|
} else if agreed != leader {
|
|
return "", fmt.Errorf("masters disagree on the leader: %s and %s", agreed, leader)
|
|
}
|
|
}
|
|
if agreed == "" {
|
|
return "", fmt.Errorf("no master is running")
|
|
}
|
|
return agreed, nil
|
|
}
|
|
|
|
// waitForPeerCount waits until node i reports the given number of raft peers.
|
|
// The count excludes the node itself.
|
|
func waitForPeerCount(mc *MasterCluster, i, want int, timeout time.Duration) error {
|
|
got := -1
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
cs, err := mc.GetClusterStatus(i)
|
|
if err == nil {
|
|
got = peerCountExcludingSelf(cs.Peers, mc.NodeAddress(i))
|
|
if got == want {
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(waitTick)
|
|
}
|
|
return fmt.Errorf("master %d reports %d peers, want %d", i, got, want)
|
|
}
|
|
|
|
func peerCountExcludingSelf(peers []string, self string) int {
|
|
count := 0
|
|
for _, peer := range peers {
|
|
if pb.ServerAddress(peer).ToHttpAddress() != self {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// TestRaftBootstrapKeepsExistingCluster covers a master restarting under
|
|
// -raftBootstrap, the way the helm chart renders it on every master on every
|
|
// roll. Bootstrapping is genesis: seeding a second cluster over committed raft
|
|
// state mints a rival TopologyId, and the split-brain guard then Fatals every
|
|
// master that still holds the first one.
|
|
func TestRaftBootstrapKeepsExistingCluster(t *testing.T) {
|
|
for _, impl := range raftImplementations {
|
|
t.Run(impl.name, func(t *testing.T) {
|
|
mc := NewMasterCluster(t, impl.raftHashicorp)
|
|
for i := range 3 {
|
|
mc.SetRaftBootstrap(i)
|
|
mc.StartNode(i)
|
|
}
|
|
before, err := mc.WaitForTopologyId(waitTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("cluster did not mint a TopologyId: %v", err)
|
|
}
|
|
|
|
for i := range 3 {
|
|
mc.StopNode(i)
|
|
}
|
|
for i := range 3 {
|
|
mc.StartNode(i)
|
|
}
|
|
|
|
after, err := mc.WaitForTopologyId(waitTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatalf("cluster did not come back after a restart: %v", err)
|
|
}
|
|
if after != before {
|
|
mc.DumpLogs()
|
|
t.Fatalf("-raftBootstrap re-seeded the cluster: TopologyId %s became %s", before, after)
|
|
}
|
|
|
|
// The leader answers for the whole cluster, so a follower that
|
|
// forked is only visible in its own log.
|
|
seen, err := mc.WaitForNodeTopologyIds(waitTimeout)
|
|
if err != nil {
|
|
mc.DumpLogs()
|
|
t.Fatal(err)
|
|
}
|
|
for i, ids := range seen {
|
|
for _, id := range ids {
|
|
if id != before {
|
|
mc.DumpLogs()
|
|
t.Fatalf("master %d saw TopologyId %s, want %s", i, id, before)
|
|
}
|
|
}
|
|
if mc.LogContains(i, "Split-brain detected") {
|
|
mc.DumpLogs()
|
|
t.Fatalf("master %d hit the split-brain guard", i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|