mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 14:30:43 +01:00
* feat: Free Volume Deadband (M) for EC Place nodes and racks |ΔfreeSlots| ≤ M is a tie; remainder is liveShards. M=0 stays strict freeSlots. M is free volume slots (× dataShards). pickNodeInRackEligible and pickRackEligible share the rule. Worker plugin config free_slots_deadband, default 0. Not on the write path. * fix: anchor EC freeSlots deadband on class maxFree Pairwise |Δfree|≤M vs the current winner is not transitive: 120→110→100 at M=10 slots picked 100. After evenness, rank by maxFree, keep |free-maxFree|≤M, then min liveShards. Shared by node and rack. M=0 is still only maxFree then live. * worker: add free_slots_deadband to ErasureCodingTaskConfig * erasure_coding: persist free_slots_deadband through TaskPolicy ApplyMaintenanceConfigFromToml accepted the key but the admin save path round-trips config through ToTaskPolicy, so the value was dropped on reload; serialize it like the other EC settings. * erasure_coding: bound free_slots_deadband at its declared maximum The TOML loader and the plugin worker config only clamped negatives, so a value above the spec's 100000 could reach the int32 TaskPolicy field and wrap; cap at 100000 on both entry paths and declare MaxValue on the plugin field. * erasure_coding: document free_slots_deadband in scaffold admin.toml --------- Co-authored-by: Chepurnov Simon <195125934+CHEPURNOV-S-S@users.noreply.github.com> Co-authored-by: user.email <chrislusf@users.noreply.github.com>
789 lines
28 KiB
Go
789 lines
28 KiB
Go
package ecbalancer
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||
storagetypes "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||
)
|
||
|
||
// Constraints configures a Place call. Ratio resolves a collection's
|
||
// (dataShards, parityShards); nil uses the standard scheme. ReplicaPlacement,
|
||
// when non-nil, caps shards per rack (DiffRackCount = max shards/rack) and per
|
||
// node within a rack (SameRackCount = max shards/node); both digits are direct
|
||
// hard caps. The data-center digit (DiffDataCenterCount) is not honored:
|
||
// the 1-byte volume ReplicaPlacement can only encode 0-2 there, too small to be a
|
||
// meaningful per-DC EC shard cap, so EC relies on the rack/node even spread instead.
|
||
//
|
||
// DiskTypePolicy controls how DiskType constrains placement (Any / Prefer /
|
||
// Require). PreferredTags drives whole-plan tag tiering: Place tries disks
|
||
// carrying the earliest tags first and widens to all disks only if a tier cannot
|
||
// place every shard.
|
||
type Constraints struct {
|
||
DiskType string
|
||
DiskTypePolicy DiskTypePolicy
|
||
PreferredTags []string
|
||
ReplicaPlacement *super_block.ReplicaPlacement
|
||
Ratio func(collection string) (dataShards, parityShards int)
|
||
// FreeSlotsDeadband (M) is free volume slots. After even-this-volume,
|
||
// Place ranks that evenness class by maxFree, keeps |free−maxFree| ≤
|
||
// M×dataShards, then lower liveShards. 0 is only maxFree then live
|
||
// (old strict freeSlots; liveShards only on an exact free tie).
|
||
FreeSlotsDeadband int
|
||
}
|
||
|
||
// DiskTypePolicy controls how Constraints.DiskType constrains placement.
|
||
type DiskTypePolicy int
|
||
|
||
const (
|
||
DiskTypeAny DiskTypePolicy = iota // any disk type
|
||
DiskTypePrefer // prefer DiskType, spill to other types if needed
|
||
DiskTypeRequire // only DiskType (HardDriveType when "")
|
||
)
|
||
|
||
// diskTypeEqual compares disk types after normalization, so "" and "hdd" (both
|
||
// HardDriveType) are equal.
|
||
func diskTypeEqual(a, b string) bool {
|
||
return storagetypes.ToDiskType(a).String() == storagetypes.ToDiskType(b).String()
|
||
}
|
||
|
||
// diskHasAnyTag reports whether the disk carries any of the given tags.
|
||
func diskHasAnyTag(d *disk, tags []string) bool {
|
||
for _, want := range tags {
|
||
for _, have := range d.tags {
|
||
if have == want {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Destination is a chosen target for one shard. DataCenter and Rack are kept as
|
||
// separate values (matching topology.DiskInfo) rather than a "dc:rack" composite,
|
||
// so callers read them directly instead of parsing.
|
||
type Destination struct {
|
||
Node string
|
||
DiskID uint32
|
||
DataCenter string
|
||
Rack string // bare rack id within DataCenter
|
||
}
|
||
|
||
// PlaceResult holds the chosen destinations, which constraints had to be relaxed
|
||
// (durability-first only), and whether placement spilled outside the preferred
|
||
// disk type or tag tiers (for parity with today's logging).
|
||
type PlaceResult struct {
|
||
Destinations map[int]Destination
|
||
Relaxed []string
|
||
SpilledToOtherDiskType bool
|
||
SpilledOutsidePreferredTags bool
|
||
}
|
||
|
||
// PlacementMode selects the strictness/relaxation policy.
|
||
type PlacementMode int
|
||
|
||
const (
|
||
// PlaceStrict: caps, the total-shards-per-rack cap and ReplicaPlacement are
|
||
// hard. Place fails rather than violate them, so the caller can defer (leave
|
||
// the volume as-is and retry).
|
||
PlaceStrict PlacementMode = iota
|
||
// PlaceDurabilityFirst (used by both encode and repair): relax per-type caps ->
|
||
// data/parity anti-affinity -> ReplicaPlacement -> the total-shards-per-rack
|
||
// cap, in that order, until each shard lands, reporting what was relaxed in
|
||
// PlaceResult.Relaxed. The per-disk durability cap (<= parityShards per disk)
|
||
// is never relaxed. Fails only if no eligible disk has room for another shard
|
||
// of the volume. Encode places best-effort this way and rebalancing tightens
|
||
// the spread afterward.
|
||
PlaceDurabilityFirst
|
||
)
|
||
|
||
// relaxation controls which placement-quality constraints are enforced on an
|
||
// attempt. preferring fresh nodes (repair's "avoid surviving-shard nodes") is not
|
||
// listed: pickNodeInRack already selects the node with the fewest shards of the
|
||
// volume, so survivors are deprioritized with built-in fallback.
|
||
type relaxation struct {
|
||
caps bool
|
||
antiAffinity bool
|
||
rp bool
|
||
rackTotal bool
|
||
}
|
||
|
||
func (r relaxation) relaxedNames() []string {
|
||
var n []string
|
||
if !r.caps {
|
||
n = append(n, "caps")
|
||
}
|
||
if !r.antiAffinity {
|
||
n = append(n, "anti-affinity")
|
||
}
|
||
if !r.rp {
|
||
n = append(n, "replica-placement")
|
||
}
|
||
if !r.rackTotal {
|
||
n = append(n, "rack-total-cap")
|
||
}
|
||
return n
|
||
}
|
||
|
||
var strictAttempts = []relaxation{{caps: true, antiAffinity: true, rp: true, rackTotal: true}}
|
||
|
||
// The total-shards-per-rack cap is sized from real capacity (see rackTotalCap),
|
||
// so it is relaxed last and only as a safety net; with it dropped, the per-disk
|
||
// cap is the only constraint left.
|
||
var durabilityAttempts = []relaxation{
|
||
{caps: true, antiAffinity: true, rp: true, rackTotal: true},
|
||
{caps: false, antiAffinity: true, rp: true, rackTotal: true},
|
||
{caps: false, antiAffinity: false, rp: true, rackTotal: true},
|
||
{caps: false, antiAffinity: false, rp: false, rackTotal: true},
|
||
{caps: false, antiAffinity: false, rp: false, rackTotal: false},
|
||
}
|
||
|
||
type placedEntry struct {
|
||
node *Node
|
||
sid int
|
||
rackKey string
|
||
}
|
||
|
||
// Place assigns destinations for the `need` shard ids of volume (collection,vid),
|
||
// reading the volume's already-placed shards from the snapshot (so encode passes
|
||
// an empty-for-this-volume snapshot, repair passes one seeded with the surviving
|
||
// shards).
|
||
//
|
||
// Tag tiering (whole-plan retry): it tries the preferred-tag tiers in order, each
|
||
// a complete candidate set, and returns the first tier that places every shard;
|
||
// only when it falls through to the no-tag tier does it set
|
||
// SpilledOutsidePreferredTags. Within a tier, disk-type Prefer spills to other
|
||
// types per shard (SpilledToOtherDiskType); Require filters strictly.
|
||
func (t *Topology) Place(vid uint32, collection string, need []int, c Constraints, mode PlacementMode) (*PlaceResult, error) {
|
||
if len(need) == 0 {
|
||
return &PlaceResult{Destinations: map[int]Destination{}}, nil
|
||
}
|
||
|
||
vk := volKey{collection: collection, vid: vid}
|
||
dataShards, parityShards := erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount
|
||
if c.Ratio != nil {
|
||
if d, p := c.Ratio(collection); d > 0 && p > 0 {
|
||
dataShards, parityShards = d, p
|
||
}
|
||
}
|
||
|
||
racks := buildRacks(t.nodes)
|
||
if len(racks) == 0 {
|
||
return nil, fmt.Errorf("no racks available for EC placement")
|
||
}
|
||
rackKeys := sortedKeys(racks)
|
||
|
||
// Disk-type eligibility (Require filters; Any/Prefer admit all) and the soft
|
||
// type preference applied in scoring under Prefer.
|
||
typeEligible := func(d *disk) bool {
|
||
if c.DiskTypePolicy == DiskTypeRequire {
|
||
return diskTypeEqual(d.diskType, c.DiskType)
|
||
}
|
||
return true
|
||
}
|
||
var prefer func(*disk) bool
|
||
if c.DiskTypePolicy == DiskTypePrefer {
|
||
prefer = func(d *disk) bool { return diskTypeEqual(d.diskType, c.DiskType) }
|
||
}
|
||
|
||
// Whole-plan retry over preferred-tag tiers; the first tier that places every
|
||
// shard wins. Reaching the no-tag tier means we spilled outside the tags.
|
||
tiers := tagTiers(c.PreferredTags)
|
||
var lastErr error
|
||
for _, tierTags := range tiers {
|
||
tt := tierTags
|
||
eligible := func(d *disk) bool {
|
||
return typeEligible(d) && (len(tt) == 0 || diskHasAnyTag(d, tt))
|
||
}
|
||
res, err := t.tryPlace(vk, need, dataShards, parityShards, racks, rackKeys, mode, c.ReplicaPlacement, eligible, prefer, volumeDeadbandToShardSlots(c.FreeSlotsDeadband, dataShards))
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
if len(c.PreferredTags) > 0 && len(tierTags) == 0 {
|
||
res.SpilledOutsidePreferredTags = true
|
||
}
|
||
return res, nil
|
||
}
|
||
return nil, lastErr
|
||
}
|
||
|
||
// tagTiers returns the eligibility tag-sets in increasing breadth, ending with an
|
||
// empty set ("any disk"). Empty preferredTags yields a single any-disk tier.
|
||
func tagTiers(preferredTags []string) [][]string {
|
||
if len(preferredTags) == 0 {
|
||
return [][]string{nil}
|
||
}
|
||
tiers := make([][]string, 0, len(preferredTags)+1)
|
||
for k := range preferredTags {
|
||
tiers = append(tiers, append([]string(nil), preferredTags[:k+1]...))
|
||
}
|
||
return append(tiers, nil)
|
||
}
|
||
|
||
// tryPlace runs one whole-plan placement attempt restricted to disks satisfying
|
||
// `eligible`, with `prefer` (may be nil) ranking soft-preferred disks first. It
|
||
// journals reservations and rolls them all back if any shard cannot be placed, so
|
||
// a failed tier leaves the snapshot unchanged for the next attempt.
|
||
func (t *Topology) tryPlace(vk volKey, need []int, dataShards, parityShards int, racks map[string]*rack, rackKeys []string, mode PlacementMode, rp *super_block.ReplicaPlacement, eligible func(*disk) bool, prefer func(*disk) bool, freeSlotsDeadband int) (*PlaceResult, error) {
|
||
result := &PlaceResult{Destinations: make(map[int]Destination, len(need))}
|
||
|
||
// Per-type shard ids per rack (even caps), total shard count per rack
|
||
// (DiffRackCount), and the racks bearing each type (anti-affinity) — all seeded
|
||
// from the volume's existing shards.
|
||
shardsPerRack := map[bool]map[string][]int{true: {}, false: {}}
|
||
rackShardCount := map[string]int{}
|
||
bearing := map[bool]map[string]bool{true: {}, false: {}}
|
||
for _, n := range t.nodes {
|
||
info, ok := n.shards[vk]
|
||
if !ok {
|
||
continue
|
||
}
|
||
for sid := range info.shardBits.All() {
|
||
s := int(sid)
|
||
isData := s < dataShards
|
||
shardsPerRack[isData][n.rack] = append(shardsPerRack[isData][n.rack], s)
|
||
rackShardCount[n.rack]++
|
||
bearing[isData][n.rack] = true
|
||
}
|
||
}
|
||
|
||
// Even per-rack caps divide by racks that can actually take another shard of
|
||
// the volume, not all racks (the snapshot keeps every disk type/tag), so a
|
||
// valid tiered cluster — e.g. SSDs in only 2 of 4 racks — is not capped
|
||
// impossibly low.
|
||
rackRoom := make(map[string]int, len(rackKeys))
|
||
rackRoomRP := make(map[string]int, len(rackKeys))
|
||
numEligibleRacks := 0
|
||
for _, rk := range rackKeys {
|
||
rackRoom[rk] = rackShardRoom(racks[rk], vk, eligible, parityShards)
|
||
rackRoomRP[rk] = rackShardRoomUnderRP(racks[rk], vk, eligible, parityShards, rp)
|
||
if rackRoom[rk] > 0 {
|
||
numEligibleRacks++
|
||
}
|
||
}
|
||
if numEligibleRacks < 1 {
|
||
numEligibleRacks = 1
|
||
}
|
||
|
||
// The per-type caps spread data and parity independently, so together they
|
||
// can still stack e.g. 2 data + 1 parity on one rack. Cap the TOTAL per rack
|
||
// as well, at the lowest value the racks' free capacity allows. Attempts that
|
||
// enforce ReplicaPlacement size it from the room left under SameRackCount, so
|
||
// a rack of few nodes does not count disk room its nodes may not use.
|
||
totalShards := len(need)
|
||
for _, n := range rackShardCount {
|
||
totalShards += n
|
||
}
|
||
maxTotalPerRack := rackTotalCap(rackKeys, rackShardCount, rackRoom, totalShards)
|
||
maxTotalPerRackRP := rackTotalCap(rackKeys, rackShardCount, rackRoomRP, totalShards)
|
||
rackCap := func(rl relaxation) int {
|
||
if rl.rp {
|
||
return maxTotalPerRackRP
|
||
}
|
||
return maxTotalPerRack
|
||
}
|
||
|
||
attempts := strictAttempts
|
||
if mode == PlaceDurabilityFirst {
|
||
attempts = durabilityAttempts
|
||
}
|
||
|
||
var journal []placedEntry
|
||
relaxedSeen := map[string]bool{}
|
||
spilledType := false
|
||
|
||
placeShard := func(sid int, isData bool) bool {
|
||
typeTotal := dataShards
|
||
if !isData {
|
||
typeTotal = parityShards
|
||
}
|
||
for _, rl := range attempts {
|
||
node, diskID, spilled, ok := chooseShardDest(vk, sid, isData, dataShards, typeTotal, numEligibleRacks, parityShards, rackCap(rl), racks, rackKeys, rp, eligible, prefer, shardsPerRack[isData], rackShardCount, bearing, rl, freeSlotsDeadband)
|
||
if !ok {
|
||
continue
|
||
}
|
||
reserveShard(node, vk, sid, diskID)
|
||
node.freeSlots--
|
||
racks[node.rack].freeSlots--
|
||
shardsPerRack[isData][node.rack] = append(shardsPerRack[isData][node.rack], sid)
|
||
rackShardCount[node.rack]++
|
||
bearing[isData][node.rack] = true
|
||
journal = append(journal, placedEntry{node: node, sid: sid, rackKey: node.rack})
|
||
result.Destinations[sid] = Destination{
|
||
Node: node.id,
|
||
DiskID: diskID,
|
||
DataCenter: node.dc,
|
||
Rack: strings.TrimPrefix(node.rack, node.dc+":"),
|
||
}
|
||
if spilled {
|
||
spilledType = true
|
||
}
|
||
for _, name := range rl.relaxedNames() {
|
||
relaxedSeen[name] = true
|
||
}
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Data shards first, then parity, so parity can avoid data-bearing racks.
|
||
for _, isData := range []bool{true, false} {
|
||
for _, sid := range shardsOfType(need, isData, dataShards) {
|
||
if placeShard(sid, isData) {
|
||
continue
|
||
}
|
||
for _, e := range journal {
|
||
releaseShard(e.node, vk, e.sid)
|
||
e.node.freeSlots++
|
||
racks[e.rackKey].freeSlots++
|
||
}
|
||
return nil, fmt.Errorf("cannot place EC shard %d of volume %d (collection %q) (rack total cap %d, per-disk cap %d)", sid, vk.vid, vk.collection, rackCap(attempts[len(attempts)-1]), parityShards)
|
||
}
|
||
}
|
||
|
||
result.SpilledToOtherDiskType = spilledType
|
||
for name := range relaxedSeen {
|
||
result.Relaxed = append(result.Relaxed, name)
|
||
}
|
||
sort.Strings(result.Relaxed)
|
||
return result, nil
|
||
}
|
||
|
||
// chooseShardDest selects a (node, disk) for one shard at the given relaxation
|
||
// level: pick a rack (total-shards-per-rack cap + even per-type cap +
|
||
// ReplicaPlacement caps + two-pass anti-affinity to the opposite type, then
|
||
// freeSlots deadband / liveShards), then the least-loaded eligible node, then
|
||
// the best eligible disk. If no node in the chosen rack fits (e.g. every node
|
||
// is at SameRackCount), the next-best rack is tried. The third return reports
|
||
// whether the disk spilled off the soft-preferred type. ok=false when no
|
||
// rack/node/disk fits.
|
||
func chooseShardDest(vk volKey, sid int, isData bool, dataShards, typeTotal, numEligibleRacks, maxPerDisk, maxTotalPerRack int, racks map[string]*rack, rackKeys []string, rp *super_block.ReplicaPlacement, eligible func(*disk) bool, prefer func(*disk) bool, shardsPerRackType map[string][]int, rackShardCount map[string]int, bearing map[bool]map[string]bool, rl relaxation, freeSlotsDeadband int) (*Node, uint32, bool, bool) {
|
||
maxPerRack := numEligibleRacks*typeTotal + 1 // effectively unlimited when caps are relaxed
|
||
if rl.caps {
|
||
if maxPerRack = ceilDivide(typeTotal, numEligibleRacks); maxPerRack < 1 {
|
||
maxPerRack = 1
|
||
}
|
||
}
|
||
|
||
var anti map[string]bool
|
||
if rl.antiAffinity {
|
||
anti = bearing[!isData] // racks already holding the opposite shard type
|
||
}
|
||
|
||
if !rl.rp {
|
||
rp = nil
|
||
}
|
||
// A rack is eligible only if it is under the total-shards-per-rack cap and,
|
||
// when set, the per-rack shard cap (DiffRackCount). The total cap does not
|
||
// depend on ReplicaPlacement, so it also holds when rp is nil.
|
||
withinLimit := func(r string) bool {
|
||
if rl.rackTotal && rackShardCount[r] >= maxTotalPerRack {
|
||
return false
|
||
}
|
||
if rp != nil && rp.DiffRackCount > 0 && rackShardCount[r] >= rp.DiffRackCount {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
tried := map[string]bool{}
|
||
hasRoom := func(r string) bool {
|
||
return !tried[r] && racks[r].freeSlots > 0 && rackShardRoom(racks[r], vk, eligible, maxPerDisk) > 0
|
||
}
|
||
for {
|
||
destRack, ok := pickRackEligible(rackKeys, shardsPerRackType, maxPerRack, anti, hasRoom, withinLimit, racks, freeSlotsDeadband)
|
||
if !ok {
|
||
return nil, 0, false, false
|
||
}
|
||
if node := pickNodeInRackEligible(racks[destRack], vk, rp, eligible, maxPerDisk, freeSlotsDeadband); node != nil {
|
||
if diskID, ok, spilled := pickBestDiskEligible(node, vk, eligible, prefer, sid, dataShards, maxPerDisk); ok {
|
||
return node, diskID, spilled, true
|
||
}
|
||
}
|
||
tried[destRack] = true
|
||
}
|
||
}
|
||
|
||
// rackTotalCap returns the smallest per-rack total c such that the racks can
|
||
// hold totalShards shards of the volume with none above c, given each rack's
|
||
// shards already placed (held) and its room for more. On a uniform cluster this
|
||
// is ceil(totalShards/racks); a nearly full rack raises it just enough for the
|
||
// other racks to absorb its share, so the cap alone never makes a feasible
|
||
// placement fail.
|
||
//
|
||
// It is the most even spread the free capacity allows, not a durability
|
||
// guarantee: losing k racks loses up to k*c shards, which the volume survives
|
||
// only while k*c <= parityShards. With few racks no placement can achieve that.
|
||
func rackTotalCap(rackKeys []string, held, room map[string]int, totalShards int) int {
|
||
for c := 1; c < totalShards; c++ {
|
||
fits := 0
|
||
for _, rk := range rackKeys {
|
||
fits += max(held[rk], min(c, held[rk]+room[rk]))
|
||
}
|
||
if fits >= totalShards {
|
||
return c
|
||
}
|
||
}
|
||
return totalShards
|
||
}
|
||
|
||
// diskShardRoom returns how many more shards of the volume the disk can take:
|
||
// its free slots, bounded by the per-disk durability cap (maxPerDisk shards of
|
||
// one volume per disk; <= 0 disables it).
|
||
func diskShardRoom(n *Node, d *disk, vk volKey, maxPerDisk int) int {
|
||
if d.noReceive {
|
||
return 0
|
||
}
|
||
room := d.freeSlots
|
||
if maxPerDisk > 0 {
|
||
held := 0
|
||
if info := n.shards[vk]; info != nil {
|
||
held = info.diskShardBits[d.diskID].Count()
|
||
}
|
||
room = min(room, maxPerDisk-held)
|
||
}
|
||
return max(room, 0)
|
||
}
|
||
|
||
// nodeShardRoom returns how many more shards of the volume the node's eligible
|
||
// disks can take, bounded by the node's free slots.
|
||
func nodeShardRoom(n *Node, vk volKey, eligible func(*disk) bool, maxPerDisk int) int {
|
||
if n.noReceive {
|
||
return 0
|
||
}
|
||
room := 0
|
||
for _, d := range n.disks {
|
||
if eligible(d) {
|
||
room += diskShardRoom(n, d, vk, maxPerDisk)
|
||
}
|
||
}
|
||
return max(min(room, n.receiveSlots()), 0)
|
||
}
|
||
|
||
// rackShardRoom returns how many more shards of the volume the rack can take.
|
||
func rackShardRoom(r *rack, vk volKey, eligible func(*disk) bool, maxPerDisk int) int {
|
||
room := 0
|
||
for _, n := range r.nodes {
|
||
room += nodeShardRoom(n, vk, eligible, maxPerDisk)
|
||
}
|
||
return room
|
||
}
|
||
|
||
// rackShardRoomUnderRP is rackShardRoom with each node further bounded by the
|
||
// shards it may still take under ReplicaPlacement's SameRackCount (max shards of
|
||
// the volume per node), which pickNodeInRackEligible enforces.
|
||
func rackShardRoomUnderRP(r *rack, vk volKey, eligible func(*disk) bool, maxPerDisk int, rp *super_block.ReplicaPlacement) int {
|
||
if rp == nil || rp.SameRackCount <= 0 {
|
||
return rackShardRoom(r, vk, eligible, maxPerDisk)
|
||
}
|
||
room := 0
|
||
for _, n := range r.nodes {
|
||
room += min(nodeShardRoom(n, vk, eligible, maxPerDisk), max(rp.SameRackCount-volumeShardCount(n, vk), 0))
|
||
}
|
||
return room
|
||
}
|
||
|
||
// pickRackEligible is pickTarget for Place racks. Even this-type / cap /
|
||
// two-pass anti-affinity stay. Among that evenness class the remainder is
|
||
// max rack freeSlots, keep |free−maxFree| ≤ shard-slot deadband, then
|
||
// lower liveShards (sum of nodeLiveShardCount). M=0 is only maxFree then
|
||
// live (old strict freeSlots). pickTarget itself is unchanged so balancer
|
||
// / DC paths stay without M.
|
||
func pickRackEligible(candidates []string, shardsPerTarget map[string][]int, maxPerTarget int, antiAffinity map[string]bool, hasFreeSlots, withinLimit func(string) bool, racks map[string]*rack, freeSlotsDeadband int) (string, bool) {
|
||
try := func(skipAnti bool) (string, bool) {
|
||
var keys []string
|
||
var free, live []int
|
||
bestCount := maxPerTarget + 1
|
||
for _, c := range candidates {
|
||
if skipAnti && antiAffinity[c] {
|
||
continue
|
||
}
|
||
if !hasFreeSlots(c) {
|
||
continue
|
||
}
|
||
if len(shardsPerTarget[c]) >= maxPerTarget {
|
||
continue
|
||
}
|
||
if !withinLimit(c) {
|
||
continue
|
||
}
|
||
cnt := len(shardsPerTarget[c])
|
||
if len(keys) == 0 || cnt < bestCount {
|
||
keys, free, live = keys[:0], free[:0], live[:0]
|
||
bestCount = cnt
|
||
} else if cnt > bestCount {
|
||
continue
|
||
}
|
||
f := 0
|
||
if r := racks[c]; r != nil {
|
||
f = r.freeSlots
|
||
}
|
||
keys = append(keys, c)
|
||
free = append(free, f)
|
||
live = append(live, rackLiveShardCount(racks[c]))
|
||
}
|
||
if len(keys) == 0 {
|
||
return "", false
|
||
}
|
||
return keys[pickDeadbandRemainder(free, live, freeSlotsDeadband)], true
|
||
}
|
||
if len(antiAffinity) > 0 {
|
||
if t, ok := try(true); ok {
|
||
return t, true
|
||
}
|
||
}
|
||
return try(false)
|
||
}
|
||
|
||
func rackLiveShardCount(r *rack) int {
|
||
if r == nil {
|
||
return 0
|
||
}
|
||
total := 0
|
||
for _, n := range r.nodes {
|
||
total += nodeLiveShardCount(n)
|
||
}
|
||
return total
|
||
}
|
||
|
||
// pickNodeInRackEligible is pickNodeInRack restricted to nodes with an eligible
|
||
// disk that can take another shard of the volume (free slot, under maxPerDisk).
|
||
// FromActiveTopology keeps all disk types/tags in the snapshot, so without this a
|
||
// node with free volume slots but no eligible disk could be chosen.
|
||
//
|
||
// Even first (this volume, machine then node). Among that class: max node
|
||
// freeSlots, keep |free−maxFree| ≤ shard-slot deadband, then lower
|
||
// liveShards. Machine freeSlots (mFree) stays dropped as on origin/master.
|
||
// SameRackCount / 2+2 unchanged. M=0 is only maxFree then live. Callers
|
||
// convert UI volume M via volumeDeadbandToShardSlots.
|
||
func pickNodeInRackEligible(r *rack, vk volKey, rp *super_block.ReplicaPlacement, eligible func(*disk) bool, maxPerDisk, freeSlotsDeadband int) *Node {
|
||
machineShards := countShardsByHost(vk, r.nodes)
|
||
var nodes []*Node
|
||
var bestMCount, bestNCount int
|
||
for _, id := range sortedNodeKeys(r.nodes) {
|
||
node := r.nodes[id]
|
||
if nodeShardRoom(node, vk, eligible, maxPerDisk) <= 0 {
|
||
continue
|
||
}
|
||
count := volumeShardCount(node, vk)
|
||
if rp != nil && rp.SameRackCount > 0 && count >= rp.SameRackCount {
|
||
continue
|
||
}
|
||
mCount := machineShards[node.host]
|
||
if len(nodes) == 0 || mCount < bestMCount || (mCount == bestMCount && count < bestNCount) {
|
||
nodes = nodes[:0]
|
||
bestMCount, bestNCount = mCount, count
|
||
} else if mCount != bestMCount || count != bestNCount {
|
||
continue
|
||
}
|
||
nodes = append(nodes, node)
|
||
}
|
||
if len(nodes) == 0 {
|
||
return nil
|
||
}
|
||
free := make([]int, len(nodes))
|
||
live := make([]int, len(nodes))
|
||
for i, n := range nodes {
|
||
free[i] = n.freeSlots
|
||
live[i] = nodeLiveShardCount(n)
|
||
}
|
||
return nodes[pickDeadbandRemainder(free, live, freeSlotsDeadband)]
|
||
}
|
||
|
||
// pickDeadbandRemainder ranks same-evenness candidates: maxFree of the
|
||
// class, keep |free−maxFree|≤m, then min live. Ties keep the earlier
|
||
// index (scan order). M=0 keeps only maxFree then live. Shared by node
|
||
// and rack so the band cannot drift.
|
||
func pickDeadbandRemainder(free, live []int, m int) int {
|
||
maxFree := free[0]
|
||
for _, f := range free[1:] {
|
||
if f > maxFree {
|
||
maxFree = f
|
||
}
|
||
}
|
||
best := -1
|
||
for i, f := range free {
|
||
if absIntDiff(f, maxFree) > m {
|
||
continue
|
||
}
|
||
if best < 0 || live[i] < live[best] {
|
||
best = i
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
func absIntDiff(a, b int) int {
|
||
if a > b {
|
||
return a - b
|
||
}
|
||
return b - a
|
||
}
|
||
|
||
// volumeDeadbandToShardSlots converts UI volume-slot M to the shard-slot
|
||
// deadband Place compares against node.freeSlots. OSS 10+4 uses dataShards=10.
|
||
func volumeDeadbandToShardSlots(volumes, dataShards int) int {
|
||
if volumes <= 0 || dataShards <= 0 {
|
||
return 0
|
||
}
|
||
if volumes > math.MaxInt/dataShards {
|
||
return math.MaxInt
|
||
}
|
||
return volumes * dataShards
|
||
}
|
||
|
||
// pickBestDiskEligible chooses the best eligible disk on a node, ranking
|
||
// soft-preferred disks (prefer != nil && prefer(d)) ahead of others so disk-type
|
||
// Prefer uses the preferred type when available but spills otherwise. Returns the
|
||
// disk id, whether one was found, and whether the chosen disk spilled off the
|
||
// preferred type.
|
||
func pickBestDiskEligible(node *Node, vk volKey, eligible func(*disk) bool, prefer func(*disk) bool, shardID, dataShardCount, maxPerDisk int) (uint32, bool, bool) {
|
||
isDataShard := dataShardCount > 0 && shardID < dataShardCount
|
||
info := node.shards[vk]
|
||
var bestDiskID uint32
|
||
bestScore := -1
|
||
bestPreferred := false
|
||
for _, diskID := range sortedDiskKeys(node.disks) {
|
||
d := node.disks[diskID]
|
||
if !eligible(d) || d.freeSlots <= 0 || d.noReceive {
|
||
continue
|
||
}
|
||
existingShards := 0
|
||
hasData := false
|
||
hasParity := false
|
||
if info != nil {
|
||
bits := info.diskShardBits[diskID]
|
||
existingShards = bits.Count()
|
||
if dataShardCount > 0 {
|
||
for sid := range bits.All() {
|
||
if int(sid) < dataShardCount {
|
||
hasData = true
|
||
} else {
|
||
hasParity = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Durability: never put more than maxPerDisk (parityShards) shards of this
|
||
// volume on one disk, or losing that disk would lose more than EC can
|
||
// recover. Hard cap, enforced even under durability-first relaxation.
|
||
if maxPerDisk > 0 && existingShards >= maxPerDisk {
|
||
continue
|
||
}
|
||
score := d.shardCount*10 + existingShards*100
|
||
if dataShardCount > 0 {
|
||
if isDataShard && hasParity {
|
||
score += 1000
|
||
} else if !isDataShard && hasData {
|
||
score += 1000
|
||
}
|
||
}
|
||
preferred := prefer == nil || prefer(d)
|
||
if !preferred {
|
||
score += 100000 // strongly deprioritize spilling to a non-preferred type
|
||
}
|
||
if bestScore == -1 || score < bestScore {
|
||
bestScore = score
|
||
bestDiskID = diskID
|
||
bestPreferred = preferred
|
||
}
|
||
}
|
||
if bestScore == -1 {
|
||
return 0, false, false
|
||
}
|
||
return bestDiskID, true, prefer != nil && !bestPreferred
|
||
}
|
||
|
||
// clearShardAccounting removes one shard copy of a volume from the snapshot's
|
||
// per-domain accounting (the volume's shard bits) WITHOUT crediting disk capacity.
|
||
// It clears only the given physical disk's bit, then recomputes the node-level
|
||
// union from the remaining disk bits, so a kept copy of the same shard on another
|
||
// disk of the same node still counts toward caps / ReplicaPlacement / anti-affinity.
|
||
//
|
||
// Repair uses this to drop the duplicate/mismatched copies it plans to delete
|
||
// before placing missing shards, so those copies do not inflate placement
|
||
// accounting. Capacity is deliberately NOT credited: the deletes run only after
|
||
// the rebuilt shards are distributed, so the slots are not free at plan time. This
|
||
// is distinct from releaseShard, which credits freeSlots and clears the union.
|
||
func clearShardAccounting(node *Node, vk volKey, shardID int, diskID uint32) {
|
||
info, ok := node.shards[vk]
|
||
if !ok {
|
||
return
|
||
}
|
||
sid := erasure_coding.ShardId(shardID)
|
||
if bits, ok := info.diskShardBits[diskID]; ok {
|
||
info.diskShardBits[diskID] = bits.Clear(sid)
|
||
}
|
||
var union erasure_coding.ShardBits
|
||
for _, b := range info.diskShardBits {
|
||
union |= b
|
||
}
|
||
info.shardBits = union
|
||
}
|
||
|
||
// ClearShardAccounting drops one shard copy of a volume from placement accounting
|
||
// without crediting capacity (see clearShardAccounting). Repair calls it for each
|
||
// copy it plans to delete before placing missing shards, so those copies do not
|
||
// inflate caps/RP/anti-affinity. No-op for an unknown node.
|
||
func (t *Topology) ClearShardAccounting(nodeID, collection string, vid uint32, shardID int, diskID uint32) {
|
||
n, ok := t.nodes[nodeID]
|
||
if !ok {
|
||
return
|
||
}
|
||
clearShardAccounting(n, volKey{collection: collection, vid: vid}, shardID, diskID)
|
||
}
|
||
|
||
// ReleaseVolumeShards removes every shard of a volume from the snapshot and
|
||
// credits the freed disk capacity. A greenfield encode calls this so any stale
|
||
// EC shards left by a prior failed attempt (which the encode task deletes before
|
||
// distributing the new shards) neither occupy capacity nor skew anti-affinity /
|
||
// per-disk caps during planning. Unlike repair's ClearShardAccounting, it credits
|
||
// freeSlots because the deletes run before the new writes.
|
||
//
|
||
// Other volumes' disk.shardCount stay put: zeroing the snapshot here is what
|
||
// made within-rack Place see a green field and 50/50 the current volume only.
|
||
func (t *Topology) ReleaseVolumeShards(collection string, vid uint32) {
|
||
vk := volKey{collection: collection, vid: vid}
|
||
for _, n := range t.nodes {
|
||
info, ok := n.shards[vk]
|
||
if !ok {
|
||
continue
|
||
}
|
||
// freed is the total disk-slots the volume occupies on this node (a shard may
|
||
// sit on more than one disk). releaseShard credits each disk's freeSlots;
|
||
// credit the node's freeSlots by the same total, since rack capacity is summed
|
||
// from node freeSlots (buildRacks) and node freeSlots gates node eligibility.
|
||
freed := 0
|
||
for _, bits := range info.diskShardBits {
|
||
freed += bits.Count()
|
||
}
|
||
sids := make([]int, 0, info.shardBits.Count())
|
||
for sid := range info.shardBits.All() {
|
||
sids = append(sids, int(sid))
|
||
}
|
||
for _, sid := range sids {
|
||
releaseShard(n, vk, sid)
|
||
}
|
||
n.freeSlots += freed
|
||
delete(n.shards, vk)
|
||
}
|
||
}
|
||
|
||
// shardsOfType returns the sorted subset of need that are data shards (id <
|
||
// dataShards) when isData, else the parity subset.
|
||
func shardsOfType(need []int, isData bool, dataShards int) []int {
|
||
var out []int
|
||
for _, s := range need {
|
||
if (s < dataShards) == isData {
|
||
out = append(out, s)
|
||
}
|
||
}
|
||
sort.Ints(out)
|
||
return out
|
||
}
|