mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-09 05:18:23 +01:00
* ec_balance: make the per-rack cap on global moves a worker setting The global load-balancing phase planned at most 10 moves per rack per detection run, hard-coded. On a cluster where one disk per machine holds several times its siblings' shards, that is about 20 moves an hour against tens of thousands needed, however many moves the worker can execute. Add global_max_moves_per_rack to the plugin worker config. The default stays 10, so nothing changes unless it is raised. It is clamped to 1..5000 because the planner reads 0 as unlimited and would plan a whole drain in one run. Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi * ec_balance: clamp global_max_moves_per_rack before narrowing it to int A value past the int32 range made ReadIntConfig return the default, so a request asking for more than the maximum got 10 moves instead of 5000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ec_balance: test the configured global cap through Detection The helper test still passed if Detection handed the planner a literal cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ec_balance: plan no more global moves per rack than the detection keeps Each global move scans the source node's volumes, and the detection keeps at most max_results moves. With the cap at 5000, three skewed racks of 20,000 volumes planned 15,000 moves to keep 500, taking about 70 s of the 300 s detection budget; bounded by max_results it takes about 5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ec_balance: test the configured global cap with a result limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
276 lines
10 KiB
Go
276 lines
10 KiB
Go
package ec_balance
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/config"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
|
)
|
|
|
|
// DefaultGlobalMaxMovesPerRack moves the global phase incrementally across detection cycles
|
|
// rather than draining a rack in one batch; raise it to work off a large skew faster.
|
|
const DefaultGlobalMaxMovesPerRack = 10
|
|
|
|
// Config extends BaseConfig with EC balance specific settings
|
|
type Config struct {
|
|
base.BaseConfig
|
|
ImbalanceThreshold float64 `json:"imbalance_threshold"`
|
|
MinServerCount int `json:"min_server_count"`
|
|
CollectionFilter string `json:"collection_filter"`
|
|
DiskType string `json:"disk_type"`
|
|
PreferredTags []string `json:"preferred_tags"`
|
|
ReplicaPlacement string `json:"replica_placement"` // e.g. "020"; empty falls back to the master default replication (even spread only when that default is empty or zero)
|
|
IoBytePerSecond int64 `json:"io_byte_per_second"` // limit each shard copy's rate; 0 falls back to the volume server's maintenance rate
|
|
// GlobalMaxMovesPerRack caps the cross-volume load-balancing moves planned per rack in one
|
|
// detection run. The plugin worker form sets it; the legacy task policy has no field for it.
|
|
GlobalMaxMovesPerRack int `json:"global_max_moves_per_rack"`
|
|
DataCenterFilter string `json:"-"` // per-detection-run, not persisted
|
|
}
|
|
|
|
// NewDefaultConfig creates a new default EC balance configuration
|
|
func NewDefaultConfig() *Config {
|
|
return &Config{
|
|
BaseConfig: base.BaseConfig{
|
|
Enabled: true,
|
|
ScanIntervalSeconds: 60 * 60, // 1 hour
|
|
MaxConcurrent: 1,
|
|
},
|
|
ImbalanceThreshold: 0.2, // 20%
|
|
MinServerCount: 3,
|
|
GlobalMaxMovesPerRack: DefaultGlobalMaxMovesPerRack,
|
|
CollectionFilter: "",
|
|
DiskType: "",
|
|
PreferredTags: nil,
|
|
}
|
|
}
|
|
|
|
// GetConfigSpec returns the configuration schema for EC balance tasks
|
|
func GetConfigSpec() base.ConfigSpec {
|
|
return base.ConfigSpec{
|
|
Fields: []*config.Field{
|
|
{
|
|
Name: "enabled",
|
|
JSONName: "enabled",
|
|
Type: config.FieldTypeBool,
|
|
DefaultValue: true,
|
|
Required: false,
|
|
DisplayName: "Enable EC Shard Balance Tasks",
|
|
Description: "Whether EC shard balance tasks should be automatically created",
|
|
HelpText: "Toggle this to enable or disable automatic EC shard balancing",
|
|
InputType: "checkbox",
|
|
CSSClasses: "form-check-input",
|
|
},
|
|
{
|
|
Name: "scan_interval_seconds",
|
|
JSONName: "scan_interval_seconds",
|
|
Type: config.FieldTypeInterval,
|
|
DefaultValue: 60 * 60,
|
|
MinValue: 10 * 60,
|
|
MaxValue: 24 * 60 * 60,
|
|
Required: true,
|
|
DisplayName: "Scan Interval",
|
|
Description: "How often to scan for EC shard imbalances",
|
|
HelpText: "The system will check for EC shard distribution imbalances at this interval",
|
|
Placeholder: "1",
|
|
Unit: config.UnitHours,
|
|
InputType: "interval",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "max_concurrent",
|
|
JSONName: "max_concurrent",
|
|
Type: config.FieldTypeInt,
|
|
DefaultValue: 1,
|
|
MinValue: 1,
|
|
MaxValue: 5,
|
|
Required: true,
|
|
DisplayName: "Max Concurrent Tasks",
|
|
Description: "Maximum number of EC shard balance tasks that can run simultaneously",
|
|
HelpText: "Limits the number of EC shard balancing operations running at the same time",
|
|
Placeholder: "1 (default)",
|
|
Unit: config.UnitCount,
|
|
InputType: "number",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "imbalance_threshold",
|
|
JSONName: "imbalance_threshold",
|
|
Type: config.FieldTypeFloat,
|
|
DefaultValue: 0.2,
|
|
MinValue: 0.05,
|
|
MaxValue: 0.5,
|
|
Required: true,
|
|
DisplayName: "Imbalance Threshold",
|
|
Description: "Minimum shard count imbalance ratio to trigger balancing",
|
|
HelpText: "EC shard distribution imbalances above this threshold will trigger rebalancing",
|
|
Placeholder: "0.20 (20%)",
|
|
Unit: config.UnitNone,
|
|
InputType: "number",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "min_server_count",
|
|
JSONName: "min_server_count",
|
|
Type: config.FieldTypeInt,
|
|
DefaultValue: 3,
|
|
MinValue: 2,
|
|
MaxValue: 100,
|
|
Required: true,
|
|
DisplayName: "Minimum Server Count",
|
|
Description: "Minimum number of servers required for EC shard balancing",
|
|
HelpText: "EC shard balancing will only occur if there are at least this many servers",
|
|
Placeholder: "3 (default)",
|
|
Unit: config.UnitCount,
|
|
InputType: "number",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "collection_filter",
|
|
JSONName: "collection_filter",
|
|
Type: config.FieldTypeString,
|
|
DefaultValue: "",
|
|
Required: false,
|
|
DisplayName: "Collection Filter",
|
|
Description: "Only balance EC shards from specific collections (comma-separated names, wildcards, or regex patterns)",
|
|
HelpText: "Leave empty to balance all collections, or list the collections to balance",
|
|
Placeholder: "pictures,videos",
|
|
InputType: "text",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "disk_type",
|
|
JSONName: "disk_type",
|
|
Type: config.FieldTypeString,
|
|
DefaultValue: "",
|
|
Required: false,
|
|
DisplayName: "Disk Type",
|
|
Description: "Only balance EC shards on this disk type",
|
|
HelpText: "Leave empty for all disk types, or specify hdd or ssd",
|
|
Placeholder: "hdd",
|
|
InputType: "text",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "preferred_tags",
|
|
JSONName: "preferred_tags",
|
|
Type: config.FieldTypeString,
|
|
DefaultValue: "",
|
|
Required: false,
|
|
DisplayName: "Preferred Disk Tags",
|
|
Description: "Comma-separated disk tags to prioritize for shard placement",
|
|
HelpText: "EC shards will be placed on disks with these tags first, ordered by preference",
|
|
Placeholder: "fast,ssd",
|
|
InputType: "text",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "replica_placement",
|
|
JSONName: "replica_placement",
|
|
Type: config.FieldTypeString,
|
|
DefaultValue: "",
|
|
Required: false,
|
|
DisplayName: "Replica Placement",
|
|
Description: "EC shard replica placement constraint (e.g. 020)",
|
|
HelpText: "Leave empty to use the master default replication (even spread only when that default is empty or zero). When set, limits shards per rack/node per the placement digits (dc/rack/node)",
|
|
Placeholder: "020",
|
|
InputType: "text",
|
|
CSSClasses: "form-control",
|
|
},
|
|
{
|
|
Name: "io_byte_per_second",
|
|
JSONName: "io_byte_per_second",
|
|
Type: config.FieldTypeInt,
|
|
DefaultValue: 0,
|
|
MinValue: 0,
|
|
Required: false,
|
|
DisplayName: "Shard Copy IO Limit (bytes/sec)",
|
|
Description: "Limit each EC shard copy's rate",
|
|
HelpText: "0 falls back to each volume server's own maintenance rate (-maintenanceBytePerSecond)",
|
|
Placeholder: "0 (server maintenance rate)",
|
|
Unit: config.UnitNone,
|
|
InputType: "number",
|
|
CSSClasses: "form-control",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ToTaskPolicy converts configuration to a TaskPolicy protobuf message
|
|
func (c *Config) ToTaskPolicy() *worker_pb.TaskPolicy {
|
|
preferredTagsCopy := append([]string(nil), c.PreferredTags...)
|
|
return &worker_pb.TaskPolicy{
|
|
Enabled: c.Enabled,
|
|
MaxConcurrent: int32(c.MaxConcurrent),
|
|
RepeatIntervalSeconds: int32(c.ScanIntervalSeconds),
|
|
CheckIntervalSeconds: int32(c.ScanIntervalSeconds),
|
|
TaskConfig: &worker_pb.TaskPolicy_EcBalanceConfig{
|
|
EcBalanceConfig: &worker_pb.EcBalanceTaskConfig{
|
|
ImbalanceThreshold: c.ImbalanceThreshold,
|
|
MinServerCount: int32(c.MinServerCount),
|
|
CollectionFilter: c.CollectionFilter,
|
|
DiskType: c.DiskType,
|
|
PreferredTags: preferredTagsCopy,
|
|
ReplicaPlacement: c.ReplicaPlacement,
|
|
IoBytePerSecond: c.IoBytePerSecond,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// FromTaskPolicy loads configuration from a TaskPolicy protobuf message
|
|
func (c *Config) FromTaskPolicy(policy *worker_pb.TaskPolicy) error {
|
|
if policy == nil {
|
|
return fmt.Errorf("policy is nil")
|
|
}
|
|
|
|
c.Enabled = policy.Enabled
|
|
c.MaxConcurrent = int(policy.MaxConcurrent)
|
|
c.ScanIntervalSeconds = int(policy.RepeatIntervalSeconds)
|
|
|
|
if ecbConfig := policy.GetEcBalanceConfig(); ecbConfig != nil {
|
|
c.ImbalanceThreshold = ecbConfig.ImbalanceThreshold
|
|
c.MinServerCount = int(ecbConfig.MinServerCount)
|
|
c.CollectionFilter = ecbConfig.CollectionFilter
|
|
c.DiskType = ecbConfig.DiskType
|
|
c.PreferredTags = append([]string(nil), ecbConfig.PreferredTags...)
|
|
c.ReplicaPlacement = ecbConfig.ReplicaPlacement
|
|
c.IoBytePerSecond = ecbConfig.IoBytePerSecond
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadConfigFromPersistence loads configuration from the persistence layer if available
|
|
func LoadConfigFromPersistence(configPersistence interface{}) *Config {
|
|
cfg := NewDefaultConfig()
|
|
|
|
if persistence, ok := configPersistence.(interface {
|
|
LoadEcBalanceTaskPolicy() (*worker_pb.TaskPolicy, error)
|
|
}); ok {
|
|
policy, err := persistence.LoadEcBalanceTaskPolicy()
|
|
switch {
|
|
case err != nil:
|
|
glog.Warningf("Could not read the persisted EC balance configuration, falling back to defaults: %v", err)
|
|
case policy == nil:
|
|
glog.V(1).Infof("No EC balance configuration persisted yet, using defaults")
|
|
default:
|
|
if err := cfg.FromTaskPolicy(policy); err != nil {
|
|
glog.Warningf("Could not apply the persisted EC balance configuration, falling back to defaults: %v", err)
|
|
} else {
|
|
glog.V(1).Infof("Loaded EC balance configuration from persistence")
|
|
return cfg
|
|
}
|
|
}
|
|
} else if configPersistence != nil {
|
|
// A store was handed in but does not expose the accessor, so the persisted
|
|
// settings are silently ignored - always a wiring bug, never a normal state.
|
|
glog.Warningf("%T cannot provide the persisted EC balance configuration: it has no LoadEcBalanceTaskPolicy() method, "+
|
|
"so the compiled-in defaults are used and any saved EC balance settings are ignored", configPersistence)
|
|
}
|
|
|
|
glog.V(1).Infof("Using default EC balance configuration")
|
|
return cfg
|
|
}
|