mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-17 20:10:51 +02:00
326 lines
9.0 KiB
Go
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
|
|
}
|