mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02: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.
807 lines
26 KiB
Go
807 lines
26 KiB
Go
package topology
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/rand/v2"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
|
|
backoff "github.com/cenkalti/backoff/v4"
|
|
|
|
hashicorpRaft "github.com/hashicorp/raft"
|
|
"github.com/seaweedfs/raft"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/sequence"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const (
|
|
// WarmupPulseMultiplier is the number of heartbeat intervals to wait after
|
|
// a leader change before treating volume lookup misses as definitive.
|
|
WarmupPulseMultiplier = 3
|
|
)
|
|
|
|
type Topology struct {
|
|
vacuumLockCounter int64
|
|
NodeImpl
|
|
|
|
collectionMap *util.ConcurrentReadMap
|
|
ecShardMap map[needle.VolumeId]*EcShardLocations
|
|
ecShardMapLock sync.RWMutex
|
|
|
|
pulse int64
|
|
|
|
volumeSizeLimit uint64
|
|
replicationAsMin bool
|
|
vacuumDisabledByOperator atomic.Bool // true when operator manually disables vacuum
|
|
vacuumDisabledByPlugin atomic.Bool // true when disabled by the vacuum plugin monitor
|
|
adminServerConnectedFunc func() bool // optional callback to check admin server presence
|
|
|
|
Sequence sequence.Sequencer
|
|
|
|
chanFullVolumes chan storage.VolumeInfo
|
|
chanCrowdedVolumes chan storage.VolumeInfo
|
|
|
|
Configuration *Configuration
|
|
|
|
RaftServer raft.Server
|
|
RaftServerAccessLock sync.RWMutex
|
|
HashicorpRaft *hashicorpRaft.Raft
|
|
barrierLock sync.Mutex
|
|
barrierDone bool
|
|
|
|
UuidAccessLock sync.RWMutex
|
|
UuidMap map[string][]string
|
|
|
|
topologyId string
|
|
topologyIdLock sync.RWMutex
|
|
|
|
lastLeaderChangeTime time.Time
|
|
hadVolumesAtLeaderChange bool
|
|
lastLeaderChangeTimeLock sync.RWMutex
|
|
|
|
// dataNodeIndex is an address -> *DataNode lookup so callers (e.g. the
|
|
// Ping admission gate) do not have to walk every dc/rack/node tier on
|
|
// every request. Keys use the canonical http form returned by
|
|
// pb.ServerAddress.ToHttpAddress so a target like "1.2.3.4:8080" finds
|
|
// the same node whether or not the grpc port suffix is present.
|
|
dataNodeIndex map[string]*DataNode
|
|
dataNodeIndexLock sync.RWMutex
|
|
}
|
|
|
|
func NewTopology(id string, seq sequence.Sequencer, volumeSizeLimit uint64, pulse int, replicationAsMin bool) *Topology {
|
|
t := &Topology{}
|
|
t.id = NodeId(id)
|
|
t.nodeType = "Topology"
|
|
t.NodeImpl.value = t
|
|
t.diskUsages = newDiskUsages()
|
|
t.children = make(map[NodeId]Node)
|
|
t.capacityReservations = newCapacityReservations()
|
|
t.collectionMap = util.NewConcurrentReadMap()
|
|
t.ecShardMap = make(map[needle.VolumeId]*EcShardLocations)
|
|
t.pulse = int64(pulse)
|
|
t.volumeSizeLimit = volumeSizeLimit
|
|
t.replicationAsMin = replicationAsMin
|
|
|
|
t.Sequence = seq
|
|
|
|
t.chanFullVolumes = make(chan storage.VolumeInfo)
|
|
t.chanCrowdedVolumes = make(chan storage.VolumeInfo)
|
|
|
|
t.Configuration = &Configuration{}
|
|
t.dataNodeIndex = make(map[string]*DataNode)
|
|
|
|
return t
|
|
}
|
|
|
|
// LookupDataNodeByAddress returns the registered DataNode that serves addr,
|
|
// or nil if no such node has been observed. Lookup is O(1) and uses the
|
|
// canonical http form of the address so callers that pass either
|
|
// "host:port" or "host:port.grpc" find the same node.
|
|
func (t *Topology) LookupDataNodeByAddress(addr pb.ServerAddress) *DataNode {
|
|
if addr == "" {
|
|
return nil
|
|
}
|
|
t.dataNodeIndexLock.RLock()
|
|
defer t.dataNodeIndexLock.RUnlock()
|
|
if t.dataNodeIndex == nil {
|
|
return nil
|
|
}
|
|
return t.dataNodeIndex[addr.ToHttpAddress()]
|
|
}
|
|
|
|
// registerDataNodeAddress records dn in the address index under its current
|
|
// http address. Callers must invoke unregisterDataNodeAddress with the prior
|
|
// address whenever a node's Ip or Port changes (e.g. k8s pod reschedule).
|
|
func (t *Topology) registerDataNodeAddress(dn *DataNode) {
|
|
if dn == nil {
|
|
return
|
|
}
|
|
key := dn.ServerAddress().ToHttpAddress()
|
|
if key == "" {
|
|
return
|
|
}
|
|
t.dataNodeIndexLock.Lock()
|
|
defer t.dataNodeIndexLock.Unlock()
|
|
if t.dataNodeIndex == nil {
|
|
t.dataNodeIndex = make(map[string]*DataNode)
|
|
}
|
|
t.dataNodeIndex[key] = dn
|
|
}
|
|
|
|
// unregisterDataNodeAddress removes the index entry for addr, but only when
|
|
// the entry still points at dn. The conditional guard avoids dropping a
|
|
// freshly re-registered node whose address happens to alias the one being
|
|
// removed (e.g. legacy id transitions or a fast restart).
|
|
func (t *Topology) unregisterDataNodeAddress(addr pb.ServerAddress, dn *DataNode) {
|
|
if addr == "" {
|
|
return
|
|
}
|
|
key := addr.ToHttpAddress()
|
|
if key == "" {
|
|
return
|
|
}
|
|
t.dataNodeIndexLock.Lock()
|
|
defer t.dataNodeIndexLock.Unlock()
|
|
if existing, ok := t.dataNodeIndex[key]; ok && (dn == nil || existing == dn) {
|
|
delete(t.dataNodeIndex, key)
|
|
}
|
|
}
|
|
|
|
func (t *Topology) IsChildLocked() (bool, error) {
|
|
if t.IsLocked() {
|
|
return true, errors.New("topology is locked")
|
|
}
|
|
for _, dcNode := range t.Children() {
|
|
if dcNode.IsLocked() {
|
|
return true, fmt.Errorf("topology child %s is locked", dcNode.String())
|
|
}
|
|
for _, rackNode := range dcNode.Children() {
|
|
if rackNode.IsLocked() {
|
|
return true, fmt.Errorf("dc %s child %s is locked", dcNode.String(), rackNode.String())
|
|
}
|
|
for _, dataNode := range rackNode.Children() {
|
|
if dataNode.IsLocked() {
|
|
return true, fmt.Errorf("rack %s child %s is locked", rackNode.String(), dataNode.Id())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// SetLastLeaderChangeTime records the time of the most recent leader transition.
|
|
// It also snapshots whether the topology already had known volumes at that
|
|
// moment. IsWarmingUp uses the snapshot instead of the live MaxVolumeId so a
|
|
// fresh cluster that happens to grow its first volume inside the warmup window
|
|
// does not retroactively flip into "warming up" state — there is no prior
|
|
// topology to wait for on a bootstrap.
|
|
func (t *Topology) SetLastLeaderChangeTime(ts time.Time) {
|
|
hadVolumes := t.GetMaxVolumeId() > 0
|
|
t.lastLeaderChangeTimeLock.Lock()
|
|
defer t.lastLeaderChangeTimeLock.Unlock()
|
|
t.lastLeaderChangeTime = ts
|
|
t.hadVolumesAtLeaderChange = hadVolumes
|
|
}
|
|
|
|
// GetLastLeaderChangeTime returns the time of the most recent leader transition.
|
|
func (t *Topology) GetLastLeaderChangeTime() time.Time {
|
|
t.lastLeaderChangeTimeLock.RLock()
|
|
defer t.lastLeaderChangeTimeLock.RUnlock()
|
|
return t.lastLeaderChangeTime
|
|
}
|
|
|
|
// IsWarmingUp returns true if the master recently became leader and may not yet
|
|
// have a complete topology. After a leader change or restart, volume servers need
|
|
// up to WarmupPulseMultiplier heartbeat intervals to reconnect and report their volumes.
|
|
// Returns false on a fresh cluster start — i.e. when no volumes existed at the
|
|
// time of the leader change — since there is no prior topology state to wait for.
|
|
// Checking the *live* MaxVolumeId here would make a bootstrapping cluster flip
|
|
// into warming-up the moment its first volume is grown, which manifested as a
|
|
// 15-second window of spurious Unavailable errors on AssignVolume for workloads
|
|
// that start writing immediately (see #8777).
|
|
func (t *Topology) IsWarmingUp() bool {
|
|
t.lastLeaderChangeTimeLock.RLock()
|
|
lastChange := t.lastLeaderChangeTime
|
|
hadVolumes := t.hadVolumesAtLeaderChange
|
|
t.lastLeaderChangeTimeLock.RUnlock()
|
|
if !hadVolumes || lastChange.IsZero() {
|
|
return false
|
|
}
|
|
return time.Since(lastChange) < t.WarmupDuration()
|
|
}
|
|
|
|
// WarmupDuration returns the configured warmup duration based on pulse interval.
|
|
func (t *Topology) WarmupDuration() time.Duration {
|
|
return time.Duration(t.pulse*WarmupPulseMultiplier) * time.Second
|
|
}
|
|
|
|
// RemainingWarmupDuration returns how much warmup time is left, or 0 if not warming up.
|
|
func (t *Topology) RemainingWarmupDuration() time.Duration {
|
|
if !t.IsWarmingUp() {
|
|
return 0
|
|
}
|
|
remaining := t.WarmupDuration() - time.Since(t.GetLastLeaderChangeTime())
|
|
if remaining < 0 {
|
|
return 0
|
|
}
|
|
return remaining
|
|
}
|
|
|
|
func (t *Topology) IsLeader() bool {
|
|
t.RaftServerAccessLock.RLock()
|
|
defer t.RaftServerAccessLock.RUnlock()
|
|
|
|
if t.RaftServer != nil {
|
|
if t.RaftServer.State() == raft.Leader {
|
|
return true
|
|
}
|
|
// Directly check leader to avoid re-acquiring lock via MaybeLeader()
|
|
leader := pb.ServerAddress(t.RaftServer.Leader())
|
|
if leader != "" {
|
|
if pb.ServerAddress(t.RaftServer.Name()).Equals(leader) {
|
|
return true
|
|
}
|
|
}
|
|
} else if t.HashicorpRaft != nil {
|
|
if t.HashicorpRaft.State() == hashicorpRaft.Leader {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *Topology) IsLeaderAndCanRead() bool {
|
|
if t.RaftServer != nil {
|
|
return t.IsLeader()
|
|
} else if t.HashicorpRaft != nil {
|
|
return t.IsLeader() && t.DoBarrier()
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (t *Topology) DoBarrier() bool {
|
|
t.barrierLock.Lock()
|
|
defer t.barrierLock.Unlock()
|
|
if t.barrierDone {
|
|
return true
|
|
}
|
|
|
|
glog.V(0).Infof("raft do barrier")
|
|
barrier := t.HashicorpRaft.Barrier(2 * time.Minute)
|
|
if err := barrier.Error(); err != nil {
|
|
glog.Errorf("failed to wait for barrier, error %s", err)
|
|
return false
|
|
|
|
}
|
|
|
|
t.barrierDone = true
|
|
glog.V(0).Infof("raft do barrier success")
|
|
return true
|
|
}
|
|
|
|
func (t *Topology) BarrierReset() {
|
|
t.barrierLock.Lock()
|
|
defer t.barrierLock.Unlock()
|
|
t.barrierDone = false
|
|
}
|
|
|
|
func (t *Topology) Leader() (l pb.ServerAddress, err error) {
|
|
exponentialBackoff := backoff.NewExponentialBackOff()
|
|
exponentialBackoff.InitialInterval = 100 * time.Millisecond
|
|
exponentialBackoff.MaxElapsedTime = 20 * time.Second
|
|
leaderNotSelected := errors.New("leader not selected yet")
|
|
l, err = backoff.RetryWithData(
|
|
func() (l pb.ServerAddress, err error) {
|
|
l, err = t.MaybeLeader()
|
|
if err == nil && l == "" {
|
|
err = leaderNotSelected
|
|
}
|
|
return l, err
|
|
},
|
|
exponentialBackoff)
|
|
if err == leaderNotSelected {
|
|
l = ""
|
|
}
|
|
return l, err
|
|
}
|
|
|
|
func (t *Topology) MaybeLeader() (l pb.ServerAddress, err error) {
|
|
t.RaftServerAccessLock.RLock()
|
|
defer t.RaftServerAccessLock.RUnlock()
|
|
|
|
if t.RaftServer != nil {
|
|
l = pb.ServerAddress(t.RaftServer.Leader())
|
|
if l == "" && t.RaftServer.State() == raft.Leader {
|
|
l = pb.ServerAddress(t.RaftServer.Name())
|
|
}
|
|
} else if t.HashicorpRaft != nil {
|
|
l = pb.ServerAddress(t.HashicorpRaft.Leader())
|
|
} else {
|
|
err = errors.New("Raft Server not ready yet!")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (t *Topology) Lookup(collection string, vid needle.VolumeId) (dataNodes []*DataNode) {
|
|
// maybe an issue if lots of collections?
|
|
if collection == "" {
|
|
for _, c := range t.collectionMap.Items() {
|
|
if list := c.(*Collection).Lookup(vid); list != nil {
|
|
return list
|
|
}
|
|
}
|
|
} else {
|
|
if c, ok := t.collectionMap.Find(collection); ok {
|
|
return c.(*Collection).Lookup(vid)
|
|
}
|
|
}
|
|
|
|
if locations, found := t.LookupEcShards(vid); found {
|
|
for _, loc := range locations.Locations {
|
|
dataNodes = append(dataNodes, loc...)
|
|
}
|
|
return dataNodes
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Topology) NextVolumeId() (needle.VolumeId, error) {
|
|
if !t.IsLeaderAndCanRead() {
|
|
return 0, fmt.Errorf("as leader can not read yet")
|
|
|
|
}
|
|
vid := t.GetMaxVolumeId()
|
|
next := vid.Next()
|
|
|
|
t.RaftServerAccessLock.RLock()
|
|
defer t.RaftServerAccessLock.RUnlock()
|
|
|
|
if t.RaftServer != nil {
|
|
if _, err := t.RaftServer.Do(NewMaxVolumeIdCommand(next, t.GetTopologyId())); err != nil {
|
|
return 0, err
|
|
}
|
|
} else if t.HashicorpRaft != nil {
|
|
b, err := json.Marshal(NewMaxVolumeIdCommand(next, t.GetTopologyId()))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed marshal NewMaxVolumeIdCommand: %+v", err)
|
|
}
|
|
if future := t.HashicorpRaft.Apply(b, time.Second); future.Error() != nil {
|
|
return 0, future.Error()
|
|
}
|
|
}
|
|
return next, nil
|
|
}
|
|
|
|
// DefaultNeedleSizeEstimate is the fallback per-file-ID size estimate when
|
|
// the client does not provide an expected data size.
|
|
const DefaultNeedleSizeEstimate uint64 = 1024 * 1024 // 1 MB
|
|
|
|
func (t *Topology) PickForWrite(requestedCount uint64, option *VolumeGrowOption, volumeLayout *VolumeLayout, expectedDataSize uint64) (fileId string, count uint64, volumeLocationList *VolumeLocationList, shouldGrow bool, err error) {
|
|
var vid needle.VolumeId
|
|
vid, count, volumeLocationList, shouldGrow, err = volumeLayout.PickForWrite(requestedCount, option)
|
|
if err != nil {
|
|
return "", 0, nil, shouldGrow, fmt.Errorf("failed to find writable volumes for collection:%s replication:%s ttl:%s error: %v", option.Collection, option.ReplicaPlacement.String(), option.Ttl.String(), err)
|
|
}
|
|
if volumeLocationList == nil || volumeLocationList.Length() == 0 {
|
|
return "", 0, nil, shouldGrow, fmt.Errorf("%s available for collection:%s replication:%s ttl:%s", NoWritableVolumes, option.Collection, option.ReplicaPlacement.String(), option.Ttl.String())
|
|
}
|
|
// Track estimated assigned bytes to spread load between heartbeats.
|
|
// Use the client hint if provided, otherwise fall back to 1MB estimate.
|
|
sizePerFile := DefaultNeedleSizeEstimate
|
|
if expectedDataSize > 0 {
|
|
sizePerFile = expectedDataSize
|
|
}
|
|
pendingBytes := min(uint64(count)*sizePerFile, uint64(math.MaxInt64))
|
|
if volumeLayout.RecordAssign(vid, int64(pendingBytes)) {
|
|
volumeLayout.AdjustActiveVolumeCountForFull(vid)
|
|
}
|
|
nextFileId := t.Sequence.NextFileId(requestedCount)
|
|
fileId = needle.NewFileId(vid, nextFileId, rand.Uint32()).String()
|
|
return fileId, count, volumeLocationList, shouldGrow, nil
|
|
}
|
|
|
|
func (t *Topology) GetVolumeLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) *VolumeLayout {
|
|
return t.collectionMap.Get(collectionName, func() interface{} {
|
|
return NewCollection(collectionName, t.volumeSizeLimit, t.replicationAsMin)
|
|
}).(*Collection).GetOrCreateVolumeLayout(rp, ttl, diskType)
|
|
}
|
|
|
|
// CollectionVolumeStats aggregates stats across all volume layouts and EC
|
|
// volumes of one collection, or across every collection when collectionName is
|
|
// empty.
|
|
func (t *Topology) CollectionVolumeStats(collectionName string) *VolumeLayoutStats {
|
|
ret := &VolumeLayoutStats{}
|
|
var collections []*Collection
|
|
if collectionName == "" {
|
|
for _, c := range t.collectionMap.Items() {
|
|
collections = append(collections, c.(*Collection))
|
|
}
|
|
} else if c, found := t.FindCollection(collectionName); found {
|
|
collections = append(collections, c)
|
|
}
|
|
for _, c := range collections {
|
|
for _, vl := range c.GetAllVolumeLayouts() {
|
|
stats := vl.Stats()
|
|
ret.TotalSize += stats.TotalSize
|
|
ret.UsedSize += stats.UsedSize
|
|
ret.LogicalUsedSize += stats.LogicalUsedSize
|
|
ret.FileCount += stats.FileCount
|
|
}
|
|
}
|
|
// EC volumes live outside collectionMap, so a collection whose volumes are
|
|
// all encoded has no layout left to report them
|
|
ecStats := t.CollectionEcVolumeStats(collectionName)
|
|
ret.TotalSize += ecStats.TotalSize
|
|
ret.UsedSize += ecStats.UsedSize
|
|
ret.LogicalUsedSize += ecStats.LogicalUsedSize
|
|
ret.FileCount += ecStats.FileCount
|
|
return ret
|
|
}
|
|
|
|
func (t *Topology) ListCollections(includeNormalVolumes, includeEcVolumes bool) (ret []string) {
|
|
found := make(map[string]bool)
|
|
|
|
if includeNormalVolumes {
|
|
t.collectionMap.RLock()
|
|
for _, c := range t.collectionMap.Items() {
|
|
found[c.(*Collection).Name] = true
|
|
}
|
|
t.collectionMap.RUnlock()
|
|
}
|
|
|
|
if includeEcVolumes {
|
|
t.ecShardMapLock.RLock()
|
|
for _, ecVolumeLocation := range t.ecShardMap {
|
|
found[ecVolumeLocation.Collection] = true
|
|
}
|
|
t.ecShardMapLock.RUnlock()
|
|
}
|
|
|
|
for k := range found {
|
|
ret = append(ret, k)
|
|
}
|
|
slices.Sort(ret)
|
|
|
|
return ret
|
|
}
|
|
|
|
func (t *Topology) FindCollection(collectionName string) (*Collection, bool) {
|
|
c, hasCollection := t.collectionMap.Find(collectionName)
|
|
if !hasCollection {
|
|
return nil, false
|
|
}
|
|
return c.(*Collection), hasCollection
|
|
}
|
|
|
|
func (t *Topology) DeleteCollection(collectionName string) {
|
|
// The layouts vanish with the collection, but every location they served
|
|
// holds a bit in its node's lookup digest. Left in place, those bits keep
|
|
// the node's held and servable digests apart forever, and the master asks
|
|
// for the full volume list on every heartbeat from then on.
|
|
// Unpublish first so a racing registration re-resolves into a fresh collection.
|
|
collection, found := t.FindCollection(collectionName)
|
|
t.collectionMap.Delete(collectionName)
|
|
if !found {
|
|
return
|
|
}
|
|
for _, vl := range collection.GetAllVolumeLayouts() {
|
|
vl.releaseLookupOwnership()
|
|
}
|
|
}
|
|
|
|
func (t *Topology) DeleteLayout(collectionName string, rp *super_block.ReplicaPlacement, ttl *needle.TTL, diskType types.DiskType) {
|
|
collection, found := t.FindCollection(collectionName)
|
|
if !found {
|
|
return
|
|
}
|
|
collection.DeleteVolumeLayout(rp, ttl, diskType)
|
|
if len(collection.storageType2VolumeLayout.Items()) == 0 {
|
|
t.DeleteCollection(collectionName)
|
|
}
|
|
}
|
|
|
|
func (t *Topology) RegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
|
|
diskType := types.ToDiskType(v.DiskType)
|
|
for {
|
|
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
|
|
if vl.RegisterVolume(&v, dn) {
|
|
vl.EnsureCorrectWritables(&v)
|
|
return
|
|
}
|
|
// Dropped with its collection; the next lookup creates a fresh one.
|
|
}
|
|
}
|
|
|
|
func (t *Topology) UnRegisterVolumeLayout(v storage.VolumeInfo, dn *DataNode) {
|
|
glog.Infof("removing volume info: %+v from %v", v, dn.id)
|
|
if v.ReplicaPlacement.GetCopyCount() > 1 {
|
|
stats.MasterReplicaPlacementMismatch.WithLabelValues(v.Collection, v.Id.String()).Set(0)
|
|
}
|
|
diskType := types.ToDiskType(v.DiskType)
|
|
volumeLayout := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
|
|
volumeLayout.UnRegisterVolume(&v, dn)
|
|
if volumeLayout.isEmpty() {
|
|
t.DeleteLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
|
|
}
|
|
}
|
|
|
|
func (t *Topology) DataCenterExists(dcName string) bool {
|
|
return dcName == "" || t.GetDataCenter(dcName) != nil
|
|
}
|
|
|
|
func (t *Topology) GetDataCenter(dcName string) (dc *DataCenter) {
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
for _, c := range t.children {
|
|
dc = c.(*DataCenter)
|
|
if string(dc.Id()) == dcName {
|
|
return dc
|
|
}
|
|
}
|
|
return dc
|
|
}
|
|
|
|
func (t *Topology) GetOrCreateDataCenter(dcName string) *DataCenter {
|
|
t.Lock()
|
|
defer t.Unlock()
|
|
for _, c := range t.children {
|
|
dc := c.(*DataCenter)
|
|
if string(dc.Id()) == dcName {
|
|
return dc
|
|
}
|
|
}
|
|
dc := NewDataCenter(dcName)
|
|
t.doLinkChildNode(dc)
|
|
return dc
|
|
}
|
|
|
|
func (t *Topology) ListDataCenters() (dcs []string) {
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
for _, c := range t.children {
|
|
dcs = append(dcs, string(c.(*DataCenter).Id()))
|
|
}
|
|
return dcs
|
|
}
|
|
|
|
func (t *Topology) ListDCAndRacks() (dcs map[NodeId][]NodeId) {
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
dcs = make(map[NodeId][]NodeId)
|
|
for _, dcNode := range t.children {
|
|
dcNodeId := dcNode.(*DataCenter).Id()
|
|
for _, rackNode := range dcNode.Children() {
|
|
dcs[dcNodeId] = append(dcs[dcNodeId], rackNode.(*Rack).Id())
|
|
}
|
|
}
|
|
return dcs
|
|
}
|
|
|
|
func (t *Topology) SyncDataNodeRegistration(volumes []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes, deletedVolumes []storage.VolumeInfo) {
|
|
// convert into in memory struct storage.VolumeInfo
|
|
volumeInfos := make([]storage.VolumeInfo, 0, len(volumes))
|
|
for _, v := range volumes {
|
|
if vi, err := storage.NewVolumeInfo(v); err == nil {
|
|
volumeInfos = append(volumeInfos, vi)
|
|
} else {
|
|
glog.V(0).Infof("Fail to convert joined volume information: %v", err)
|
|
}
|
|
}
|
|
// find out the delta volumes
|
|
newVolumes, deletedVolumes, _ = dn.UpdateVolumes(volumeInfos)
|
|
for _, v := range newVolumes {
|
|
t.RegisterVolumeLayout(v, dn)
|
|
}
|
|
for _, v := range deletedVolumes {
|
|
t.UnRegisterVolumeLayout(v, dn)
|
|
}
|
|
// Update effective sizes for all reported volumes (decay pending estimates).
|
|
// If decay brings a volume eagerly removed by RecordAssign back under the
|
|
// writable threshold, restore the matching activeVolumeCount.
|
|
for _, v := range volumeInfos {
|
|
if v.ReplicaPlacement == nil {
|
|
continue
|
|
}
|
|
diskType := types.ToDiskType(v.DiskType)
|
|
vl := t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
|
|
// Self-heal: a volume reported by the data node but missing from the
|
|
// lookup index is re-registered. This repairs the split left by a
|
|
// disconnect/reconnect race, where UnRegisterDataNode dropped the volume
|
|
// from vid2location but the reconnecting full heartbeat skipped it
|
|
// (still in the disk map, so UpdateVolumes did not report it as new).
|
|
// Without this, the volume stays visible in volume.list/admin UI yet
|
|
// LookupVolume returns "volume id not found".
|
|
if !vl.HasDataNode(v.Id, dn) {
|
|
for !vl.RegisterVolume(&v, dn) {
|
|
// Dropped with its collection; the next lookup creates a fresh
|
|
// one, which the calls below must use too.
|
|
vl = t.GetVolumeLayout(v.Collection, v.ReplicaPlacement, v.Ttl, diskType)
|
|
}
|
|
// Volumes new to the disk map were registered above, so reaching
|
|
// here means only the lookup index had lost it. Clients were told
|
|
// it went when the node dropped out, so the repair has to tell them
|
|
// it is back.
|
|
newVolumes = append(newVolumes, v)
|
|
}
|
|
vl.UpdateOversizedState(&v, dn)
|
|
if vl.UpdateVolumeSize(v.Id, v.Size, v.CompactRevision) {
|
|
vl.AdjustActiveVolumeCountAfterRecovery(v.Id)
|
|
}
|
|
vl.EnsureCorrectWritables(&v)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *Topology) IncrementalSyncDataNodeRegistration(newVolumes, deletedVolumes []*master_pb.VolumeShortInformationMessage, dn *DataNode) {
|
|
var newVis, oldVis []storage.VolumeInfo
|
|
for _, v := range newVolumes {
|
|
vi, err := storage.NewVolumeInfoFromShort(v)
|
|
if err != nil {
|
|
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
|
|
continue
|
|
}
|
|
newVis = append(newVis, vi)
|
|
}
|
|
for _, v := range deletedVolumes {
|
|
vi, err := storage.NewVolumeInfoFromShort(v)
|
|
if err != nil {
|
|
glog.V(0).Infof("NewVolumeInfoFromShort %v: %v", v, err)
|
|
continue
|
|
}
|
|
oldVis = append(oldVis, vi)
|
|
}
|
|
dn.DeltaUpdateVolumes(newVis, oldVis)
|
|
|
|
for _, vi := range newVis {
|
|
t.RegisterVolumeLayout(vi, dn)
|
|
}
|
|
for _, vi := range oldVis {
|
|
t.UnRegisterVolumeLayout(vi, dn)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ApplyVolumeChanges records the volumes a heartbeat reported as changed and
|
|
// returns the ones the node did not already have. Only the named volumes are
|
|
// touched: unlike a full report, silence about a volume says nothing about
|
|
// whether the server still has it.
|
|
//
|
|
// Most changes are a volume growing, which moves no location, so returning
|
|
// only the arrivals keeps a busy cluster from telling every client about
|
|
// volumes they can already reach.
|
|
func (t *Topology) ApplyVolumeChanges(changed []*master_pb.VolumeInformationMessage, dn *DataNode) (newVolumes []storage.VolumeInfo) {
|
|
volumeInfos := make([]storage.VolumeInfo, 0, len(changed))
|
|
for _, v := range changed {
|
|
vi, err := storage.NewVolumeInfo(v)
|
|
if err != nil {
|
|
glog.V(0).Infof("Fail to convert changed volume information: %v", err)
|
|
continue
|
|
}
|
|
volumeInfos = append(volumeInfos, vi)
|
|
}
|
|
|
|
for _, vi := range volumeInfos {
|
|
isNew, _ := dn.AddOrUpdateVolume(vi)
|
|
if vi.ReplicaPlacement == nil {
|
|
if isNew {
|
|
newVolumes = append(newVolumes, vi)
|
|
}
|
|
continue
|
|
}
|
|
vl := t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
|
|
// Reaching the lookup index is what makes a volume servable, so a
|
|
// volume only that index had lost is an arrival as far as clients are
|
|
// concerned: they were told it went when the node dropped out.
|
|
becameServable := !vl.HasDataNode(vi.Id, dn)
|
|
for becameServable && !vl.RegisterVolume(&vi, dn) {
|
|
// Dropped with its collection; the next lookup creates a fresh one.
|
|
vl = t.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, vi.Ttl, types.ToDiskType(vi.DiskType))
|
|
}
|
|
if isNew || becameServable {
|
|
newVolumes = append(newVolumes, vi)
|
|
}
|
|
vl.UpdateOversizedState(&vi, dn)
|
|
if vl.UpdateVolumeSize(vi.Id, vi.Size, vi.CompactRevision) {
|
|
vl.AdjustActiveVolumeCountAfterRecovery(vi.Id)
|
|
}
|
|
vl.EnsureCorrectWritables(&vi)
|
|
}
|
|
return newVolumes
|
|
}
|
|
|
|
func (t *Topology) DataNodeRegistration(dcName, rackName string, dn *DataNode) {
|
|
if dn.Parent() != nil {
|
|
return
|
|
}
|
|
// registration to topo
|
|
dc := t.GetOrCreateDataCenter(dcName)
|
|
rack := dc.GetOrCreateRack(rackName)
|
|
rack.LinkChildNode(dn)
|
|
glog.Infof("[%s] reLink To topo ", dn.Id())
|
|
}
|
|
|
|
// IsVacuumDisabled returns true if vacuum is disabled by either the
|
|
// operator or the plugin monitor.
|
|
func (t *Topology) IsVacuumDisabled() bool {
|
|
return t.vacuumDisabledByOperator.Load() || t.vacuumDisabledByPlugin.Load()
|
|
}
|
|
|
|
// DisableVacuum is called by the operator (shell command / manual RPC).
|
|
// Only sets the operator flag; does not affect the plugin flag.
|
|
func (t *Topology) DisableVacuum() {
|
|
glog.V(0).Infof("DisableVacuum (by operator)")
|
|
t.vacuumDisabledByOperator.Store(true)
|
|
}
|
|
|
|
// EnableVacuum is called by the operator (shell command / manual RPC).
|
|
// Only clears the operator flag; does not affect the plugin flag.
|
|
func (t *Topology) EnableVacuum() {
|
|
glog.V(0).Infof("EnableVacuum (by operator)")
|
|
t.vacuumDisabledByOperator.Store(false)
|
|
}
|
|
|
|
// DisableVacuumByPlugin is called by the admin server's vacuum monitor
|
|
// when a vacuum plugin worker connects. Only sets the plugin flag.
|
|
func (t *Topology) DisableVacuumByPlugin() {
|
|
glog.V(0).Infof("DisableVacuum (by plugin worker)")
|
|
t.vacuumDisabledByPlugin.Store(true)
|
|
}
|
|
|
|
// EnableVacuumByPlugin is called by the admin server's vacuum monitor
|
|
// when a vacuum plugin worker disconnects. Only clears the plugin flag.
|
|
func (t *Topology) EnableVacuumByPlugin() {
|
|
glog.V(0).Infof("EnableVacuum (by plugin worker)")
|
|
t.vacuumDisabledByPlugin.Store(false)
|
|
}
|
|
|
|
// IsVacuumDisabledByPlugin returns whether the plugin monitor has disabled vacuum.
|
|
func (t *Topology) IsVacuumDisabledByPlugin() bool {
|
|
return t.vacuumDisabledByPlugin.Load()
|
|
}
|
|
|
|
// SetAdminServerConnectedFunc sets an optional callback used by the vacuum
|
|
// safety net to detect when the admin server has disconnected.
|
|
func (t *Topology) SetAdminServerConnectedFunc(f func() bool) {
|
|
t.adminServerConnectedFunc = f
|
|
}
|
|
|
|
func (t *Topology) GetTopologyId() string {
|
|
t.topologyIdLock.RLock()
|
|
defer t.topologyIdLock.RUnlock()
|
|
return t.topologyId
|
|
}
|
|
|
|
func (t *Topology) SetTopologyId(topologyId string) {
|
|
t.topologyIdLock.Lock()
|
|
defer t.topologyIdLock.Unlock()
|
|
if topologyId == "" {
|
|
return
|
|
}
|
|
if t.topologyId == "" {
|
|
t.topologyId = topologyId
|
|
return
|
|
}
|
|
if t.topologyId != topologyId {
|
|
glog.Fatalf("Split-brain detected! Current TopologyId is %s, but received %s. Stopping to prevent data corruption.", t.topologyId, topologyId)
|
|
}
|
|
}
|