mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 23:14:21 +00:00
* Add shared super_block.ResolveReplicaPlacement; use it in ec_balance * Add ecbalancer.FromActiveTopology snapshot constructor for EC encode/repair * Add ecbalancer.Place greenfield/repair placement core (strict + durability-first) * topology: add GetEffectiveAvailableEcShardSlots; FromActiveTopology uses shard-granular free slots GetDisksWithEffectiveCapacity flattens reserved shard slots into volume slots via integer truncation, so an in-flight EC task reserving a non-multiple-of- DataShardsCount number of shards was lost from the snapshot and freeSlots was over-reported. GetEffectiveAvailableEcShardSlots subtracts the full reservation impact at shard granularity. * ecbalancer.Place: reject nodes without a free disk of the requested type FromActiveTopology keeps all disk types in the snapshot, so an SSD-only request could be routed to a node with only HDD capacity (pickBestDiskOnNode then returns disk 0 on the wrong tier). Filter rack/node selection to those with a free disk of the requested type. * ecbalancer.Place: enforce ReplicaPlacement DiffDataCenterCount (per-DC shard cap) * ecbalancer: enforce DiffDataCenterCount in balance (cross-DC phase + cross-rack DC cap) Adds a cross-DC corrective phase that drains data centers holding more than DiffDataCenterCount shards of a volume, and a per-DC cap on cross-rack move targets. Both are no-ops when DiffDataCenterCount is unset, so balance output is unchanged for non-DC placements. * topology: ratio-aware EC shard slots and provisional empty-disk slot GetEffectiveAvailableEcShardSlots now takes the target collection's data-shard count, so a 4+2 volume's larger shards are not over-counted at 10 per volume slot; and it keeps the one provisional slot for freshly started empty servers that report max=0, matching getEffectiveAvailableCapacityUnsafe. FromActiveTopology threads the ratio through. * ecbalancer.Place: explicit disk-type filter signal (fix HDD vs any ambiguity) HardDriveType normalizes to "", which collided with "" meaning any disk. Add Constraints.FilterDiskType and normalize both sides so a hdd request matches disks reported as "" and never leaks to SSD, while filter=false still means any. * ecbalancer: add clearShardAccounting for repair snapshot reconciliation Clears one disk's copy of a shard from per-domain accounting and recomputes the node-level union (preserving a kept copy on another disk of the same node), without crediting capacity. Repair uses it to drop to-be-deleted copies before placing missing shards. * ecbalancer: don't cap cross-DC target racks when DiffRackCount is unset len(racks)+1 wrongly limited each target rack (3 in a 2-rack cluster), so draining a DC could stop short of the DiffDataCenterCount cap. Use MaxShardCount+1 as the effectively-unlimited default. * topology/ecbalancer: ratio-correct EC capacity accounting Reservation shard slots (default ShardsPerVolumeSlot units) are now converted to the target ratio before subtracting, and existing EC shards are charged by size (targetDataShards/shardDataShards) so a 2+1 shard isn't counted as one 10+4 slot. Per-shard ratio lookup is behind shardDataShards (OSS uses the standard ratio). * ecbalancer.Place: candidate tiering and eligible-rack caps Adds a per-disk eligibility/preference abstraction so Place supports: - preferred-tag whole-plan retry (try disks carrying the earliest tags first, widen to all only if a tier cannot place every shard; reports SpilledOutsidePreferredTags), - soft disk-type spill via DiskTypePolicy (Any/Prefer/Require): Prefer fills the preferred type then spills, reporting SpilledToOtherDiskType; Require filters, - even per-rack caps that divide by racks holding an eligible disk, so a tiered cluster (e.g. SSDs in 2 of 4 racks) isn't capped impossibly low. Disk tags carried via Node.AddDiskTags + FromActiveTopology. * ecbalancer: export ClearShardAccounting for repair snapshot reconciliation * ecbalancer: address review feedback (ratio rounding, bitmap walk, same-DC moves) - topology/ecbalancer: round shard-reservation and existing-shard footprint up when converting to target-ratio shard slots, so a sub-slot reservation is not truncated to zero and free capacity is not overstated for low-data-shard layouts (targetDataShards < ds). - erasure_coding: add ShardBits.All iterator and use it across the balancer, cross-DC phase, and placement scoring instead of scanning 0..MaxShardCount and probing Has on every id. - ecbalancer: allow same-DC cross-rack moves when a DC already sits at its DiffDataCenterCount cap; a same-DC move leaves the DC total unchanged. Add a regression test that fails without the guard. - ecbalancer cross-DC phase: pick targets via the eligible-aware pickNodeInRackEligible/pickBestDiskEligible helpers so the disk-type filter is honored and a 0 disk id is not mistaken for a valid selection. * ecbalancer: test ecShardSlotsOnDisk fractional round-up Cover the mixed-ratio path (targetDataShards < existing data shards) so a shard's fractional footprint is never floored to zero and free capacity is not overstated. Exercises the round-up via the targetDataShards parameter; OSS uses the standard ratio at runtime while the enterprise build hits it with real per-volume ratios. * ecbalancer: assert node B rack in TestFromActiveTopology * ecbalancer: split Destination into separate DataCenter and bare Rack Replace the composite "dc:rack" Rack field on Destination with separate DataCenter and bare Rack values, matching topology.DiskInfo and the worker-task convention. Callers (and tests) read the data center directly instead of parsing the composite with strings.SplitN. * shell ec.balance: use utilization-based global balancing (parity with worker) The shell's global rebalance phase balanced by raw shard count; switch it to fractional fullness (shards/capacity), as the worker already does. On uniform capacity the two agree; on heterogeneous capacity it fills nodes proportionally instead of driving small-capacity nodes toward full. Updates the heterogeneous-capacity regression test to assert even fullness (~equal shards/capacity per node) rather than even shard count. * ecbalancer: bounded-proportional per-DC shard spread DiffDataCenterCount was enforced only as a ceiling (drain-to-cap), which could leave a within-cap-but-lopsided DC distribution under a loose cap (e.g. 10/4 of 14 with cap=10). Now the cross-DC phase, the cross-rack DC guard, and Place all target boundedMaxPerDC = min(DiffDataCenterCount, max(ceil(total/numDCs), parityShards)): shards spread proportionally across DCs, but no tighter than the durability floor (once each DC holds <= parityShards a DC loss is recoverable, so further spreading only adds cross-DC/WAN traffic). No-op when DiffDataCenterCount is 0; identical to before when the cap is the binding constraint. * ecbalancer: drop DiffDataCenterCount enforcement for EC placement The 1-byte volume ReplicaPlacement packs xyz into x*100+y*10+z<=255, so the DC digit can only be 0-2 -- far too small to be a meaningful per-DC EC shard cap (a cap of 1-2 would demand 7-14 DCs for a 10+4 volume). It's volume replica-placement, not an EC spec. Removes the cross-DC balance phase, the DC guard in the cross-rack phase, and the per-DC cap in Place (and the just-added bounded-proportional logic); EC relies on the RP-independent rack/node even spread instead. Rack/node caps (DiffRackCount/SameRackCount) are unchanged. Per-domain EC caps are left for a real EC placement spec. * ecbalancer: enforce per-disk durability cap; symmetric reserve/release Place now refuses to put more than parityShards shards of a volume on a single disk (pickBestDiskEligible skips a disk once it holds parityShards of the volume, a hard cap not relaxed even in durability-first). Previously Place assigned by free capacity, so a skewed near-full cluster could pile >parityShards onto one disk -> losing it loses the volume; only distinct-disk count was checked. This covers encode and repair (both route through Place); the caller skips/leaves the volume rather than minting an unrecoverable layout. Also makes reserveShard decrement freeSlots unconditionally, symmetric with releaseShard's unconditional increment (the old guarded decrement could credit a phantom slot on release if a shard were ever reserved onto a full disk). * ecbalancer: add Topology.ReleaseVolumeShards (clear + credit) for greenfield encode Releases all of a volume's shards from the snapshot and credits the freed disk capacity, so a greenfield encode can plan as if stale EC shards from a prior failed attempt are gone. Safe to credit because the encode task deletes stale shards (cleanupStaleEcShards) before distributing the new ones. Distinct from ClearShardAccounting (repair), which does not credit. * ecbalancer: ReleaseVolumeShards credits node freeSlots, not just disks releaseShard only increments per-disk freeSlots, but rack capacity is summed from node freeSlots (buildRacks) and node freeSlots gates node eligibility. Crediting only disks left a node/rack looking full after releasing stale shards, so a greenfield encode still couldn't use the freed capacity. Now credits the node by the total disk-slots freed. * ecbalancer: correct PlacementMode docs (encode uses durability-first) PlaceStrict was labeled '(encode)' but encode uses PlaceDurabilityFirst. Clarify that durability-first is used by both encode and repair, reports relaxations in PlaceResult.Relaxed, and never relaxes the per-disk durability cap. * ecbalancer: treat SameRackCount as a direct per-node shard cap The 3rd ReplicaPlacement digit now caps shards per node at exactly the digit value, matching how DiffRackCount (2nd digit) caps per rack, instead of allowing digit+1 per node. This makes the per-rack and per-node caps consistent and matches the documented "digits cap EC shards per rack and per node" semantics; e.g. 011 now means at most one shard per rack and one per node.
280 lines
9.3 KiB
Go
280 lines
9.3 KiB
Go
package ec_balance
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding/ecbalancer"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
storagetypes "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
|
)
|
|
|
|
// Detection builds an EC balance topology snapshot from the cluster's active
|
|
// topology, runs the shared ecbalancer planner, and converts the planned moves
|
|
// into worker task proposals. The balancing policy lives in
|
|
// weed/storage/erasure_coding/ecbalancer, shared with the shell ec.balance
|
|
// command so the two cannot drift.
|
|
func Detection(
|
|
ctx context.Context,
|
|
metrics []*types.VolumeHealthMetrics,
|
|
clusterInfo *types.ClusterInfo,
|
|
config base.TaskConfig,
|
|
maxResults int,
|
|
) ([]*types.TaskDetectionResult, bool, error) {
|
|
if !config.IsEnabled() {
|
|
return nil, false, nil
|
|
}
|
|
|
|
ecConfig := config.(*Config)
|
|
if maxResults < 0 {
|
|
maxResults = 0
|
|
}
|
|
|
|
if clusterInfo == nil || clusterInfo.ActiveTopology == nil {
|
|
return nil, false, fmt.Errorf("active topology not available for EC balance detection")
|
|
}
|
|
topoInfo := clusterInfo.ActiveTopology.GetTopologyInfo()
|
|
if topoInfo == nil {
|
|
return nil, false, fmt.Errorf("topology info not available")
|
|
}
|
|
|
|
topo, nodeCount := buildBalancerTopology(topoInfo, ecConfig)
|
|
if nodeCount < ecConfig.MinServerCount {
|
|
glog.V(1).Infof("EC balance: only %d servers, need at least %d", nodeCount, ecConfig.MinServerCount)
|
|
return nil, false, nil
|
|
}
|
|
|
|
replicaPlacement := resolveReplicaPlacement(ecConfig, clusterInfo)
|
|
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
// Canonical disk type for placement/execution: "hdd" -> "" (HardDriveType),
|
|
// matching the topology's disk keys and the volume server's move RPCs.
|
|
normalizedDiskType := storagetypes.ToDiskType(ecConfig.DiskType).String()
|
|
|
|
moves := ecbalancer.Plan(topo, ecbalancer.Options{
|
|
DiskType: normalizedDiskType,
|
|
ImbalanceThreshold: ecConfig.ImbalanceThreshold,
|
|
ReplicaPlacement: replicaPlacement,
|
|
Ratio: func(collection string) (int, int) {
|
|
return resolveECRatio(clusterInfo, collection)
|
|
},
|
|
// Move incrementally across detection cycles rather than draining a rack
|
|
// in one batch; the scheduler re-evaluates each cycle.
|
|
GlobalMaxMovesPerRack: 10,
|
|
// Balance heterogeneous-capacity racks by fractional fullness.
|
|
GlobalUtilizationBased: true,
|
|
})
|
|
if len(moves) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
hasMore := false
|
|
if maxResults > 0 && len(moves) > maxResults {
|
|
moves = moves[:maxResults]
|
|
hasMore = true
|
|
}
|
|
|
|
now := time.Now()
|
|
results := make([]*types.TaskDetectionResult, 0, len(moves))
|
|
for i, m := range moves {
|
|
taskID := fmt.Sprintf("ec_balance_%d_%d_%s_%s_%d_%d",
|
|
m.VolumeID, m.ShardID, m.SourceNode, m.TargetNode, now.UnixNano(), i)
|
|
results = append(results, &types.TaskDetectionResult{
|
|
TaskID: taskID,
|
|
TaskType: types.TaskTypeECBalance,
|
|
VolumeID: m.VolumeID,
|
|
Server: m.SourceNode,
|
|
Collection: m.Collection,
|
|
Priority: movePhasePriority(m.Phase),
|
|
Reason: fmt.Sprintf("EC shard %d.%d %s: %s → %s",
|
|
m.VolumeID, m.ShardID, m.Phase, m.SourceNode, m.TargetNode),
|
|
ScheduleAt: now,
|
|
TypedParams: &worker_pb.TaskParams{
|
|
TaskId: taskID,
|
|
VolumeId: m.VolumeID,
|
|
Collection: m.Collection,
|
|
Sources: []*worker_pb.TaskSource{{
|
|
Node: m.SourceNode,
|
|
DiskId: m.SourceDisk,
|
|
Rack: m.SourceRack,
|
|
ShardIds: []uint32{uint32(m.ShardID)},
|
|
}},
|
|
Targets: []*worker_pb.TaskTarget{{
|
|
Node: m.TargetNode,
|
|
DiskId: m.TargetDisk,
|
|
Rack: m.TargetRack,
|
|
ShardIds: []uint32{uint32(m.ShardID)},
|
|
}},
|
|
TaskParams: &worker_pb.TaskParams_EcBalanceParams{
|
|
EcBalanceParams: &worker_pb.EcBalanceTaskParams{
|
|
DiskType: normalizedDiskType,
|
|
TimeoutSeconds: 600,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
glog.V(1).Infof("EC balance detection: %d moves proposed", len(results))
|
|
return results, hasMore, nil
|
|
}
|
|
|
|
// buildBalancerTopology builds an ecbalancer.Topology from the master topology,
|
|
// applying the data-center, disk-type, and collection filters. Rack keys are
|
|
// dc:rack composites to avoid cross-DC name collisions. Per-disk free capacity
|
|
// is split evenly from the node total because the wire collapses same-type disks.
|
|
// Returns the topology and the number of eligible nodes (for MinServerCount).
|
|
func buildBalancerTopology(topoInfo *master_pb.TopologyInfo, config *Config) (*ecbalancer.Topology, int) {
|
|
topo := ecbalancer.NewTopology()
|
|
allowedCollections := wildcard.CompileWildcardMatchers(config.CollectionFilter)
|
|
|
|
// Normalize the disk-type filter: "hdd" (and the default "") map to the
|
|
// HardDriveType, which the topology reports under the empty-string key. Keep a
|
|
// separate "filter requested" flag so a configured "hdd" still filters to HDD
|
|
// disks instead of being mistaken for "all disk types".
|
|
filterByDiskType := config.DiskType != ""
|
|
wantDiskType := storagetypes.ToDiskType(config.DiskType).String()
|
|
|
|
nodeCount := 0
|
|
for _, dc := range topoInfo.DataCenterInfos {
|
|
if config.DataCenterFilter != "" {
|
|
matchers := wildcard.CompileWildcardMatchers(config.DataCenterFilter)
|
|
if !wildcard.MatchesAnyWildcard(matchers, dc.Id) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
for _, rack := range dc.RackInfos {
|
|
rackKey := dc.Id + ":" + rack.Id
|
|
|
|
for _, dn := range rack.DataNodeInfos {
|
|
freeSlots := 0
|
|
diskTypeOf := make(map[uint32]string) // physical disk_id -> disk type
|
|
diskShardCount := make(map[uint32]int)
|
|
hasMatchingDisk := false
|
|
|
|
for diskType, diskInfo := range dn.DiskInfos {
|
|
if filterByDiskType && diskType != wantDiskType {
|
|
continue
|
|
}
|
|
hasMatchingDisk = true
|
|
|
|
fs := int(diskInfo.MaxVolumeCount-diskInfo.VolumeCount)*erasure_coding.DataShardsCount - countEcShards(diskInfo.EcShardInfos)
|
|
if fs > 0 {
|
|
freeSlots += fs
|
|
}
|
|
// Discover physical disks from regular volumes too, so an
|
|
// EC-empty disk is still a candidate destination.
|
|
for _, vi := range diskInfo.VolumeInfos {
|
|
if _, ok := diskTypeOf[vi.DiskId]; !ok {
|
|
diskTypeOf[vi.DiskId] = diskType
|
|
}
|
|
}
|
|
for _, eci := range diskInfo.EcShardInfos {
|
|
if _, ok := diskTypeOf[eci.DiskId]; !ok {
|
|
diskTypeOf[eci.DiskId] = diskType
|
|
}
|
|
// Disk occupancy counts ALL volumes' shards (capacity model),
|
|
// independent of the collection filter below.
|
|
diskShardCount[eci.DiskId] += erasure_coding.GetShardCount(eci)
|
|
}
|
|
}
|
|
|
|
if !hasMatchingDisk {
|
|
continue
|
|
}
|
|
|
|
node := topo.AddNode(dn.Id, dc.Id, rackKey, freeSlots)
|
|
|
|
perDiskFree := 0
|
|
if diskCount := len(diskTypeOf); diskCount > 0 && freeSlots > 0 {
|
|
perDiskFree = freeSlots / diskCount
|
|
}
|
|
for diskID, diskType := range diskTypeOf {
|
|
node.AddDisk(diskID, diskType, perDiskFree, diskShardCount[diskID])
|
|
}
|
|
|
|
// Add shards only for volumes whose collection passes the filter;
|
|
// those are the volumes the planner will balance.
|
|
for diskType, diskInfo := range dn.DiskInfos {
|
|
if filterByDiskType && diskType != wantDiskType {
|
|
continue
|
|
}
|
|
for _, eci := range diskInfo.EcShardInfos {
|
|
if len(allowedCollections) > 0 && !wildcard.MatchesAnyWildcard(allowedCollections, eci.Collection) {
|
|
continue
|
|
}
|
|
node.AddShards(eci.Id, eci.Collection, eci.DiskId, erasure_coding.ShardBits(eci.EcIndexBits))
|
|
}
|
|
}
|
|
|
|
nodeCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
return topo, nodeCount
|
|
}
|
|
|
|
// resolveECRatio returns the (dataShards, parityShards) for a collection from the
|
|
// admin EC config snapshot when present, else the local default. This keeps the
|
|
// enterprise-only custom-ratio plumbing out of the shared planner.
|
|
func resolveECRatio(_ *types.ClusterInfo, _ string) (int, int) {
|
|
// Custom EC ratios are an enterprise feature; OSS uses the standard scheme.
|
|
return normalizeECShardCounts(0, 0)
|
|
}
|
|
|
|
// resolveReplicaPlacement picks the EC shard replica placement constraint: an
|
|
// explicit config value wins; otherwise it falls back to the master's default
|
|
// replication (matching the shell ec.balance default). A missing, invalid, or
|
|
// zero-replication value yields nil, meaning even spread / no constraint.
|
|
func resolveReplicaPlacement(ecConfig *Config, clusterInfo *types.ClusterInfo) *super_block.ReplicaPlacement {
|
|
clusterDefault := ""
|
|
if clusterInfo != nil {
|
|
clusterDefault = clusterInfo.DefaultReplicaPlacement
|
|
}
|
|
return super_block.ResolveReplicaPlacement(ecConfig.ReplicaPlacement, clusterDefault)
|
|
}
|
|
|
|
func normalizeECShardCounts(dataShards, parityShards int) (int, int) {
|
|
if dataShards <= 0 {
|
|
dataShards = erasure_coding.DataShardsCount
|
|
}
|
|
if parityShards <= 0 {
|
|
parityShards = erasure_coding.ParityShardsCount
|
|
}
|
|
return dataShards, parityShards
|
|
}
|
|
|
|
func countEcShards(ecShardInfos []*master_pb.VolumeEcShardInformationMessage) int {
|
|
count := 0
|
|
for _, eci := range ecShardInfos {
|
|
count += erasure_coding.GetShardCount(eci)
|
|
}
|
|
return count
|
|
}
|
|
|
|
func movePhasePriority(phase string) types.TaskPriority {
|
|
switch phase {
|
|
case "dedup":
|
|
return types.TaskPriorityHigh
|
|
case "cross_rack":
|
|
return types.TaskPriorityMedium
|
|
default:
|
|
return types.TaskPriorityLow
|
|
}
|
|
}
|