mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 06:50:45 +01:00
ec: strict_placement option so encode only runs while guarantees hold (#11656)
* 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>
This commit is contained in:
1 parent
30cf53265b
commit
8f80dac30f
10 files changed
+134
-5
No files matched your search
@@ -18,6 +18,7 @@ type Config struct {
|
||||
MinSizeMB int `json:"min_size_mb"`
|
||||
PreferredTags []string `json:"preferred_tags"`
|
||||
ReplicaPlacement string `json:"replica_placement"` // e.g. "020"; empty falls back to the master default replication
|
||||
StrictPlacement bool `json:"strict_placement"` // fail planning instead of relaxing placement constraints
|
||||
}
|
||||
|
||||
// NewDefaultConfig creates a new default erasure coding configuration
|
||||
@@ -171,6 +172,18 @@ func GetConfigSpec() base.ConfigSpec {
|
||||
InputType: "text",
|
||||
CSSClasses: "form-control",
|
||||
},
|
||||
{
|
||||
Name: "strict_placement",
|
||||
JSONName: "strict_placement",
|
||||
Type: config.FieldTypeBool,
|
||||
DefaultValue: false,
|
||||
Required: false,
|
||||
DisplayName: "Strict Placement",
|
||||
Description: "Refuse to encode a volume when the placement constraints can't be satisfied",
|
||||
HelpText: "When enabled, a volume is only encoded if every shard can be placed within the per-disk, anti-affinity, replica-placement and per-rack caps, so the configured resilience is preserved. When disabled (default), unsatisfiable constraints are relaxed and noted in the log",
|
||||
InputType: "checkbox",
|
||||
CSSClasses: "form-check-input",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -192,6 +205,7 @@ func (c *Config) ToTaskPolicy() *worker_pb.TaskPolicy {
|
||||
CollectionFilter: c.CollectionFilter,
|
||||
PreferredTags: preferredTagsCopy,
|
||||
ReplicaPlacement: c.ReplicaPlacement,
|
||||
StrictPlacement: c.StrictPlacement,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -216,6 +230,7 @@ func (c *Config) FromTaskPolicy(policy *worker_pb.TaskPolicy) error {
|
||||
c.CollectionFilter = ecConfig.CollectionFilter
|
||||
c.PreferredTags = append([]string(nil), ecConfig.PreferredTags...)
|
||||
c.ReplicaPlacement = ecConfig.ReplicaPlacement
|
||||
c.StrictPlacement = ecConfig.StrictPlacement
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -487,8 +487,11 @@ func buildNodeAddressMap(at *topology.ActiveTopology) map[string]string {
|
||||
//
|
||||
// Encode is lenient (PlaceDurabilityFirst): it relaxes caps/anti-affinity/RP and,
|
||||
// last, the total-shards-per-rack cap as needed, failing only when no eligible
|
||||
// disk has room. It prefers the source disk type but spills if that type can't
|
||||
// hold every shard. rp is the resolved replica placement (may be nil).
|
||||
// disk has room. ecConfig.StrictPlacement instead fails the volume's planning
|
||||
// when the constraints cannot be satisfied (PlaceStrict), so a volume is only
|
||||
// encoded while its configured resilience is preserved. The task prefers the
|
||||
// source disk type but spills if that type can't hold every shard. rp is the
|
||||
// resolved replica placement (may be nil).
|
||||
func planECDestinations(snap *ecbalancer.Topology, nodeAddresses map[string]string, metric *types.VolumeHealthMetrics, ecConfig *Config, rp *super_block.ReplicaPlacement, dataShards, parityShards int) (*topology.MultiDestinationPlan, [][]uint32, error) {
|
||||
if snap == nil {
|
||||
return nil, nil, fmt.Errorf("EC placement snapshot not available")
|
||||
@@ -511,13 +514,17 @@ func planECDestinations(snap *ecbalancer.Topology, nodeAddresses map[string]stri
|
||||
for i := range need {
|
||||
need[i] = i
|
||||
}
|
||||
mode := ecbalancer.PlaceDurabilityFirst
|
||||
if ecConfig.StrictPlacement {
|
||||
mode = ecbalancer.PlaceStrict
|
||||
}
|
||||
res, err := snap.Place(metric.VolumeID, metric.Collection, need, ecbalancer.Constraints{
|
||||
DiskType: metric.DiskType,
|
||||
DiskTypePolicy: ecbalancer.DiskTypePrefer,
|
||||
PreferredTags: ecConfig.PreferredTags,
|
||||
ReplicaPlacement: rp,
|
||||
Ratio: func(string) (int, int) { return dataShards, parityShards },
|
||||
}, ecbalancer.PlaceDurabilityFirst)
|
||||
}, mode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding/ecbalancer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// StrictPlacement refuses a volume whose configured caps cannot be met,
|
||||
// rather than relaxing them: here one rack must hold all 14 shards under
|
||||
// a 2-shards-per-rack replica placement.
|
||||
func TestPlanECDestinationsStrictPlacement(t *testing.T) {
|
||||
activeTopology := buildActiveTopology(t, 7, []string{"hdd"}, 100, 0, "")
|
||||
metric := &types.VolumeHealthMetrics{
|
||||
VolumeID: 1,
|
||||
Server: "10.0.0.1:8080",
|
||||
Size: 100 * 1024 * 1024,
|
||||
}
|
||||
rp, err := super_block.NewReplicaPlacementFromString("020")
|
||||
require.NoError(t, err)
|
||||
nodeAddresses := buildNodeAddressMap(activeTopology)
|
||||
|
||||
snap := ecbalancer.FromActiveTopology(activeTopology, erasure_coding.DataShardsCount)
|
||||
cfg := NewDefaultConfig()
|
||||
plan, shardsPerPlan, err := planECDestinations(snap, nodeAddresses, metric, cfg, rp, erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount)
|
||||
require.NoError(t, err, "lenient placement relaxes the unsatisfiable rack cap")
|
||||
requireAllShardsPlaced(t, plan, shardsPerPlan)
|
||||
|
||||
snap = ecbalancer.FromActiveTopology(activeTopology, erasure_coding.DataShardsCount)
|
||||
cfg.StrictPlacement = true
|
||||
_, _, err = planECDestinations(snap, nodeAddresses, metric, cfg, rp, erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount)
|
||||
require.Error(t, err, "strict placement must refuse rather than weaken the rack cap")
|
||||
}
|
||||
|
||||
func TestStrictPlacementRoundTripsThroughTaskPolicy(t *testing.T) {
|
||||
cfg := NewDefaultConfig()
|
||||
cfg.StrictPlacement = true
|
||||
|
||||
restored := NewDefaultConfig()
|
||||
require.NoError(t, restored.FromTaskPolicy(cfg.ToTaskPolicy()))
|
||||
require.True(t, restored.StrictPlacement, "strict placement must survive the persisted policy round trip")
|
||||
|
||||
restored.StrictPlacement = false
|
||||
require.NoError(t, restored.FromTaskPolicy(restored.ToTaskPolicy()))
|
||||
require.False(t, restored.StrictPlacement)
|
||||
}
|
||||
@@ -146,6 +146,13 @@ func (h *ErasureCodingHandler) Descriptor() *plugin_pb.JobTypeDescriptor {
|
||||
FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_STRING,
|
||||
Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_TEXT,
|
||||
},
|
||||
{
|
||||
Name: "strict_placement",
|
||||
Label: "Strict Placement",
|
||||
Description: "Fail EC planning when the placement constraints cannot be met instead of relaxing them.",
|
||||
FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_BOOL,
|
||||
Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_TOGGLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -165,6 +172,9 @@ func (h *ErasureCodingHandler) Descriptor() *plugin_pb.JobTypeDescriptor {
|
||||
"replica_placement": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: ""},
|
||||
},
|
||||
"strict_placement": {
|
||||
Kind: &plugin_pb.ConfigValue_BoolValue{BoolValue: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
AdminRuntimeDefaults: &plugin_pb.AdminRuntimeDefaults{
|
||||
@@ -624,6 +634,8 @@ func deriveErasureCodingWorkerConfig(values map[string]*plugin_pb.ConfigValue) *
|
||||
|
||||
taskConfig.ReplicaPlacement = strings.TrimSpace(pluginworker.ReadStringConfig(values, "replica_placement", taskConfig.ReplicaPlacement))
|
||||
|
||||
taskConfig.StrictPlacement = pluginworker.ReadBoolConfig(values, "strict_placement", taskConfig.StrictPlacement)
|
||||
|
||||
return &erasureCodingWorkerConfig{
|
||||
TaskConfig: taskConfig,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user