Files
seaweedfs/weed/admin/dash/config_toml.go
T
fefac028ec feat: send EC shards in parallel per destination disk (#12050)
* 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>
2026-10-25 06:37:26 -07:00

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
}