mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-11 00:37:52 +02:00
* ec: strict_placement option so encode only runs while guarantees hold Shard placement during encode was best-effort (PlaceDurabilityFirst): when the cluster could not satisfy the per-disk caps, anti-affinity, replica-placement or per-rack caps, the constraints were relaxed and the volume was encoded anyway, weaker than configured. A strict_placement option on the erasure coding task switches planning to PlaceStrict so the volume's planning fails instead, and the encode is retried when capacity allows the guarantee. Also documents the resilience rule in ec.encode help: a volume survives losing any nodes or racks holding at most parity-shards shards between them, and how -shardReplicaPlacement's rack and node digits bound that loss. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec: expose strict_placement through the plugin form and persisted task policy The admin UI, the admin.toml maintenance mapping, and the TaskPolicy serialization all dropped the new flag; add the bool field to ErasureCodingTaskConfig, the worker config form, and both conversion directions. * shell: describe shardReplicaPlacement as requested limits, not guarantees ec.encode places shards best-effort, so the configured rack/node caps only bound shard loss when the final placement actually satisfies them. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
323 lines
11 KiB
Go
323 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 !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,
|
|
},
|
|
// 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
|
|
}
|