mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-17 12:00:44 +02: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.
423 lines
16 KiB
Go
423 lines
16 KiB
Go
package ecbalancer
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
)
|
|
|
|
// buildPlaceTopo makes a topology of racks x nodesPerRack, each node one disk with
|
|
// perDiskFree free EC shard slots.
|
|
func buildPlaceTopo(racks, nodesPerRack, perDiskFree int) *Topology {
|
|
topo := NewTopology()
|
|
for r := 0; r < racks; r++ {
|
|
rackKey := fmt.Sprintf("dc1:rack%d", r)
|
|
for n := 0; n < nodesPerRack; n++ {
|
|
id := fmt.Sprintf("10.0.%d.%d:8080", r, n)
|
|
node := topo.AddNode(id, "dc1", rackKey, perDiskFree)
|
|
node.AddDisk(0, "", perDiskFree, 0)
|
|
}
|
|
}
|
|
return topo
|
|
}
|
|
|
|
func allShards() []int {
|
|
out := make([]int, erasure_coding.TotalShardsCount)
|
|
for i := range out {
|
|
out[i] = i
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestPlaceStrictSpreadAndCaps places a fresh 10+4 volume and checks every shard
|
|
// lands on a distinct node and no rack exceeds the even per-type cap.
|
|
func TestPlaceStrictSpreadAndCaps(t *testing.T) {
|
|
const racks = 4
|
|
topo := buildPlaceTopo(racks, 4, 50)
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d shards, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
|
|
usedNodes := map[string]bool{}
|
|
dataPerRack := map[string]int{}
|
|
parityPerRack := map[string]int{}
|
|
for sid, d := range res.Destinations {
|
|
if usedNodes[d.Node] {
|
|
t.Errorf("node %s reused for shard %d (expected distinct nodes with ample capacity)", d.Node, sid)
|
|
}
|
|
usedNodes[d.Node] = true
|
|
if sid < erasure_coding.DataShardsCount {
|
|
dataPerRack[d.Rack]++
|
|
} else {
|
|
parityPerRack[d.Rack]++
|
|
}
|
|
}
|
|
dataCap := ceilDivide(erasure_coding.DataShardsCount, racks)
|
|
parityCap := ceilDivide(erasure_coding.ParityShardsCount, racks)
|
|
for rk, n := range dataPerRack {
|
|
if n > dataCap {
|
|
t.Errorf("rack %s holds %d data shards, cap %d", rk, n, dataCap)
|
|
}
|
|
}
|
|
for rk, n := range parityPerRack {
|
|
if n > parityCap {
|
|
t.Errorf("rack %s holds %d parity shards, cap %d", rk, n, parityCap)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPlaceStrictFailsAndRollsBack: a single tiny disk cannot hold 14 shards, so
|
|
// strict Place fails and leaves the snapshot untouched.
|
|
func TestPlaceStrictFailsAndRollsBack(t *testing.T) {
|
|
topo := buildPlaceTopo(1, 1, 2) // one node, room for 2 shards
|
|
node := topo.nodes["10.0.0.0:8080"]
|
|
freeBefore := node.freeSlots
|
|
diskFreeBefore := node.disks[0].freeSlots
|
|
|
|
_, err := topo.Place(1, "c1", allShards(), Constraints{}, PlaceStrict)
|
|
if err == nil {
|
|
t.Fatal("expected Place to fail on insufficient capacity")
|
|
}
|
|
if info, ok := node.shards[volKey{collection: "c1", vid: 1}]; ok && info.shardBits.Count() != 0 {
|
|
t.Errorf("volume shard bits left on node after failed strict Place (rollback incomplete): %b", info.shardBits)
|
|
}
|
|
if node.freeSlots != freeBefore {
|
|
t.Errorf("node freeSlots = %d after rollback, want %d", node.freeSlots, freeBefore)
|
|
}
|
|
if node.disks[0].freeSlots != diskFreeBefore {
|
|
t.Errorf("disk freeSlots = %d after rollback, want %d", node.disks[0].freeSlots, diskFreeBefore)
|
|
}
|
|
}
|
|
|
|
// TestPlaceDurabilityFirstRelaxesRP: a ReplicaPlacement rack limit too tight for
|
|
// the shard count makes strict fail, while durability-first relaxes RP to place
|
|
// everything and reports the relaxation.
|
|
func TestPlaceDurabilityFirstRelaxesRP(t *testing.T) {
|
|
rp := &super_block.ReplicaPlacement{DiffRackCount: 3} // <=3 shards per rack
|
|
topo := buildPlaceTopo(2, 8, 50) // 2 racks: 2*3=6 < 14 under RP
|
|
|
|
if _, err := topo.Place(1, "c1", allShards(), Constraints{ReplicaPlacement: rp}, PlaceStrict); err == nil {
|
|
t.Fatal("strict Place should fail when RP rack limit cannot fit all shards")
|
|
}
|
|
|
|
topo = buildPlaceTopo(2, 8, 50)
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{ReplicaPlacement: rp}, PlaceDurabilityFirst)
|
|
if err != nil {
|
|
t.Fatalf("durability-first Place: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d shards, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
relaxedRP := false
|
|
for _, r := range res.Relaxed {
|
|
if r == "replica-placement" {
|
|
relaxedRP = true
|
|
}
|
|
}
|
|
if !relaxedRP {
|
|
t.Errorf("expected replica-placement relaxation, got %v", res.Relaxed)
|
|
}
|
|
}
|
|
|
|
// TestPlaceSameRackCountIsDirectPerNodeCap: the 3rd ReplicaPlacement digit
|
|
// (SameRackCount) caps shards per node directly (max == digit), matching the
|
|
// per-rack DiffRackCount cap rather than allowing digit+1 per node.
|
|
func TestPlaceSameRackCountIsDirectPerNodeCap(t *testing.T) {
|
|
rp := &super_block.ReplicaPlacement{SameRackCount: 2} // <=2 shards per node
|
|
|
|
// 5 single-node racks: 5 nodes * 2 = 10 < 14, so a strict 10+4 placement
|
|
// cannot satisfy the per-node cap and must fail. Under the old digit+1 reading
|
|
// the cap would be 3/node => 15 slots and this would have wrongly succeeded.
|
|
topo := buildPlaceTopo(5, 1, 50)
|
|
if _, err := topo.Place(1, "c1", allShards(), Constraints{ReplicaPlacement: rp}, PlaceStrict); err == nil {
|
|
t.Fatal("strict Place should fail: 5 nodes cannot hold 14 shards at <=2 per node")
|
|
}
|
|
|
|
// Durability-first relaxes the unsatisfiable per-node cap, still places every
|
|
// shard, and reports the relaxation so it isn't silently weakened.
|
|
topo = buildPlaceTopo(5, 1, 50)
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{ReplicaPlacement: rp}, PlaceDurabilityFirst)
|
|
if err != nil {
|
|
t.Fatalf("durability-first Place: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d shards, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
relaxedRP := false
|
|
for _, r := range res.Relaxed {
|
|
if r == "replica-placement" {
|
|
relaxedRP = true
|
|
}
|
|
}
|
|
if !relaxedRP {
|
|
t.Errorf("expected replica-placement relaxation, got %v", res.Relaxed)
|
|
}
|
|
}
|
|
|
|
// TestPlaceDiskTypeHardFilter: with DiskType set, shards land only on disks of
|
|
// that type, even though the snapshot also contains other-typed disks.
|
|
func TestPlaceDiskTypeHardFilter(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 4; r++ {
|
|
rackKey := fmt.Sprintf("dc1:rack%d", r)
|
|
ssd := topo.AddNode(fmt.Sprintf("ssd-%d:8080", r), "dc1", rackKey, 50)
|
|
ssd.AddDisk(0, "ssd", 50, 0)
|
|
hdd := topo.AddNode(fmt.Sprintf("hdd-%d:8080", r), "dc1", rackKey, 50)
|
|
hdd.AddDisk(0, "hdd", 50, 0)
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{DiskType: "ssd", DiskTypePolicy: DiskTypeRequire}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place ssd: %v", err)
|
|
}
|
|
for sid, d := range res.Destinations {
|
|
node := topo.nodes[d.Node]
|
|
disk := node.disks[d.DiskID]
|
|
if disk == nil || disk.diskType != "ssd" {
|
|
t.Errorf("shard %d placed on non-ssd disk: node=%s diskID=%d", sid, d.Node, d.DiskID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPlaceDiskTypeUnavailableFails: a request for a disk type with no matching
|
|
// disks fails rather than silently placing on the wrong tier.
|
|
func TestPlaceDiskTypeUnavailableFails(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 4; r++ {
|
|
n := topo.AddNode(fmt.Sprintf("hdd-%d:8080", r), "dc1", fmt.Sprintf("dc1:rack%d", r), 50)
|
|
n.AddDisk(0, "hdd", 50, 0)
|
|
}
|
|
if _, err := topo.Place(1, "c1", allShards(), Constraints{DiskType: "ssd", DiskTypePolicy: DiskTypeRequire}, PlaceStrict); err == nil {
|
|
t.Fatal("expected Place to fail when no disks of the requested type exist")
|
|
}
|
|
}
|
|
|
|
// TestPlaceHDDRequestMatchesEmptyTypeDisks: a "hdd" request normalizes to
|
|
// HardDriveType ("") and must land on the HDD disk (reported as ""), never the SSD
|
|
// disk, even on nodes that have both.
|
|
func TestPlaceHDDRequestMatchesEmptyTypeDisks(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 6; r++ {
|
|
n := topo.AddNode(fmt.Sprintf("n%d:8080", r), "dc1", fmt.Sprintf("dc1:rack%d", r), 100)
|
|
n.AddDisk(0, "", 50, 0) // HDD (HardDriveType, reported as "")
|
|
n.AddDisk(1, "ssd", 50, 0) // SSD
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{DiskType: "hdd", DiskTypePolicy: DiskTypeRequire}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place hdd: %v", err)
|
|
}
|
|
for sid, d := range res.Destinations {
|
|
if d.DiskID != 0 { // disk 0 is the HDD disk on every node
|
|
t.Errorf("shard %d placed on disk %d (expected HDD disk 0) on node %s", sid, d.DiskID, d.Node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPlaceDurabilityCapRejectsSkewed: in a near-full cluster where only one disk
|
|
// has spare room, Place must not pile more than parityShards shards onto it (losing
|
|
// it would then lose more than EC can recover). It fails instead, so the caller
|
|
// leaves the volume unencoded rather than minting an unrecoverable layout.
|
|
func TestPlaceDurabilityCapRejectsSkewed(t *testing.T) {
|
|
topo := NewTopology()
|
|
// One spacious node plus four nearly-full ones, all in a single rack.
|
|
a := topo.AddNode("a:8080", "dc1", "dc1:rack0", 100)
|
|
a.AddDisk(0, "", 100, 0)
|
|
for i := 0; i < 4; i++ {
|
|
n := topo.AddNode(fmt.Sprintf("b%d:8080", i), "dc1", "dc1:rack0", 1)
|
|
n.AddDisk(0, "", 1, 0)
|
|
}
|
|
|
|
// 14 shards, parity 4: node a is capped at 4, the others hold 1 each -> at most
|
|
// 4+4=8 placeable without exceeding parityShards on a disk, so Place must fail.
|
|
// (Without the per-disk cap, a would greedily absorb 10 shards and "succeed".)
|
|
if _, err := topo.Place(1, "c1", allShards(), Constraints{}, PlaceDurabilityFirst); err == nil {
|
|
t.Fatal("expected Place to fail rather than pile >parityShards shards on one disk")
|
|
}
|
|
}
|
|
|
|
// TestReleaseVolumeShards: removes all of a volume's shards from the snapshot and
|
|
// credits the freed capacity at BOTH disk and node level (rack capacity sums node
|
|
// freeSlots), mirroring how FromActiveTopology accounts stale shards.
|
|
func TestReleaseVolumeShards(t *testing.T) {
|
|
topo := NewTopology()
|
|
// Node total 20 = two disks of 10. Two stale shards occupy one slot each, so the
|
|
// snapshot would show disk0/disk1 at 9 and node at 18 (as FromActiveTopology does).
|
|
n := topo.AddNode("n0:8080", "dc1", "dc1:rack0", 20)
|
|
n.AddDisk(0, "", 10, 0)
|
|
n.AddDisk(1, "", 10, 0)
|
|
vk := volKey{collection: "c1", vid: 1}
|
|
n.AddShards(1, "c1", 0, erasure_coding.ShardBits(uint32(1)<<3))
|
|
n.AddShards(1, "c1", 1, erasure_coding.ShardBits(uint32(1)<<7))
|
|
n.disks[0].freeSlots = 9
|
|
n.disks[1].freeSlots = 9
|
|
n.freeSlots = 18
|
|
|
|
topo.ReleaseVolumeShards("c1", 1)
|
|
|
|
if _, ok := n.shards[vk]; ok {
|
|
t.Error("volume shards should be gone after ReleaseVolumeShards")
|
|
}
|
|
if n.disks[0].freeSlots != 10 || n.disks[1].freeSlots != 10 {
|
|
t.Errorf("disk freeSlots not restored: disk0=%d, disk1=%d (want 10, 10)", n.disks[0].freeSlots, n.disks[1].freeSlots)
|
|
}
|
|
if n.freeSlots != 20 {
|
|
t.Errorf("node freeSlots = %d, want 20 (must be credited at node level too)", n.freeSlots)
|
|
}
|
|
}
|
|
|
|
// TestClearShardAccounting: dropping one disk's copy of a shard preserves a kept
|
|
// copy of the same shard on another disk of the same node, and credits no capacity.
|
|
func TestClearShardAccounting(t *testing.T) {
|
|
topo := NewTopology()
|
|
n := topo.AddNode("n0:8080", "dc1", "dc1:rack0", 50)
|
|
n.AddDisk(0, "", 50, 0)
|
|
n.AddDisk(1, "", 50, 0)
|
|
vk := volKey{collection: "c1", vid: 1}
|
|
// Shard 3 lives on disk 0 (keep) and disk 1 (duplicate to delete).
|
|
n.AddShards(1, "c1", 0, erasure_coding.ShardBits(uint32(1)<<3))
|
|
n.AddShards(1, "c1", 1, erasure_coding.ShardBits(uint32(1)<<3))
|
|
if got := n.shards[vk].shardBits.Count(); got != 1 {
|
|
t.Fatalf("union count = %d, want 1", got)
|
|
}
|
|
freeBefore := n.disks[1].freeSlots
|
|
|
|
clearShardAccounting(n, vk, 3, 1)
|
|
|
|
if !n.shards[vk].shardBits.Has(erasure_coding.ShardId(3)) {
|
|
t.Error("kept copy of shard 3 (disk 0) lost from the node-level union")
|
|
}
|
|
if n.shards[vk].diskShardBits[1].Has(erasure_coding.ShardId(3)) {
|
|
t.Error("disk-1 copy of shard 3 was not cleared")
|
|
}
|
|
if n.disks[1].freeSlots != freeBefore {
|
|
t.Errorf("freeSlots changed %d -> %d; clearShardAccounting must not credit capacity", freeBefore, n.disks[1].freeSlots)
|
|
}
|
|
}
|
|
|
|
// TestPlaceDiskTypePreferSpills: DiskTypePrefer fills the preferred type first and
|
|
// spills the remainder to other types, reporting SpilledToOtherDiskType. SSD is
|
|
// scarce (one tiny SSD per node) so the volume must spill to HDD, but there are
|
|
// enough disks to keep each within the parityShards durability cap.
|
|
func TestPlaceDiskTypePreferSpills(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 8; r++ {
|
|
n := topo.AddNode(fmt.Sprintf("n%d:8080", r), "dc1", fmt.Sprintf("dc1:rack%d", r), 100)
|
|
n.AddDisk(0, "ssd", 1, 0) // tiny SSD: 1 shard
|
|
n.AddDisk(1, "", 50, 0) // roomy HDD
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{DiskType: "ssd", DiskTypePolicy: DiskTypePrefer}, PlaceDurabilityFirst)
|
|
if err != nil {
|
|
t.Fatalf("Place: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
ssd, hdd := 0, 0
|
|
for _, d := range res.Destinations {
|
|
if topo.nodes[d.Node].disks[d.DiskID].diskType == "ssd" {
|
|
ssd++
|
|
} else {
|
|
hdd++
|
|
}
|
|
}
|
|
if ssd == 0 || hdd == 0 {
|
|
t.Errorf("expected prefer-then-spill: some shards on SSD and some on HDD, got ssd=%d hdd=%d", ssd, hdd)
|
|
}
|
|
if !res.SpilledToOtherDiskType {
|
|
t.Error("expected SpilledToOtherDiskType when SSD cannot hold every shard")
|
|
}
|
|
}
|
|
|
|
// TestPlacePreferredTagsUseTaggedDisks: when tagged disks can hold the whole plan,
|
|
// every shard lands on a tagged disk and no spill is reported.
|
|
func TestPlacePreferredTagsUseTaggedDisks(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 4; r++ {
|
|
rackKey := fmt.Sprintf("dc1:rack%d", r)
|
|
fast := topo.AddNode(fmt.Sprintf("fast-%d:8080", r), "dc1", rackKey, 50)
|
|
fast.AddDisk(0, "", 50, 0)
|
|
fast.AddDiskTags(0, []string{"fast"})
|
|
topo.AddNode(fmt.Sprintf("slow-%d:8080", r), "dc1", rackKey, 50).AddDisk(0, "", 50, 0)
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{PreferredTags: []string{"fast"}}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place: %v", err)
|
|
}
|
|
for sid, d := range res.Destinations {
|
|
if !diskHasAnyTag(topo.nodes[d.Node].disks[d.DiskID], []string{"fast"}) {
|
|
t.Errorf("shard %d placed on an untagged disk (node %s)", sid, d.Node)
|
|
}
|
|
}
|
|
if res.SpilledOutsidePreferredTags {
|
|
t.Error("did not expect tag spill when tagged disks suffice")
|
|
}
|
|
}
|
|
|
|
// TestPlacePreferredTagsSpillWhenInsufficient: when the tagged tier cannot hold the
|
|
// whole plan, Place falls back to all disks and reports SpilledOutsidePreferredTags.
|
|
func TestPlacePreferredTagsSpillWhenInsufficient(t *testing.T) {
|
|
topo := NewTopology()
|
|
for r := 0; r < 4; r++ {
|
|
n := topo.AddNode(fmt.Sprintf("n-%d:8080", r), "dc1", fmt.Sprintf("dc1:rack%d", r), 50)
|
|
if r == 0 {
|
|
n.AddDisk(0, "", 5, 0) // the only tagged disk, too small for 14 shards
|
|
n.AddDiskTags(0, []string{"fast"})
|
|
} else {
|
|
n.AddDisk(0, "", 50, 0)
|
|
}
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{PreferredTags: []string{"fast"}}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
if !res.SpilledOutsidePreferredTags {
|
|
t.Error("expected SpilledOutsidePreferredTags when the single fast disk cannot hold the plan")
|
|
}
|
|
}
|
|
|
|
// TestPlaceStrictCapsCountEligibleRacks: with DiskTypeRequire, the even per-rack
|
|
// cap divides by racks that have a matching disk, not all racks, so SSDs in only
|
|
// some racks still place successfully.
|
|
func TestPlaceStrictCapsCountEligibleRacks(t *testing.T) {
|
|
topo := NewTopology()
|
|
// SSDs live in only 2 of 4 racks, with several SSD nodes per rack so 14 shards
|
|
// fit at <= parityShards per disk. The even per-rack cap must divide by the 2
|
|
// eligible racks (ceil(10/2)=5 data/rack), not all 4 (ceil(10/4)=3 -> infeasible).
|
|
for r := 0; r < 4; r++ {
|
|
rackKey := fmt.Sprintf("dc1:rack%d", r)
|
|
topo.AddNode(fmt.Sprintf("hdd-%d:8080", r), "dc1", rackKey, 50).AddDisk(0, "", 50, 0)
|
|
if r < 2 {
|
|
for n := 0; n < 4; n++ {
|
|
topo.AddNode(fmt.Sprintf("ssd-%d-%d:8080", r, n), "dc1", rackKey, 50).AddDisk(0, "ssd", 50, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
res, err := topo.Place(1, "c1", allShards(), Constraints{DiskType: "ssd", DiskTypePolicy: DiskTypeRequire}, PlaceStrict)
|
|
if err != nil {
|
|
t.Fatalf("Place ssd in 2/4 racks: %v", err)
|
|
}
|
|
if len(res.Destinations) != erasure_coding.TotalShardsCount {
|
|
t.Fatalf("placed %d, want %d", len(res.Destinations), erasure_coding.TotalShardsCount)
|
|
}
|
|
for sid, d := range res.Destinations {
|
|
if disk := topo.nodes[d.Node].disks[d.DiskID]; disk == nil || disk.diskType != "ssd" {
|
|
t.Errorf("shard %d not on an SSD disk: node=%s disk=%d", sid, d.Node, d.DiskID)
|
|
}
|
|
}
|
|
}
|