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

147 lines
3.7 KiB
Go

package vacuum
import (
"sort"
)
// VacuumCandidate represents a volume eligible for vacuum
type VacuumCandidate struct {
VolumeID uint32
DataNodeID string
Size uint64
UsedSpace uint64
DeadSpace uint64
DeadSpacePercent float64
ReplicaCount int
RackID string
DataCenterID string
FileCount int64
LastModified int64
CanVacuum bool
FragmentationScore float64
Reason string
}
// DetectionOptions contains options for detection
type DetectionOptions struct {
MinVolumeSize uint64
MaxVolumeSize uint64
DeadSpaceThreshold int
TargetUtilization int
ExcludeNodes []string
PreferredNodes []string
}
// Detector scans for vacuum candidates
type Detector struct {
config DetectionOptions
}
// NewDetector creates a new vacuum detector
func NewDetector(opts DetectionOptions) *Detector {
return &Detector{
config: opts,
}
}
// DetectJobs scans volumes for vacuum candidates
func (d *Detector) DetectJobs(volumeMetrics map[uint32]*VolumeMetric) ([]*VacuumCandidate, error) {
candidates := make([]*VacuumCandidate, 0)
for volumeID, metric := range volumeMetrics {
candidate, shouldInclude := d.evaluateVolume(volumeID, metric)
if shouldInclude {
candidates = append(candidates, candidate)
}
}
d.SortByFragmentation(candidates)
return candidates, nil
}
// evaluateVolume checks if a volume should be vacuumed
func (d *Detector) evaluateVolume(volumeID uint32, metric *VolumeMetric) (*VacuumCandidate, bool) {
deadSpace := metric.Size - metric.UsedSpace
deadSpacePercent := 0.0
if metric.Size > 0 {
deadSpacePercent = float64(deadSpace) * 100.0 / float64(metric.Size)
}
candidate := &VacuumCandidate{
VolumeID: volumeID,
DataNodeID: metric.DataNodeID,
Size: metric.Size,
UsedSpace: metric.UsedSpace,
DeadSpace: deadSpace,
DeadSpacePercent: deadSpacePercent,
ReplicaCount: metric.ReplicaCount,
RackID: metric.RackID,
DataCenterID: metric.DataCenterID,
FileCount: metric.FileCount,
LastModified: metric.LastModified,
}
if metric.Size < d.config.MinVolumeSize {
candidate.CanVacuum = false
candidate.Reason = "volume too small"
return candidate, false
}
if metric.Size > d.config.MaxVolumeSize {
candidate.CanVacuum = false
candidate.Reason = "volume too large"
return candidate, false
}
if int(deadSpacePercent) < d.config.DeadSpaceThreshold {
candidate.CanVacuum = false
candidate.Reason = "insufficient dead space"
return candidate, false
}
if d.isNodeExcluded(metric.DataNodeID) {
candidate.CanVacuum = false
candidate.Reason = "node excluded"
return candidate, false
}
if len(d.config.PreferredNodes) > 0 && !d.isPreferredNode(metric.DataNodeID) {
candidate.CanVacuum = false
candidate.Reason = "node not preferred"
return candidate, false
}
if metric.IsRebalancing {
candidate.CanVacuum = false
candidate.Reason = "volume rebalancing"
return candidate, false
}
utilization := 0
if metric.Size > 0 {
utilization = int(float64(metric.UsedSpace) * 100.0 / float64(metric.Size))
}
candidate.CanVacuum = true
candidate.FragmentationScore = calculateFragmentationScore(deadSpacePercent, float64(utilization))
return candidate, true
}
// isNodeExcluded checks if a node is excluded
func (d *Detector) isNodeExcluded(nodeID string) bool {
for _, excluded := range d.config.ExcludeNodes {
if excluded == nodeID {
return true
}
}
return false
}
// isPreferredNode checks if a node is preferred
func (d *Detector) isPreferredNode(nodeID string) bool {
for _, preferred := range d.config.PreferredNodes {
if preferred == nodeID {
return true
}
}