mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-25 16:47:28 +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.
455 lines
12 KiB
Go
455 lines
12 KiB
Go
package multi_master
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/seaweedfs/seaweedfs/test/testutil"
|
||
)
|
||
|
||
const (
|
||
waitTimeout = 30 * time.Second
|
||
waitTick = 200 * time.Millisecond
|
||
)
|
||
|
||
// masterNode represents a single master process in the cluster.
|
||
type masterNode struct {
|
||
port int
|
||
grpcPort int
|
||
dataDir string
|
||
cmd *exec.Cmd
|
||
logFile string
|
||
stopped bool
|
||
// peersStr overrides the cluster-wide peer list for this node, so a test
|
||
// can start a master that only knows about a subset of the cluster.
|
||
peersStr string
|
||
}
|
||
|
||
// MasterCluster manages a 3-node master raft cluster for integration tests.
|
||
type MasterCluster struct {
|
||
t testing.TB
|
||
weedBinary string
|
||
baseDir string
|
||
logsDir string
|
||
keepLogs bool
|
||
|
||
nodes [3]*masterNode
|
||
mu sync.Mutex
|
||
|
||
// peers string shared by all nodes, e.g. "127.0.0.1:9333,127.0.0.1:9334,127.0.0.1:9335"
|
||
peersStr string
|
||
|
||
// raftHashicorp starts the masters with -raftHashicorp
|
||
raftHashicorp bool
|
||
}
|
||
|
||
// clusterStatus is the JSON returned by /cluster/status.
|
||
type clusterStatus struct {
|
||
IsLeader bool `json:"IsLeader"`
|
||
Leader string `json:"Leader"`
|
||
Peers []string `json:"Peers"`
|
||
}
|
||
|
||
// StartMasterCluster boots a 3-node master raft cluster and waits for a leader.
|
||
func StartMasterCluster(t testing.TB) *MasterCluster {
|
||
t.Helper()
|
||
|
||
mc := NewMasterCluster(t, false)
|
||
for i := range 3 {
|
||
mc.StartNode(i)
|
||
}
|
||
|
||
if err := mc.WaitForLeader(waitTimeout); err != nil {
|
||
mc.DumpLogs()
|
||
mc.StopAll()
|
||
t.Fatalf("cluster did not elect a leader: %v", err)
|
||
}
|
||
|
||
// Wait for TopologyId to be generated and propagated. This is async
|
||
// after leader election, and we need it committed before tests can
|
||
// reliably stop/restart nodes.
|
||
if _, err := mc.WaitForTopologyId(waitTimeout); err != nil {
|
||
mc.DumpLogs()
|
||
mc.StopAll()
|
||
t.Fatalf("TopologyId not generated: %v", err)
|
||
}
|
||
|
||
return mc
|
||
}
|
||
|
||
// NewMasterCluster allocates ports and data directories for a 3-node master
|
||
// cluster without starting anything, so a test can choose what each node comes
|
||
// up with.
|
||
func NewMasterCluster(t testing.TB, raftHashicorp bool) *MasterCluster {
|
||
t.Helper()
|
||
|
||
weedBinary, err := findOrBuildWeedBinary()
|
||
if err != nil {
|
||
t.Fatalf("resolve weed binary: %v", err)
|
||
}
|
||
|
||
keepLogs := os.Getenv("MULTI_MASTER_IT_KEEP_LOGS") == "1"
|
||
baseDir, err := os.MkdirTemp("", "seaweedfs_multi_master_it_")
|
||
if err != nil {
|
||
t.Fatalf("create temp dir: %v", err)
|
||
}
|
||
logsDir := filepath.Join(baseDir, "logs")
|
||
os.MkdirAll(logsDir, 0o755)
|
||
|
||
// Allocate 3 mini-safe ports (each guarantees port+10000 is also free).
|
||
httpPorts, err := testutil.AllocateMiniPorts(3)
|
||
if err != nil {
|
||
t.Fatalf("allocate ports: %v", err)
|
||
}
|
||
var nodes [3]*masterNode
|
||
var peerParts []string
|
||
for i, hp := range httpPorts {
|
||
dataDir := filepath.Join(baseDir, fmt.Sprintf("m%d", i))
|
||
os.MkdirAll(dataDir, 0o755)
|
||
nodes[i] = &masterNode{
|
||
port: hp,
|
||
grpcPort: hp + testutil.GrpcPortOffset,
|
||
dataDir: dataDir,
|
||
logFile: filepath.Join(logsDir, fmt.Sprintf("master%d.log", i)),
|
||
}
|
||
peerParts = append(peerParts, fmt.Sprintf("127.0.0.1:%d", hp))
|
||
}
|
||
|
||
mc := &MasterCluster{
|
||
t: t,
|
||
weedBinary: weedBinary,
|
||
baseDir: baseDir,
|
||
logsDir: logsDir,
|
||
keepLogs: keepLogs,
|
||
nodes: nodes,
|
||
peersStr: strings.Join(peerParts, ","),
|
||
raftHashicorp: raftHashicorp,
|
||
}
|
||
|
||
t.Cleanup(func() {
|
||
mc.StopAll()
|
||
})
|
||
return mc
|
||
}
|
||
|
||
// SetNodePeers narrows the peer list node i starts with, mirroring a
|
||
// StatefulSet whose replica count changed under a running master.
|
||
func (mc *MasterCluster) SetNodePeers(i int, peers string) {
|
||
mc.mu.Lock()
|
||
defer mc.mu.Unlock()
|
||
mc.nodes[i].peersStr = peers
|
||
}
|
||
|
||
// StartNode starts the master process at the given index (0–2).
|
||
func (mc *MasterCluster) StartNode(i int) {
|
||
mc.t.Helper()
|
||
mc.mu.Lock()
|
||
defer mc.mu.Unlock()
|
||
|
||
n := mc.nodes[i]
|
||
if n.cmd != nil && !n.stopped {
|
||
return // already running
|
||
}
|
||
|
||
logFile, err := os.OpenFile(n.logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||
if err != nil {
|
||
mc.t.Fatalf("create log for node %d: %v", i, err)
|
||
}
|
||
|
||
peersStr := n.peersStr
|
||
if peersStr == "" {
|
||
peersStr = mc.peersStr
|
||
}
|
||
args := []string{
|
||
"master",
|
||
"-ip=127.0.0.1",
|
||
"-port=" + strconv.Itoa(n.port),
|
||
"-port.grpc=" + strconv.Itoa(n.grpcPort),
|
||
"-mdir=" + n.dataDir,
|
||
"-peers=" + peersStr,
|
||
"-electionTimeout=3s",
|
||
"-volumeSizeLimitMB=32",
|
||
"-defaultReplication=000",
|
||
}
|
||
if mc.raftHashicorp {
|
||
args = append(args, "-raftHashicorp")
|
||
}
|
||
|
||
n.cmd = exec.Command(mc.weedBinary, args...)
|
||
n.cmd.Dir = mc.baseDir
|
||
n.cmd.Stdout = logFile
|
||
n.cmd.Stderr = logFile
|
||
n.stopped = false
|
||
if err := n.cmd.Start(); err != nil {
|
||
mc.t.Fatalf("start node %d: %v", i, err)
|
||
}
|
||
}
|
||
|
||
// StopNode gracefully stops the master at the given index.
|
||
func (mc *MasterCluster) StopNode(i int) {
|
||
mc.mu.Lock()
|
||
defer mc.mu.Unlock()
|
||
mc.stopNodeLocked(i)
|
||
}
|
||
|
||
func (mc *MasterCluster) stopNodeLocked(i int) {
|
||
n := mc.nodes[i]
|
||
if n.cmd == nil || n.stopped {
|
||
return
|
||
}
|
||
n.stopped = true
|
||
_ = n.cmd.Process.Signal(os.Interrupt)
|
||
done := make(chan error, 1)
|
||
go func() { done <- n.cmd.Wait() }()
|
||
select {
|
||
case <-time.After(10 * time.Second):
|
||
_ = n.cmd.Process.Kill()
|
||
<-done
|
||
case <-done:
|
||
}
|
||
}
|
||
|
||
// StopAll stops all running master nodes.
|
||
func (mc *MasterCluster) StopAll() {
|
||
mc.mu.Lock()
|
||
defer mc.mu.Unlock()
|
||
for i := range 3 {
|
||
mc.stopNodeLocked(i)
|
||
}
|
||
if !mc.keepLogs && !mc.t.Failed() {
|
||
os.RemoveAll(mc.baseDir)
|
||
} else if mc.baseDir != "" {
|
||
mc.t.Logf("multi-master logs kept at %s", mc.baseDir)
|
||
}
|
||
}
|
||
|
||
// NodeURL returns the HTTP URL for node i.
|
||
func (mc *MasterCluster) NodeURL(i int) string {
|
||
return fmt.Sprintf("http://127.0.0.1:%d", mc.nodes[i].port)
|
||
}
|
||
|
||
// NodeAddress returns "127.0.0.1:port" for node i.
|
||
func (mc *MasterCluster) NodeAddress(i int) string {
|
||
return fmt.Sprintf("127.0.0.1:%d", mc.nodes[i].port)
|
||
}
|
||
|
||
// NodeGRPCAddress returns "127.0.0.1:grpcPort" for node i.
|
||
func (mc *MasterCluster) NodeGRPCAddress(i int) string {
|
||
return fmt.Sprintf("127.0.0.1:%d", mc.nodes[i].grpcPort)
|
||
}
|
||
|
||
// IsNodeRunning returns true if the node at index i has a live process.
|
||
func (mc *MasterCluster) IsNodeRunning(i int) bool {
|
||
mc.mu.Lock()
|
||
defer mc.mu.Unlock()
|
||
n := mc.nodes[i]
|
||
return n.cmd != nil && !n.stopped
|
||
}
|
||
|
||
// GetClusterStatus fetches /cluster/status from node i.
|
||
func (mc *MasterCluster) GetClusterStatus(i int) (*clusterStatus, error) {
|
||
client := &http.Client{Timeout: 2 * time.Second}
|
||
resp, err := client.Get(mc.NodeURL(i) + "/cluster/status")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(resp.Body)
|
||
var cs clusterStatus
|
||
if err := json.Unmarshal(body, &cs); err != nil {
|
||
return nil, fmt.Errorf("parse cluster/status: %w (body: %s)", err, string(body))
|
||
}
|
||
return &cs, nil
|
||
}
|
||
|
||
// GetTopologyId fetches the TopologyId from /dir/status on node i.
|
||
func (mc *MasterCluster) GetTopologyId(i int) (string, error) {
|
||
client := &http.Client{Timeout: 2 * time.Second}
|
||
resp, err := client.Get(mc.NodeURL(i) + "/dir/status")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(resp.Body)
|
||
var raw map[string]any
|
||
if err := json.Unmarshal(body, &raw); err != nil {
|
||
return "", err
|
||
}
|
||
if id, ok := raw["TopologyId"].(string); ok {
|
||
return id, nil
|
||
}
|
||
return "", nil
|
||
}
|
||
|
||
// FindLeader returns the index of the leader node and its address.
|
||
// Returns -1 if no leader is found.
|
||
func (mc *MasterCluster) FindLeader() (int, string) {
|
||
for i := range 3 {
|
||
if !mc.IsNodeRunning(i) {
|
||
continue
|
||
}
|
||
cs, err := mc.GetClusterStatus(i)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if cs.IsLeader {
|
||
return i, mc.NodeAddress(i)
|
||
}
|
||
}
|
||
return -1, ""
|
||
}
|
||
|
||
// WaitForLeader polls until a leader is elected or timeout.
|
||
func (mc *MasterCluster) WaitForLeader(timeout time.Duration) error {
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
if idx, _ := mc.FindLeader(); idx >= 0 {
|
||
return nil
|
||
}
|
||
time.Sleep(waitTick)
|
||
}
|
||
return fmt.Errorf("no leader elected within %v", timeout)
|
||
}
|
||
|
||
// WaitForNoLeader waits until no running master claims leadership. goraft only
|
||
// checks whether it still has a quorum on an election-timeout ticker, and needs
|
||
// its peers to go quiet for a full timeout first, so a master that has lost its
|
||
// quorum can keep claiming leadership for tens of seconds. It cannot commit
|
||
// anything in that window.
|
||
func (mc *MasterCluster) WaitForNoLeader(timeout time.Duration) error {
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
idx, _ := mc.FindLeader()
|
||
if idx < 0 {
|
||
return nil
|
||
}
|
||
time.Sleep(waitTick)
|
||
}
|
||
idx, addr := mc.FindLeader()
|
||
return fmt.Errorf("master %d at %s still claims leadership after %v", idx, addr, timeout)
|
||
}
|
||
|
||
// WaitForNewLeader waits for a leader that is different from the given address.
|
||
func (mc *MasterCluster) WaitForNewLeader(oldLeaderAddr string, timeout time.Duration) (int, string, error) {
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
idx, addr := mc.FindLeader()
|
||
if idx >= 0 && addr != oldLeaderAddr {
|
||
return idx, addr, nil
|
||
}
|
||
time.Sleep(waitTick)
|
||
}
|
||
return -1, "", fmt.Errorf("no new leader (different from %s) within %v", oldLeaderAddr, timeout)
|
||
}
|
||
|
||
// WaitForTopologyId waits until the leader reports a non-empty TopologyId, and
|
||
// returns it. It is only readable once the leader has applied the raft entry
|
||
// carrying it, which lands after the election it won.
|
||
func (mc *MasterCluster) WaitForTopologyId(timeout time.Duration) (string, error) {
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
if idx, _ := mc.FindLeader(); idx >= 0 {
|
||
if id, err := mc.GetTopologyId(idx); err == nil && id != "" {
|
||
return id, nil
|
||
}
|
||
}
|
||
time.Sleep(waitTick)
|
||
}
|
||
return "", fmt.Errorf("TopologyId not available within %v", timeout)
|
||
}
|
||
|
||
// WaitForNodeReady waits for node i to respond to HTTP.
|
||
func (mc *MasterCluster) WaitForNodeReady(i int, timeout time.Duration) error {
|
||
client := &http.Client{Timeout: 1 * time.Second}
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
resp, err := client.Get(mc.NodeURL(i) + "/cluster/status")
|
||
if err == nil {
|
||
resp.Body.Close()
|
||
return nil
|
||
}
|
||
time.Sleep(waitTick)
|
||
}
|
||
return fmt.Errorf("node %d not ready within %v", i, timeout)
|
||
}
|
||
|
||
// DumpLogs prints the tail of all master logs.
|
||
func (mc *MasterCluster) DumpLogs() {
|
||
for i := range 3 {
|
||
mc.t.Logf("=== master%d log tail ===\n%s", i, mc.tailLog(i))
|
||
}
|
||
}
|
||
|
||
func (mc *MasterCluster) tailLog(i int) string {
|
||
f, err := os.Open(mc.nodes[i].logFile)
|
||
if err != nil {
|
||
return "(no log)"
|
||
}
|
||
defer f.Close()
|
||
scanner := bufio.NewScanner(f)
|
||
lines := make([]string, 0, 50)
|
||
for scanner.Scan() {
|
||
lines = append(lines, scanner.Text())
|
||
if len(lines) > 50 {
|
||
lines = lines[1:]
|
||
}
|
||
}
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
func findOrBuildWeedBinary() (string, error) {
|
||
if fromEnv := os.Getenv("WEED_BINARY"); fromEnv != "" {
|
||
if isExecutableFile(fromEnv) {
|
||
return fromEnv, nil
|
||
}
|
||
return "", fmt.Errorf("WEED_BINARY not executable: %s", fromEnv)
|
||
}
|
||
|
||
repoRoot := ""
|
||
if _, file, _, ok := runtime.Caller(0); ok {
|
||
repoRoot = filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
|
||
}
|
||
if repoRoot == "" {
|
||
return "", fmt.Errorf("unable to detect repository root")
|
||
}
|
||
|
||
// Check if already built
|
||
binDir := filepath.Join(os.TempDir(), "seaweedfs_multi_master_it_bin")
|
||
os.MkdirAll(binDir, 0o755)
|
||
binPath := filepath.Join(binDir, "weed")
|
||
if isExecutableFile(binPath) {
|
||
return binPath, nil
|
||
}
|
||
|
||
cmd := exec.Command("go", "build", "-o", binPath, ".")
|
||
cmd.Dir = filepath.Join(repoRoot, "weed")
|
||
var out bytes.Buffer
|
||
cmd.Stdout = &out
|
||
cmd.Stderr = &out
|
||
if err := cmd.Run(); err != nil {
|
||
return "", fmt.Errorf("build weed binary: %w\n%s", err, out.String())
|
||
}
|
||
return binPath, nil
|
||
}
|
||
|
||
func isExecutableFile(path string) bool {
|
||
info, err := os.Stat(path)
|
||
if err != nil || info.IsDir() {
|
||
return false
|
||
}
|
||
return info.Mode().Perm()&0o111 != 0
|
||
}
|