Files
seaweedfs/weed/admin/plugin/workers/balance/detector.go
T

326 lines
9.0 KiB
Go

package balance
import (
"math"
"sort"
)
// NodeDiskMetric contains disk usage information for a data node
type NodeDiskMetric struct {
NodeID string
TotalSpace uint64
UsedSpace uint64
FreeSpace uint64
VolumeCount int
}
// RebalanceCandidate represents a candidate volume for rebalancing
type RebalanceCandidate struct {
VolumeID uint32
SourceNodeID string
DestinationNodeID string
VolumeSize uint64
CurrentNodeUsage float64
DestinationUsage float64
ExpectedBenefit float64
ImbalanceScore float64
Priority int
CanRelocate bool
Reason string
}
// DetectionOptions contains options for rebalancing detection
type DetectionOptions struct {
MinVolumeSize uint64
MaxVolumeSize uint64
DiskUsageThreshold float64
AcceptableImbalancePercent float64
PreferBalancedDistribution bool
DataNodeCount int
}
// Detector identifies imbalanced data distribution
type Detector struct {
config DetectionOptions
}
// NewDetector creates a new balance detector
func NewDetector(opts DetectionOptions) *Detector {
return &Detector{
config: opts,
}
}
// DetectJobs analyzes disk usage across nodes and identifies rebalance opportunities
func (d *Detector) DetectJobs(nodeMetrics map[string]*NodeDiskMetric) ([]*RebalanceCandidate, error) {
candidates := make([]*RebalanceCandidate, 0)
if len(nodeMetrics) == 0 {
return candidates, nil
}
// Calculate statistics
avgUsage := d.calculateAverageUsage(nodeMetrics)
stdDev := d.calculateUsageStdDev(nodeMetrics, avgUsage)
imbalanceScore := stdDev / avgUsage
// Check if imbalance exceeds threshold
threshold := d.config.AcceptableImbalancePercent / 100.0
if imbalanceScore < threshold {
return candidates, nil
}
// Find source and destination nodes
sourceNodes := d.findSourceNodes(nodeMetrics, avgUsage)
destNodes := d.findDestinationNodes(nodeMetrics, avgUsage)
// Generate rebalance candidates
for _, sourceNode := range sourceNodes {
for _, destNode := range destNodes {
candidate := d.evaluateRebalanceOpportunity(sourceNode, destNode, nodeMetrics, imbalanceScore)
if candidate.CanRelocate {
candidates = append(candidates, candidate)
}
}
}
// Sort by priority
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].Priority > candidates[j].Priority
})
return candidates, nil
}
// calculateAverageUsage calculates average disk usage across nodes
func (d *Detector) calculateAverageUsage(nodeMetrics map[string]*NodeDiskMetric) float64 {
if len(nodeMetrics) == 0 {
return 0
}
var totalUsage float64
for _, node := range nodeMetrics {
if node.TotalSpace > 0 {
totalUsage += float64(node.UsedSpace) / float64(node.TotalSpace)
}
}
return totalUsage / float64(len(nodeMetrics))
}
// calculateUsageStdDev calculates standard deviation of disk usage
func (d *Detector) calculateUsageStdDev(nodeMetrics map[string]*NodeDiskMetric, avgUsage float64) float64 {
if len(nodeMetrics) <= 1 {
return 0
}
var sumSquaredDiff float64
for _, node := range nodeMetrics {
var nodeUsage float64
if node.TotalSpace > 0 {
nodeUsage = float64(node.UsedSpace) / float64(node.TotalSpace)
}
diff := nodeUsage - avgUsage
sumSquaredDiff += diff * diff
}
variance := sumSquaredDiff / float64(len(nodeMetrics))
return math.Sqrt(variance)
}
// findSourceNodes identifies nodes with high disk usage
func (d *Detector) findSourceNodes(nodeMetrics map[string]*NodeDiskMetric, avgUsage float64) []*NodeDiskMetric {
sources := make([]*NodeDiskMetric, 0)
threshold := avgUsage * 1.2 // 20% above average
for _, node := range nodeMetrics {
if node.TotalSpace == 0 {
continue
}
nodeUsage := float64(node.UsedSpace) / float64(node.TotalSpace)
if nodeUsage > threshold && float64(node.UsedSpace) > 0 {
sources = append(sources, node)
}
}
// Sort by usage (highest first)
sort.Slice(sources, func(i, j int) bool {
usageI := float64(sources[i].UsedSpace) / float64(sources[i].TotalSpace)
usageJ := float64(sources[j].UsedSpace) / float64(sources[j].TotalSpace)
return usageI > usageJ
})
return sources
}
// findDestinationNodes identifies nodes with low disk usage
func (d *Detector) findDestinationNodes(nodeMetrics map[string]*NodeDiskMetric, avgUsage float64) []*NodeDiskMetric {
destinations := make([]*NodeDiskMetric, 0)
threshold := avgUsage * 0.8 // 20% below average
for _, node := range nodeMetrics {
if node.TotalSpace == 0 {
continue
}
nodeUsage := float64(node.UsedSpace) / float64(node.TotalSpace)
if nodeUsage < threshold && node.FreeSpace > 0 {
destinations = append(destinations, node)
}
}
// Sort by free space (most available first)
sort.Slice(destinations, func(i, j int) bool {
return destinations[i].FreeSpace > destinations[j].FreeSpace
})
return destinations
}
// evaluateRebalanceOpportunity evaluates if rebalancing between two nodes is beneficial
func (d *Detector) evaluateRebalanceOpportunity(
sourceNode, destNode *NodeDiskMetric,
allNodes map[string]*NodeDiskMetric,
currentImbalanceScore float64,
) *RebalanceCandidate {
candidate := &RebalanceCandidate{
SourceNodeID: sourceNode.NodeID,
DestinationNodeID: destNode.NodeID,
CanRelocate: false,
}
// Check if destination node has sufficient capacity
if !d.checkNodeCapacity(destNode) {
candidate.Reason = "destination node insufficient capacity"
return candidate
}
// Calculate current usage
sourceUsage := float64(sourceNode.UsedSpace) / float64(sourceNode.TotalSpace)
destUsage := float64(destNode.UsedSpace) / float64(destNode.TotalSpace)
candidate.CurrentNodeUsage = sourceUsage
candidate.DestinationUsage = destUsage
candidate.VolumeSize = 1000 // Default volume size
// Estimate benefit
benefit := d.estimateRebalanceBenefit(sourceUsage, destUsage)
candidate.ExpectedBenefit = benefit
// Calculate imbalance score for this candidate
candidate.ImbalanceScore = currentImbalanceScore
// Determine priority
candidate.Priority = int(benefit * 100)
if candidate.Priority < 0 {
candidate.Priority = 0
}
// Check if rebalancing is worthwhile
if benefit > 0.01 { // 1% improvement threshold
candidate.CanRelocate = true
candidate.Reason = "beneficial rebalancing opportunity"
} else {
candidate.Reason = "insufficient benefit from rebalancing"
}
return candidate
}
// checkNodeCapacity validates if destination node can accept data
func (d *Detector) checkNodeCapacity(node *NodeDiskMetric) bool {
if node.TotalSpace == 0 {
return false
}
// Check if node has at least 10% free space
freePercentage := float64(node.FreeSpace) / float64(node.TotalSpace)
if freePercentage < 0.1 {
return false
}
// Check if node doesn't exceed disk usage threshold
usagePercentage := float64(node.UsedSpace) / float64(node.TotalSpace)
if usagePercentage > d.config.DiskUsageThreshold/100.0 {
return false
}
return true
}
// estimateRebalanceBenefit estimates the benefit of moving data from source to destination
func (d *Detector) estimateRebalanceBenefit(sourceUsage, destUsage float64) float64 {
// Simple calculation: difference between source and destination usage
return sourceUsage - destUsage
}
// SortByImbalance sorts candidates by imbalance impact
func SortByImbalance(candidates []*RebalanceCandidate) {
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].ExpectedBenefit != candidates[j].ExpectedBenefit {
return candidates[i].ExpectedBenefit > candidates[j].ExpectedBenefit
}
return candidates[i].Priority > candidates[j].Priority
})
}
// VolumeMetric contains volume statistics
type VolumeMetric struct {
VolumeID uint32
DataNodeID string
Size uint64
FreeSpace uint64
ReplicaCount int
RackID string
DataCenterID string
FileCount int64
LastModified int64
Collection string
}
// FilterByCriteria filters rebalance candidates by specific criteria
func FilterByCriteria(candidates []*RebalanceCandidate, criteria map[string]string) []*RebalanceCandidate {
filtered := make([]*RebalanceCandidate, 0)
for _, candidate := range candidates {
if !candidate.CanRelocate {
continue
}
// Apply source node filter if specified
if sourceNode, ok := criteria["source_node"]; ok && sourceNode != "" && candidate.SourceNodeID != sourceNode {
continue
}
// Apply destination node filter if specified
if destNode, ok := criteria["dest_node"]; ok && destNode != "" && candidate.DestinationNodeID != destNode {
continue
}
// Apply minimum benefit filter if specified
if minBenefit, ok := criteria["min_benefit"]; ok && minBenefit != "" {
// Would parse minBenefit and filter
}
filtered = append(filtered, candidate)
}
return filtered
}
// GroupBySourceNode groups candidates by source node for parallel execution
func GroupBySourceNode(candidates []*RebalanceCandidate) map[string][]*RebalanceCandidate {
grouped := make(map[string][]*RebalanceCandidate)
for _, candidate := range candidates {
sourceID := candidate.SourceNodeID
if sourceID == "" {
sourceID = "unknown"
}
grouped[sourceID] = append(grouped[sourceID], candidate)
}
return grouped
}