mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 19:47:47 +01:00
Delete the EC placement package now that encode/repair use ecbalancer.Place (#9624)
Delete the EC placement package and the dead encode planner code Now that encode (and repair) place via ecbalancer.Place, nothing uses the erasure_coding/placement package or the EC-only planner machinery (ecPlacementPlanner, diskInfosToCandidates, calculateECScoreCandidate, distributeECShards) in detection.go. Removes them and the package, along with the planner-direct unit tests.
This commit is contained in:
1 parent
0566fbd552
commit
d1665750e1
4 files changed
-1047
No files matched your search
@@ -15,9 +15,7 @@ import (
|
||||
"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/erasure_coding/placement"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
||||
workerutil "github.com/seaweedfs/seaweedfs/weed/worker/tasks/util"
|
||||
@@ -421,273 +419,6 @@ func Detection(ctx context.Context, metrics []*types.VolumeHealthMetrics, cluste
|
||||
return results, hasMore, nil
|
||||
}
|
||||
|
||||
type ecDiskState struct {
|
||||
baseAvailable int64
|
||||
reservedVolumes int32
|
||||
reservedShardSlots int32
|
||||
}
|
||||
|
||||
type ecPlacementPlanner struct {
|
||||
activeTopology *topology.ActiveTopology
|
||||
candidates []*placement.DiskCandidate
|
||||
candidateByKey map[string]*placement.DiskCandidate
|
||||
diskStates map[string]*ecDiskState
|
||||
diskTags map[string][]string
|
||||
preferredTags []string
|
||||
}
|
||||
|
||||
func newECPlacementPlanner(activeTopology *topology.ActiveTopology, preferredTags []string) *ecPlacementPlanner {
|
||||
if activeTopology == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
disks := activeTopology.GetDisksWithEffectiveCapacity(topology.TaskTypeErasureCoding, "", 0)
|
||||
candidates := diskInfosToCandidates(disks)
|
||||
tagsByKey := collectDiskTags(disks)
|
||||
normalizedPreferredTags := util.NormalizeTagList(preferredTags)
|
||||
if len(candidates) == 0 {
|
||||
return &ecPlacementPlanner{
|
||||
activeTopology: activeTopology,
|
||||
candidates: candidates,
|
||||
candidateByKey: map[string]*placement.DiskCandidate{},
|
||||
diskStates: map[string]*ecDiskState{},
|
||||
diskTags: tagsByKey,
|
||||
preferredTags: normalizedPreferredTags,
|
||||
}
|
||||
}
|
||||
|
||||
candidateByKey := make(map[string]*placement.DiskCandidate, len(candidates))
|
||||
diskStates := make(map[string]*ecDiskState, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||||
candidateByKey[key] = candidate
|
||||
diskStates[key] = &ecDiskState{
|
||||
baseAvailable: int64(candidate.FreeSlots),
|
||||
}
|
||||
}
|
||||
|
||||
return &ecPlacementPlanner{
|
||||
activeTopology: activeTopology,
|
||||
candidates: candidates,
|
||||
candidateByKey: candidateByKey,
|
||||
diskStates: diskStates,
|
||||
diskTags: tagsByKey,
|
||||
preferredTags: normalizedPreferredTags,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) selectDestinations(sourceRack, sourceDC, sourceDiskType string, shardsNeeded int) ([]*placement.DiskCandidate, error) {
|
||||
if p == nil || p.activeTopology == nil {
|
||||
return nil, fmt.Errorf("ec placement planner is not initialized")
|
||||
}
|
||||
if shardsNeeded <= 0 {
|
||||
return nil, fmt.Errorf("invalid shardsNeeded %d", shardsNeeded)
|
||||
}
|
||||
|
||||
config := placement.PlacementRequest{
|
||||
ShardsNeeded: shardsNeeded,
|
||||
MaxShardsPerServer: 0,
|
||||
MaxShardsPerRack: 0,
|
||||
MaxTaskLoad: topology.MaxTaskLoadForECPlacement,
|
||||
PreferDifferentServers: true,
|
||||
PreferDifferentRacks: true,
|
||||
// Bias placement toward disks matching the source volume's disk
|
||||
// type; placement spills to other types only if the preferred
|
||||
// pool can't satisfy ShardsNeeded (#9423).
|
||||
PreferredDiskType: sourceDiskType,
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, candidates := range p.buildCandidateSets(shardsNeeded) {
|
||||
if len(candidates) == 0 {
|
||||
continue
|
||||
}
|
||||
result, err := placement.SelectDestinations(candidates, config)
|
||||
if err == nil {
|
||||
if result.SpilledToOtherDiskType {
|
||||
glog.Warningf("EC placement spilled to disks outside preferred disk type %q to reach %d shards (source rack=%s dc=%s)",
|
||||
sourceDiskType, shardsNeeded, sourceRack, sourceDC)
|
||||
}
|
||||
return result.SelectedDisks, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no EC placement candidates available")
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) applyTaskReservations(volumeSize int64, sources []topology.TaskSourceSpec, destinations []topology.TaskDestinationSpec) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
touched := make(map[string]bool)
|
||||
|
||||
for _, source := range sources {
|
||||
impact := p.sourceImpact(source, volumeSize)
|
||||
p.applyImpact(source.ServerID, source.DiskID, impact)
|
||||
p.bumpShardCount(source.ServerID, source.DiskID, impact.ShardSlots)
|
||||
key := ecDiskKey(source.ServerID, source.DiskID)
|
||||
if !touched[key] {
|
||||
p.bumpLoad(source.ServerID, source.DiskID)
|
||||
touched[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, dest := range destinations {
|
||||
impact := p.destinationImpact(dest, volumeSize)
|
||||
p.applyImpact(dest.ServerID, dest.DiskID, impact)
|
||||
p.bumpShardCount(dest.ServerID, dest.DiskID, impact.ShardSlots)
|
||||
key := ecDiskKey(dest.ServerID, dest.DiskID)
|
||||
if !touched[key] {
|
||||
p.bumpLoad(dest.ServerID, dest.DiskID)
|
||||
touched[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) sourceImpact(source topology.TaskSourceSpec, volumeSize int64) topology.StorageSlotChange {
|
||||
if source.StorageImpact != nil {
|
||||
return *source.StorageImpact
|
||||
}
|
||||
if source.CleanupType == topology.CleanupECShards {
|
||||
return topology.CalculateECShardCleanupImpact(volumeSize)
|
||||
}
|
||||
impact, _ := topology.CalculateTaskStorageImpact(topology.TaskTypeErasureCoding, volumeSize)
|
||||
return impact
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) destinationImpact(dest topology.TaskDestinationSpec, volumeSize int64) topology.StorageSlotChange {
|
||||
if dest.StorageImpact != nil {
|
||||
return *dest.StorageImpact
|
||||
}
|
||||
_, impact := topology.CalculateTaskStorageImpact(topology.TaskTypeErasureCoding, volumeSize)
|
||||
return impact
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) applyImpact(nodeID string, diskID uint32, impact topology.StorageSlotChange) {
|
||||
if impact.IsZero() {
|
||||
return
|
||||
}
|
||||
key := ecDiskKey(nodeID, diskID)
|
||||
state, ok := p.diskStates[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
state.reservedVolumes += impact.VolumeSlots
|
||||
state.reservedShardSlots += impact.ShardSlots
|
||||
|
||||
available := state.baseAvailable - int64(state.reservedVolumes) - int64(state.reservedShardSlots)/int64(topology.ShardsPerVolumeSlot)
|
||||
if available < 0 {
|
||||
available = 0
|
||||
}
|
||||
|
||||
if candidate, ok := p.candidateByKey[key]; ok {
|
||||
candidate.FreeSlots = int(available)
|
||||
candidate.VolumeCount = candidate.MaxVolumeCount - available
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) bumpLoad(nodeID string, diskID uint32) {
|
||||
key := ecDiskKey(nodeID, diskID)
|
||||
if candidate, ok := p.candidateByKey[key]; ok {
|
||||
candidate.LoadCount++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ecPlacementPlanner) bumpShardCount(nodeID string, diskID uint32, delta int32) {
|
||||
if delta == 0 {
|
||||
return
|
||||
}
|
||||
key := ecDiskKey(nodeID, diskID)
|
||||
if candidate, ok := p.candidateByKey[key]; ok {
|
||||
candidate.ShardCount += int(delta)
|
||||
if candidate.ShardCount < 0 {
|
||||
candidate.ShardCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ecDiskKey(nodeID string, diskID uint32) string {
|
||||
return fmt.Sprintf("%s:%d", nodeID, diskID)
|
||||
}
|
||||
|
||||
func collectDiskTags(disks []*topology.DiskInfo) map[string][]string {
|
||||
tagMap := make(map[string][]string, len(disks))
|
||||
for _, disk := range disks {
|
||||
if disk == nil || disk.DiskInfo == nil {
|
||||
continue
|
||||
}
|
||||
key := ecDiskKey(disk.NodeID, disk.DiskID)
|
||||
tags := util.NormalizeTagList(disk.DiskInfo.Tags)
|
||||
if len(tags) > 0 {
|
||||
tagMap[key] = tags
|
||||
}
|
||||
}
|
||||
return tagMap
|
||||
}
|
||||
|
||||
func diskHasTag(tags []string, tag string) bool {
|
||||
if tag == "" || len(tags) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, candidate := range tags {
|
||||
if candidate == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// buildCandidateSets builds tiered candidate sets for preferred-tag prioritized placement.
|
||||
// For a planner with preferredTags, it accumulates disks matching each tag in order into
|
||||
// progressively larger tiers. It emits a candidate set once a tier reaches shardsNeeded,
|
||||
// then continues accumulating for subsequent tags. Finally, it falls back to the full
|
||||
// p.candidates set if preferred-tag tiers are insufficient. This ensures tagged disks
|
||||
// are selected first before falling back to all available candidates.
|
||||
func (p *ecPlacementPlanner) buildCandidateSets(shardsNeeded int) [][]*placement.DiskCandidate {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
if len(p.preferredTags) == 0 {
|
||||
return [][]*placement.DiskCandidate{p.candidates}
|
||||
}
|
||||
selected := make(map[string]bool, len(p.candidates))
|
||||
var tier []*placement.DiskCandidate
|
||||
var candidateSets [][]*placement.DiskCandidate
|
||||
for _, tag := range p.preferredTags {
|
||||
for _, candidate := range p.candidates {
|
||||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||||
if selected[key] {
|
||||
continue
|
||||
}
|
||||
if diskHasTag(p.diskTags[key], tag) {
|
||||
selected[key] = true
|
||||
tier = append(tier, candidate)
|
||||
}
|
||||
}
|
||||
if shardsNeeded > 0 && len(tier) >= shardsNeeded {
|
||||
candidateSets = append(candidateSets, append([]*placement.DiskCandidate(nil), tier...))
|
||||
}
|
||||
}
|
||||
// Defensive check: selectDestinations always ensures shardsNeeded > 0 before calling
|
||||
// buildCandidateSets, but this branch handles direct callers and edge cases.
|
||||
if shardsNeeded <= 0 && len(tier) > 0 {
|
||||
candidateSets = append(candidateSets, append([]*placement.DiskCandidate(nil), tier...))
|
||||
}
|
||||
if len(tier) < len(p.candidates) {
|
||||
candidateSets = append(candidateSets, p.candidates)
|
||||
} else if len(candidateSets) == 0 {
|
||||
candidateSets = append(candidateSets, p.candidates)
|
||||
}
|
||||
return candidateSets
|
||||
}
|
||||
|
||||
// planECDestinations plans the destinations for erasure coding operation.
|
||||
// dataShards/parityShards are parameters so callers can drive non-10+4 ratios.
|
||||
// countTopologyNodes counts volume-server nodes in the active topology, used by
|
||||
// the min-node safety gate.
|
||||
func countTopologyNodes(at *topology.ActiveTopology) int {
|
||||
@@ -827,23 +558,6 @@ func planECDestinations(at *topology.ActiveTopology, metric *types.VolumeHealthM
|
||||
}, shardsPerPlan, nil
|
||||
}
|
||||
|
||||
// distributeECShards assigns shard ids 0..totalShards-1 across numTargets
|
||||
// targets round-robin, so each target holds either floor or ceil of
|
||||
// totalShards/numTargets shards. When numTargets < totalShards this packs
|
||||
// several shards onto a target; planECDestinations guarantees numTargets is at
|
||||
// least ceil(totalShards/parityShards), so no target exceeds parityShards shards.
|
||||
func distributeECShards(totalShards, numTargets int) [][]uint32 {
|
||||
targetShards := make([][]uint32, numTargets)
|
||||
for i := range targetShards {
|
||||
targetShards[i] = make([]uint32, 0)
|
||||
}
|
||||
for shardId := 0; shardId < totalShards; shardId++ {
|
||||
targetIndex := shardId % numTargets
|
||||
targetShards[targetIndex] = append(targetShards[targetIndex], uint32(shardId))
|
||||
}
|
||||
return targetShards
|
||||
}
|
||||
|
||||
// createECTargets builds TaskTargets from the per-disk plans and the shard ids
|
||||
// ecbalancer.Place assigned to each (shardsPerPlan is parallel to multiPlan.Plans).
|
||||
func createECTargets(multiPlan *topology.MultiDestinationPlan, shardsPerPlan [][]uint32) []*worker_pb.TaskTarget {
|
||||
@@ -913,68 +627,6 @@ func createECTaskParams(dataShards, parityShards int, sourceDiskType string) *wo
|
||||
}
|
||||
}
|
||||
|
||||
// diskInfosToCandidates converts topology.DiskInfo slice to placement.DiskCandidate slice
|
||||
func diskInfosToCandidates(disks []*topology.DiskInfo) []*placement.DiskCandidate {
|
||||
var candidates []*placement.DiskCandidate
|
||||
for _, disk := range disks {
|
||||
if disk.DiskInfo == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate free slots (using default max if not set)
|
||||
freeSlots := int(disk.DiskInfo.MaxVolumeCount - disk.DiskInfo.VolumeCount)
|
||||
if freeSlots < 0 {
|
||||
freeSlots = 0
|
||||
}
|
||||
|
||||
// Calculate EC shard count for this specific disk
|
||||
// EcShardInfos contains all shards, so we need to filter by DiskId and sum actual shard counts
|
||||
ecShardCount := 0
|
||||
if disk.DiskInfo.EcShardInfos != nil {
|
||||
for _, shardInfo := range disk.DiskInfo.EcShardInfos {
|
||||
if shardInfo.DiskId == disk.DiskID {
|
||||
ecShardCount += erasure_coding.GetShardCount(shardInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates = append(candidates, &placement.DiskCandidate{
|
||||
NodeID: disk.NodeID,
|
||||
DiskID: disk.DiskID,
|
||||
DataCenter: disk.DataCenter,
|
||||
Rack: disk.Rack,
|
||||
DiskType: disk.DiskType,
|
||||
VolumeCount: disk.DiskInfo.VolumeCount,
|
||||
MaxVolumeCount: disk.DiskInfo.MaxVolumeCount,
|
||||
ShardCount: ecShardCount,
|
||||
FreeSlots: freeSlots,
|
||||
LoadCount: disk.LoadCount,
|
||||
})
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// calculateECScoreCandidate calculates placement score for EC operations.
|
||||
// Used for logging and plan metadata.
|
||||
func calculateECScoreCandidate(disk *placement.DiskCandidate, sourceRack, sourceDC string) float64 {
|
||||
if disk == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
score := 0.0
|
||||
|
||||
// Prefer disks with available capacity (primary factor)
|
||||
if disk.MaxVolumeCount > 0 {
|
||||
utilization := float64(disk.VolumeCount) / float64(disk.MaxVolumeCount)
|
||||
score += (1.0 - utilization) * 60.0 // Up to 60 points for available capacity
|
||||
}
|
||||
|
||||
// Consider current load (secondary factor)
|
||||
score += (10.0 - float64(disk.LoadCount)) // Up to 10 points for low load
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// findVolumeReplicaLocations finds all replica locations (server + disk) for the specified volume
|
||||
// Uses O(1) indexed lookup for optimal performance on large clusters.
|
||||
func findVolumeReplicaLocations(activeTopology *topology.ActiveTopology, volumeID uint32, collection string) []topology.VolumeReplica {
|
||||
|
||||
@@ -14,37 +14,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestECPlacementPlannerApplyReservations(t *testing.T) {
|
||||
activeTopology := buildActiveTopology(t, 1, []string{"hdd"}, 10, 0)
|
||||
|
||||
planner := newECPlacementPlanner(activeTopology, nil)
|
||||
require.NotNil(t, planner)
|
||||
|
||||
key := ecDiskKey("10.0.0.1:8080", 0)
|
||||
candidate, ok := planner.candidateByKey[key]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 10, candidate.FreeSlots)
|
||||
assert.Equal(t, 0, candidate.ShardCount)
|
||||
assert.Equal(t, 0, candidate.LoadCount)
|
||||
|
||||
shardImpact := topology.CalculateECShardStorageImpact(1, 1)
|
||||
destinations := make([]topology.TaskDestinationSpec, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
destinations[i] = topology.TaskDestinationSpec{
|
||||
ServerID: "10.0.0.1:8080",
|
||||
DiskID: 0,
|
||||
StorageImpact: &shardImpact,
|
||||
}
|
||||
}
|
||||
|
||||
planner.applyTaskReservations(1024, nil, destinations)
|
||||
|
||||
candidate = planner.candidateByKey[key]
|
||||
assert.Equal(t, 9, candidate.FreeSlots, "10 shard slots should reduce available volume slots by 1")
|
||||
assert.Equal(t, 10, candidate.ShardCount)
|
||||
assert.Equal(t, 1, candidate.LoadCount, "load should only be incremented once per disk")
|
||||
}
|
||||
|
||||
func TestPlanECDestinationsUsesPlanner(t *testing.T) {
|
||||
activeTopology := buildActiveTopology(t, 7, []string{"hdd", "ssd"}, 100, 0)
|
||||
|
||||
@@ -61,70 +30,6 @@ func TestPlanECDestinationsUsesPlanner(t *testing.T) {
|
||||
requireAllShardsPlaced(t, plan, shardsPerPlan)
|
||||
}
|
||||
|
||||
func TestECPlacementPlannerPrefersTaggedDisks(t *testing.T) {
|
||||
activeTopology := buildActiveTopology(t, 3, []string{"hdd"}, 10, 0)
|
||||
topo := activeTopology.GetTopologyInfo()
|
||||
for _, dc := range topo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for k, node := range rack.DataNodeInfos {
|
||||
for diskType := range node.DiskInfos {
|
||||
if k < 2 {
|
||||
node.DiskInfos[diskType].Tags = []string{"fast"}
|
||||
} else {
|
||||
node.DiskInfos[diskType].Tags = []string{"slow"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NoError(t, activeTopology.UpdateTopology(topo))
|
||||
|
||||
planner := newECPlacementPlanner(activeTopology, []string{"fast"})
|
||||
require.NotNil(t, planner)
|
||||
|
||||
selected, err := planner.selectDestinations("", "", "", 2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, selected, 2)
|
||||
|
||||
for _, candidate := range selected {
|
||||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||||
assert.True(t, diskHasTag(planner.diskTags[key], "fast"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestECPlacementPlannerFallsBackWhenTagsInsufficient(t *testing.T) {
|
||||
activeTopology := buildActiveTopology(t, 3, []string{"hdd"}, 10, 0)
|
||||
topo := activeTopology.GetTopologyInfo()
|
||||
for _, dc := range topo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for i, node := range rack.DataNodeInfos {
|
||||
for diskType := range node.DiskInfos {
|
||||
if i == 0 {
|
||||
node.DiskInfos[diskType].Tags = []string{"fast"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NoError(t, activeTopology.UpdateTopology(topo))
|
||||
|
||||
planner := newECPlacementPlanner(activeTopology, []string{"fast"})
|
||||
require.NotNil(t, planner)
|
||||
|
||||
selected, err := planner.selectDestinations("", "", "", 3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, selected, 3)
|
||||
|
||||
taggedCount := 0
|
||||
for _, candidate := range selected {
|
||||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||||
if diskHasTag(planner.diskTags[key], "fast") {
|
||||
taggedCount++
|
||||
}
|
||||
}
|
||||
assert.Less(t, taggedCount, len(selected))
|
||||
}
|
||||
|
||||
// TestDetectionSkipsWhenECShardsAlreadyExist guards against issue #9448: a
|
||||
// regular replica that survived a previous successful EC encode (source
|
||||
// delete didn't clean it up for some reason) gets re-proposed for encoding,
|
||||
|
||||
Reference in new issue
Block a user