mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 15:00:48 +01:00
* feat: send EC shards in parallel per destination disk One stream per destination disk. max_shard_transfers unset means no global cap, 0 sends shards sequentially, and N is a global cap that still keeps one stream per disk. The value is persisted through TaskPolicy. The maintenance form is a checkbox plus Max transfers because unset and 0 differ. The key is intentionally omitted from GetConfigSpec. * ec shard distribution: treat negative max_shard_transfers as sequential The config layer (normalizedMaxShardTransfers) clamps negative values to 0, which selects sequential distribution. shardTransferCap diverged: a negative cap that reached ShardDistributionOptions directly would have selected uncapped parallel mode, the most aggressive setting. Align the options layer with the same normalization so invalid input degrades to the conservative mode. * ec: serialize shard transfers to a server when a target leaves disk choice to the receiver disk_id 0 is auto-placement on the receiving volume server, so an auto-placed stream can land on a disk another queued stream is pinned to. Grouping by (destNode, disk_id) alone let both run at once; collapse all of that server's transfers under the destNode key instead. * worker: clamp max_shard_transfers to the int32 wire range ReadIntConfig returned the 0 fallback for values above MaxInt32, turning a large positive cap into sequential mode. Read int64 and clamp into range instead. * worker: drop unused shard-transfer form helper The maintenance form renders and collects max_shard_transfers in plugin.templ JavaScript; the Go copy had no callers. * worker: bound max_shard_transfers conversion under an in-range guard --------- Co-authored-by: Chepurnov Simon <195125934+CHEPURNOV-S-S@users.noreply.github.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
344 lines
11 KiB
Go
344 lines
11 KiB
Go
package dash
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
// TomlConfig is the subset of viper used to read admin.toml values.
|
|
type TomlConfig interface {
|
|
IsSet(key string) bool
|
|
GetBool(key string) bool
|
|
GetInt(key string) int
|
|
GetFloat64(key string) float64
|
|
GetString(key string) string
|
|
GetStringSlice(key string) []string
|
|
}
|
|
|
|
// ApplyMaintenanceConfigFromToml writes maintenance task settings declared in
|
|
// admin.toml through to the persisted task configs, so they survive data
|
|
// directory loss and override admin UI edits on restart. Absent keys keep
|
|
// their persisted values.
|
|
func (cp *ConfigPersistence) ApplyMaintenanceConfigFromToml(v TomlConfig) error {
|
|
var maintenanceConf *MaintenanceConfig
|
|
maintenanceChanged := false
|
|
if k := "maintenance.enabled"; v.IsSet(k) {
|
|
conf, err := cp.LoadMaintenanceConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("load maintenance config: %w", err)
|
|
}
|
|
conf.Enabled = proto.Bool(v.GetBool(k))
|
|
// the policy lives in the per-task config files; don't snapshot it here
|
|
conf.Policy = nil
|
|
maintenanceConf = conf
|
|
maintenanceChanged = true
|
|
}
|
|
|
|
vacuumConf := vacuum.LoadConfigFromPersistence(cp)
|
|
vacuumChanged := applyBaseConfigFromToml(v, "maintenance.vacuum.", &vacuumConf.BaseConfig)
|
|
if k := "maintenance.vacuum.garbage_threshold"; v.IsSet(k) {
|
|
vacuumConf.GarbageThreshold = v.GetFloat64(k)
|
|
vacuumChanged = true
|
|
}
|
|
if k := "maintenance.vacuum.min_volume_age_seconds"; v.IsSet(k) {
|
|
vacuumConf.MinVolumeAgeSeconds = v.GetInt(k)
|
|
vacuumChanged = true
|
|
}
|
|
|
|
balanceConf := balance.LoadConfigFromPersistence(cp)
|
|
balanceChanged := applyBaseConfigFromToml(v, "maintenance.balance.", &balanceConf.BaseConfig)
|
|
if k := "maintenance.balance.imbalance_threshold"; v.IsSet(k) {
|
|
balanceConf.ImbalanceThreshold = v.GetFloat64(k)
|
|
balanceChanged = true
|
|
}
|
|
if k := "maintenance.balance.min_server_count"; v.IsSet(k) {
|
|
balanceConf.MinServerCount = v.GetInt(k)
|
|
balanceChanged = true
|
|
}
|
|
|
|
ecConf := erasure_coding.LoadConfigFromPersistence(cp)
|
|
ecChanged := applyBaseConfigFromToml(v, "maintenance.erasure_coding.", &ecConf.BaseConfig)
|
|
if k := "maintenance.erasure_coding.fullness_ratio"; v.IsSet(k) {
|
|
ecConf.FullnessRatio = v.GetFloat64(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.quiet_for_seconds"; v.IsSet(k) {
|
|
ecConf.QuietForSeconds = v.GetInt(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.min_size_mb"; v.IsSet(k) {
|
|
ecConf.MinSizeMB = v.GetInt(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.collection_filter"; v.IsSet(k) {
|
|
ecConf.CollectionFilter = v.GetString(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.preferred_tags"; v.IsSet(k) {
|
|
// viper does not split comma-separated values from env vars
|
|
var tags []string
|
|
for _, tag := range v.GetStringSlice(k) {
|
|
tags = append(tags, strings.Split(tag, ",")...)
|
|
}
|
|
ecConf.PreferredTags = util.NormalizeTagList(tags)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.replica_placement"; v.IsSet(k) {
|
|
ecConf.ReplicaPlacement = v.GetString(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.strict_placement"; v.IsSet(k) {
|
|
ecConf.StrictPlacement = v.GetBool(k)
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.free_slots_deadband"; v.IsSet(k) {
|
|
ecConf.FreeSlotsDeadband = v.GetInt(k)
|
|
if ecConf.FreeSlotsDeadband < 0 {
|
|
ecConf.FreeSlotsDeadband = 0
|
|
} else if ecConf.FreeSlotsDeadband > 100000 {
|
|
ecConf.FreeSlotsDeadband = 100000
|
|
}
|
|
ecChanged = true
|
|
}
|
|
if k := "maintenance.erasure_coding.max_shard_transfers"; v.IsSet(k) {
|
|
n := v.GetInt(k)
|
|
if n < 0 {
|
|
n = 0
|
|
} else if n > math.MaxInt32 {
|
|
n = math.MaxInt32
|
|
}
|
|
ecConf.MaxShardTransfers = &n
|
|
ecChanged = true
|
|
}
|
|
|
|
if !maintenanceChanged && !vacuumChanged && !balanceChanged && !ecChanged {
|
|
return nil
|
|
}
|
|
if !cp.IsConfigured() {
|
|
return fmt.Errorf("admin.toml maintenance settings require -dataDir to persist")
|
|
}
|
|
|
|
if maintenanceChanged {
|
|
if err := cp.SaveMaintenanceConfig(maintenanceConf); err != nil {
|
|
return fmt.Errorf("save maintenance config: %w", err)
|
|
}
|
|
glog.V(0).Infof("Applied [maintenance] settings from admin.toml (enabled: %v)", maintenanceConf.GetEnabled())
|
|
}
|
|
if vacuumChanged {
|
|
if err := cp.SaveVacuumTaskPolicy(vacuumConf.ToTaskPolicy()); err != nil {
|
|
return fmt.Errorf("save vacuum task config: %w", err)
|
|
}
|
|
glog.V(0).Infof("Applied [maintenance.vacuum] settings from admin.toml")
|
|
}
|
|
if balanceChanged {
|
|
if err := cp.SaveBalanceTaskPolicy(balanceConf.ToTaskPolicy()); err != nil {
|
|
return fmt.Errorf("save balance task config: %w", err)
|
|
}
|
|
glog.V(0).Infof("Applied [maintenance.balance] settings from admin.toml")
|
|
}
|
|
if ecChanged {
|
|
if err := cp.SaveErasureCodingTaskPolicy(ecConf.ToTaskPolicy()); err != nil {
|
|
return fmt.Errorf("save erasure coding task config: %w", err)
|
|
}
|
|
glog.V(0).Infof("Applied [maintenance.erasure_coding] settings from admin.toml")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyBaseConfigFromToml(v TomlConfig, prefix string, c *base.BaseConfig) bool {
|
|
changed := false
|
|
if k := prefix + "enabled"; v.IsSet(k) {
|
|
c.Enabled = v.GetBool(k)
|
|
changed = true
|
|
}
|
|
if k := prefix + "scan_interval_seconds"; v.IsSet(k) {
|
|
c.ScanIntervalSeconds = v.GetInt(k)
|
|
changed = true
|
|
}
|
|
if k := prefix + "max_concurrent"; v.IsSet(k) {
|
|
c.MaxConcurrent = v.GetInt(k)
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// ApplyPluginConfigFromToml overlays admin.toml [maintenance.*] settings onto
|
|
// existing plugin job type configs. Configs that do not exist yet get the
|
|
// overlay in applyPluginTomlDefaults when the plugin bootstraps them from a
|
|
// worker descriptor, so descriptor defaults are never lost.
|
|
func (s *AdminServer) ApplyPluginConfigFromToml(v TomlConfig) error {
|
|
plugin := s.GetPlugin()
|
|
if plugin == nil {
|
|
return nil
|
|
}
|
|
for _, section := range pluginConfigSections {
|
|
cfg, err := plugin.LoadJobTypeConfig(section.jobType)
|
|
if err != nil {
|
|
return fmt.Errorf("load %s plugin config: %w", section.jobType, err)
|
|
}
|
|
if cfg == nil {
|
|
continue
|
|
}
|
|
if !section.applyToml(v, cfg) {
|
|
continue
|
|
}
|
|
if err := plugin.SaveJobTypeConfig(cfg); err != nil {
|
|
return fmt.Errorf("save %s plugin config: %w", section.jobType, err)
|
|
}
|
|
glog.V(0).Infof("Applied [%s] settings from admin.toml to plugin config", section.prefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// applyPluginTomlDefaults overlays admin.toml onto a job type config the
|
|
// plugin is bootstrapping from descriptor defaults.
|
|
func applyPluginTomlDefaults(v TomlConfig, cfg *plugin_pb.PersistedJobTypeConfig) {
|
|
for _, section := range pluginConfigSections {
|
|
if section.jobType == cfg.JobType {
|
|
if section.applyToml(v, cfg) {
|
|
glog.V(0).Infof("Applied [%s] settings from admin.toml to plugin defaults", section.prefix)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type pluginConfigSection struct {
|
|
jobType string
|
|
prefix string
|
|
workerKeys map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue
|
|
adminKeys map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue
|
|
}
|
|
|
|
var pluginConfigSections = []pluginConfigSection{
|
|
{
|
|
jobType: "vacuum",
|
|
prefix: "maintenance.vacuum",
|
|
workerKeys: map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue{
|
|
"garbage_threshold": doubleValue,
|
|
"min_volume_age_seconds": int64Value,
|
|
},
|
|
},
|
|
{
|
|
jobType: "volume_balance",
|
|
prefix: "maintenance.balance",
|
|
workerKeys: map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue{
|
|
"imbalance_threshold": doubleValue,
|
|
"min_server_count": int64Value,
|
|
},
|
|
},
|
|
{
|
|
jobType: "erasure_coding",
|
|
prefix: "maintenance.erasure_coding",
|
|
workerKeys: map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue{
|
|
"fullness_ratio": doubleValue,
|
|
"quiet_for_seconds": int64Value,
|
|
"min_size_mb": int64Value,
|
|
"preferred_tags": stringListValue,
|
|
"replica_placement": stringValue,
|
|
"strict_placement": boolValue,
|
|
"free_slots_deadband": int64Value,
|
|
"max_shard_transfers": int64Value,
|
|
},
|
|
// workers read collection_filter from the admin values, not the worker values
|
|
adminKeys: map[string]func(v TomlConfig, key string) *plugin_pb.ConfigValue{
|
|
"collection_filter": stringValue,
|
|
},
|
|
},
|
|
}
|
|
|
|
func doubleValue(v TomlConfig, key string) *plugin_pb.ConfigValue {
|
|
return &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_DoubleValue{DoubleValue: v.GetFloat64(key)}}
|
|
}
|
|
|
|
func int64Value(v TomlConfig, key string) *plugin_pb.ConfigValue {
|
|
return &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(v.GetInt(key))}}
|
|
}
|
|
|
|
func boolValue(v TomlConfig, key string) *plugin_pb.ConfigValue {
|
|
return &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_BoolValue{BoolValue: v.GetBool(key)}}
|
|
}
|
|
|
|
func stringValue(v TomlConfig, key string) *plugin_pb.ConfigValue {
|
|
return &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_StringValue{StringValue: v.GetString(key)}}
|
|
}
|
|
|
|
func stringListValue(v TomlConfig, key string) *plugin_pb.ConfigValue {
|
|
// viper does not split comma-separated values from env vars
|
|
var items []string
|
|
for _, item := range v.GetStringSlice(key) {
|
|
items = append(items, strings.Split(item, ",")...)
|
|
}
|
|
return &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_StringList{StringList: &plugin_pb.StringList{Values: util.NormalizeTagList(items)}}}
|
|
}
|
|
|
|
func (section pluginConfigSection) applyToml(v TomlConfig, cfg *plugin_pb.PersistedJobTypeConfig) bool {
|
|
changed := false
|
|
if k := section.prefix + ".enabled"; v.IsSet(k) {
|
|
ensureAdminRuntime(cfg).Enabled = v.GetBool(k)
|
|
changed = true
|
|
}
|
|
if k := section.prefix + ".retry_limit"; v.IsSet(k) {
|
|
ensureAdminRuntime(cfg).RetryLimit = int32Setting(v, k)
|
|
changed = true
|
|
}
|
|
if k := section.prefix + ".retry_backoff_seconds"; v.IsSet(k) {
|
|
ensureAdminRuntime(cfg).RetryBackoffSeconds = int32Setting(v, k)
|
|
changed = true
|
|
}
|
|
for tomlKey, mapper := range section.workerKeys {
|
|
if k := section.prefix + "." + tomlKey; v.IsSet(k) {
|
|
if cfg.WorkerConfigValues == nil {
|
|
cfg.WorkerConfigValues = make(map[string]*plugin_pb.ConfigValue)
|
|
}
|
|
cfg.WorkerConfigValues[tomlKey] = mapper(v, k)
|
|
changed = true
|
|
}
|
|
}
|
|
for tomlKey, mapper := range section.adminKeys {
|
|
if k := section.prefix + "." + tomlKey; v.IsSet(k) {
|
|
if cfg.AdminConfigValues == nil {
|
|
cfg.AdminConfigValues = make(map[string]*plugin_pb.ConfigValue)
|
|
}
|
|
cfg.AdminConfigValues[tomlKey] = mapper(v, k)
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
cfg.UpdatedAt = timestamppb.Now()
|
|
cfg.UpdatedBy = "admin.toml"
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// int32Setting clamps to [0, MaxInt32] so oversized toml values cannot wrap negative
|
|
func int32Setting(v TomlConfig, key string) int32 {
|
|
n := v.GetInt(key)
|
|
if n < 0 {
|
|
return 0
|
|
}
|
|
if n > math.MaxInt32 {
|
|
return math.MaxInt32
|
|
}
|
|
return int32(n)
|
|
}
|
|
|
|
func ensureAdminRuntime(cfg *plugin_pb.PersistedJobTypeConfig) *plugin_pb.AdminRuntimeConfig {
|
|
if cfg.AdminRuntime == nil {
|
|
// maintenance job types are enabled by default; a bare runtime must not disable them
|
|
cfg.AdminRuntime = &plugin_pb.AdminRuntimeConfig{Enabled: true}
|
|
}
|
|
return cfg.AdminRuntime
|
|
}
|