mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-11 09:00:45 +02:00
EC placement scored destinations purely by free EC shard slots (derived from maxVolumeCount) and shard counts, blind to real disk fullness — the same defect as volume balancing. A disk that is physically full but still shows free EC slots kept being chosen, and EC shard bytes are captured by statfs free space yet not by any slot accounting, so the slot math is exactly the metric that can't see EC fullness. Treat a disk at/above 90% physical usage as having zero free EC slots at snapshot-build time, so every existing freeSlots>0 placement predicate excludes it. Applied in all three snapshot builders (shell countFreeShardSlots, the shared ecbalancer FromActiveTopology, and the worker ec_balance buildBalancerTopology) via the shared balancer.DiskTooFullAfter gate. Servers not reporting disk bytes fall back to slot-only behavior. ec.rebuild recovery is left ungated so shard recovery can still complete onto fuller disks.
133 lines
5.3 KiB
Go
133 lines
5.3 KiB
Go
package ecbalancer
|
|
|
|
import (
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/topology"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/topology/balancer"
|
|
)
|
|
|
|
// diskTooFull reports whether a disk's physical filesystem is at/above the
|
|
// balancing high-water mark, making it ineligible as an EC-shard target even
|
|
// when slot math says it has room. Statfs free bytes already include EC shard
|
|
// files. No-opinion when the disk reports no bytes (slot-only fallback).
|
|
func diskTooFull(d *topology.DiskInfo) bool {
|
|
return balancer.DiskTooFullAfter(d.DiskInfo.DiskTotalBytes, d.DiskInfo.DiskFreeBytes, 0, balancer.DefaultMaxDiskUsagePercent)
|
|
}
|
|
|
|
// FromActiveTopology builds a Topology snapshot from the cluster's ActiveTopology
|
|
// using the reservation-aware effective-capacity view that EC encode and repair
|
|
// rely on. It collects ALL EC-eligible disks with no hard disk-type filter;
|
|
// disk-type preference is applied later by callers (Place). Per-disk free EC shard
|
|
// slots come from GetEffectiveAvailableEcShardSlots (shard-granular, so in-flight
|
|
// task reservations that are not whole-volume multiples are not lost) minus the EC
|
|
// shards already persisted on the disk. Rack keys are composite "dc:rack".
|
|
//
|
|
// dataShards is the target collection's data-shard count, used to size free EC
|
|
// shard slots correctly for custom ratios (a 4+2 volume's shards are larger, so
|
|
// fewer fit per volume slot). Pass <= 0 for the default scheme. Because of this,
|
|
// the snapshot is ratio-specific; build one per collection ratio being placed.
|
|
//
|
|
// This is the encode/repair-side constructor; balance keeps its own raw-topology
|
|
// builder (buildBalancerTopology) until the snapshot sources are reconciled.
|
|
func FromActiveTopology(at *topology.ActiveTopology, dataShards int) *Topology {
|
|
topo := NewTopology()
|
|
if at == nil {
|
|
return topo
|
|
}
|
|
|
|
disks := at.GetDisksWithEffectiveCapacity(topology.TaskTypeErasureCoding, "", 0)
|
|
|
|
// Accumulate node-level free slots and group the node's disks together.
|
|
nodeFree := make(map[string]int)
|
|
nodeDC := make(map[string]string)
|
|
nodeRack := make(map[string]string)
|
|
nodeAddr := make(map[string]string)
|
|
byNode := make(map[string][]*topology.DiskInfo)
|
|
for _, d := range disks {
|
|
if d == nil || d.DiskInfo == nil {
|
|
continue
|
|
}
|
|
if free := perDiskFreeECSlots(at, d, dataShards); free > 0 && !diskTooFull(d) {
|
|
nodeFree[d.NodeID] += free
|
|
}
|
|
nodeDC[d.NodeID] = d.DataCenter
|
|
nodeRack[d.NodeID] = d.DataCenter + ":" + d.Rack
|
|
nodeAddr[d.NodeID] = d.Address
|
|
byNode[d.NodeID] = append(byNode[d.NodeID], d)
|
|
}
|
|
|
|
for nodeID, ds := range byNode {
|
|
node := topo.AddNode(nodeID, nodeDC[nodeID], nodeRack[nodeID], nodeFree[nodeID])
|
|
// Group by physical machine: derive the host from the address (NodeID may be
|
|
// an opaque id), falling back to the id when no address is recorded.
|
|
addr := nodeAddr[nodeID]
|
|
if addr == "" {
|
|
addr = nodeID
|
|
}
|
|
node.SetHost(pb.ServerAddress(addr).ToHost())
|
|
for _, d := range ds {
|
|
free := perDiskFreeECSlots(at, d, dataShards)
|
|
if free < 0 || diskTooFull(d) {
|
|
free = 0
|
|
}
|
|
node.AddDisk(d.DiskID, d.DiskType, free, ecShardCountOnDisk(d))
|
|
node.AddDiskTags(d.DiskID, d.DiskInfo.Tags)
|
|
for _, eci := range d.DiskInfo.EcShardInfos {
|
|
if eci.DiskId != d.DiskID {
|
|
continue
|
|
}
|
|
node.AddShards(eci.Id, eci.Collection, d.DiskID, erasure_coding.ShardBits(eci.EcIndexBits))
|
|
}
|
|
}
|
|
}
|
|
|
|
return topo
|
|
}
|
|
|
|
// perDiskFreeECSlots returns the disk's free EC shard slots for a volume with
|
|
// dataShards data shards: the topology's shard-granular effective availability
|
|
// (sized by the target ratio) minus the EC shards already on the disk, also
|
|
// expressed in the target ratio's shard slots so mixed-ratio disks are charged by
|
|
// size rather than raw count.
|
|
func perDiskFreeECSlots(at *topology.ActiveTopology, d *topology.DiskInfo, dataShards int) int {
|
|
return at.GetEffectiveAvailableEcShardSlots(d.NodeID, d.DiskID, dataShards) - ecShardSlotsOnDisk(d, dataShards)
|
|
}
|
|
|
|
// ecShardCountOnDisk counts the EC shards physically on this disk (matching
|
|
// eci.DiskId), across all volumes. Used as a per-disk load metric for scoring.
|
|
func ecShardCountOnDisk(d *topology.DiskInfo) int {
|
|
count := 0
|
|
for _, eci := range d.DiskInfo.EcShardInfos {
|
|
if eci.DiskId == d.DiskID {
|
|
count += erasure_coding.GetShardCount(eci)
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// ecShardSlotsOnDisk returns the EC shards already on the disk expressed in the
|
|
// TARGET collection's shard slots. A shard of a collection with d data shards
|
|
// occupies ~1/d of a volume, i.e. targetDataShards/d target slots, so a 2+1 shard
|
|
// counted against a 10+4 snapshot consumes ~5 slots, not 1.
|
|
func ecShardSlotsOnDisk(d *topology.DiskInfo, targetDataShards int) int {
|
|
if targetDataShards <= 0 {
|
|
targetDataShards = erasure_coding.DataShardsCount
|
|
}
|
|
total := 0
|
|
for _, eci := range d.DiskInfo.EcShardInfos {
|
|
if eci.DiskId != d.DiskID {
|
|
continue
|
|
}
|
|
ds := shardDataShards(eci)
|
|
if ds <= 0 {
|
|
ds = erasure_coding.DataShardsCount
|
|
}
|
|
// Round up so an existing shard always consumes at least its fractional
|
|
// footprint; flooring lets a low-data-shard volume (targetDataShards < ds)
|
|
// count as zero target slots and overstate the disk's free capacity.
|
|
total += (erasure_coding.GetShardCount(eci)*targetDataShards + ds - 1) / ds
|
|
}
|
|
return total
|
|
}
|