mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Admin computes a per-worker share of cluster_deletes_per_second at
ExecuteJob time and ships it to the worker via
ClusterContext.Metadata. The worker reads the share, constructs a
golang.org/x/time/rate.Limiter, and passes it to dailyrun.Run via
cfg.Limiter (Phase 2 already plumbed the field). Phase 5 deletes the
streaming path; until then streaming ignores the cap.
Why allocate at admin: the cluster cap is a single knob operators
care about. Dividing it locally per worker would either need
out-of-band coordination or accept N× the configured budget. Admin
is the only party that knows how many execute-capable workers there
are, so it owns the math.
Admin side (weed/admin/plugin):
- Registry.CountCapableExecutors(jobType) returns the number of
non-stale workers with CanExecute=true.
- New file cluster_rate_limit.go: decorateClusterContextForJob clones
the input ClusterContext and injects two metadata keys for
s3_lifecycle. cloneClusterContext duplicates Metadata so per-job
decoration doesn't race shared base state.
- executeJobWithExecutor calls the decorator after loading the admin
config; other job types pass through unchanged.
Worker side (weed/worker/tasks/s3_lifecycle):
- New cluster_rate_limit.go declares the constants both sides agree
on (admin-config field names, metadata keys). Plain strings on the
admin side keep weed/admin/plugin free of a dependency on the
s3_lifecycle worker package; the two sets of constants are pinned
to identical values and a mismatch would silently disable rate
limiting.
- handler.go executeDailyReplay reads ClusterContext.Metadata,
builds a rate.Limiter, and passes it into dailyrun.Config{Limiter}.
Missing/empty/non-positive values → no limiter (legacy unlimited
behavior). burst defaults to 2 × rate, clamped to ≥1 to avoid a
bucket that never refills.
- Admin form gains two fields under "Scope": cluster_deletes_per_second
(rate, 0 = unlimited) and cluster_deletes_burst (0 = 2 × rate).
Metric:
- New S3LifecycleDispatchLimiterWaitSeconds histogram observes how
long each Limiter.Wait blocks before a LifecycleDelete RPC.
Operators tune the cap by reading p95 — near-zero means the cap
isn't binding, a long tail at 1/rate means it is.
Tests:
- weed/admin/plugin/cluster_rate_limit_test.go: 9 cases covering
pass-through for non-allocator job types, rps=0 / no-executors
skip, even sharing, burst sharing, burst=0 omit (worker default
kicks in), burst floor of 1, no mutation of input metadata, nil
input.
- weed/worker/tasks/s3_lifecycle/cluster_rate_limit_test.go: 7 cases
covering nil/empty/missing metadata, non-positive/invalid rate,
positive rate builds correctly, burst missing defaults to 2× rate,
tiny rate clamps burst to ≥1.
Build clean. Phase 2 (#9446) and Phase 4 engine (#9447) are the
parents; this branch stacks on Phase 2 since it consumes
dailyrun.Config{Limiter} which lands there.
135 lines
4.9 KiB
Go
135 lines
4.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
|
)
|
|
|
|
// Job types whose ExecuteJobRequest needs per-worker rate-allocation
|
|
// metadata injected. Keyed by the job-type string so plugin.go's
|
|
// generic dispatch path stays job-agnostic.
|
|
//
|
|
// To add a new job type to the share-allocation pipeline: register an
|
|
// entry here that knows how to read its admin-config field(s) and
|
|
// produce the metadata keys/values the worker reads.
|
|
|
|
// s3LifecycleClusterDeletesPerSecondKey, s3LifecycleClusterDeletesBurstKey,
|
|
// s3LifecycleMetadataDeletesPerSecond, and s3LifecycleMetadataDeletesBurst
|
|
// are the contract between admin and worker. The values must match the
|
|
// constants exported from weed/worker/tasks/s3_lifecycle/cluster_rate_limit.go
|
|
// — duplicated here as plain strings rather than imported so the admin
|
|
// plugin package doesn't pull a dependency on the worker handler
|
|
// package. A mismatch on either side would silently disable rate
|
|
// limiting; tests pin the constants in both packages against the same
|
|
// values.
|
|
const (
|
|
s3LifecycleJobType = "s3_lifecycle"
|
|
s3LifecycleClusterDeletesPerSecondKey = "cluster_deletes_per_second"
|
|
s3LifecycleClusterDeletesBurstKey = "cluster_deletes_burst"
|
|
s3LifecycleMetadataDeletesPerSecond = "s3_lifecycle.deletes_per_second"
|
|
s3LifecycleMetadataDeletesBurst = "s3_lifecycle.deletes_burst"
|
|
)
|
|
|
|
// decorateClusterContextForJob returns a new ClusterContext with any
|
|
// per-job-type metadata the admin needs to inject before the
|
|
// ExecuteJobRequest is sent. Returns the input cc unchanged when no
|
|
// decoration applies.
|
|
//
|
|
// Today only s3_lifecycle decorates; the function exists so a future
|
|
// job type's plumbing slots in alongside without touching
|
|
// executeJobWithExecutor.
|
|
func (r *Plugin) decorateClusterContextForJob(cc *plugin_pb.ClusterContext, jobType string, adminConfigValues map[string]*plugin_pb.ConfigValue) *plugin_pb.ClusterContext {
|
|
if cc == nil {
|
|
return cc
|
|
}
|
|
if jobType != s3LifecycleJobType {
|
|
return cc
|
|
}
|
|
rps := readNonNegativeInt(adminConfigValues, s3LifecycleClusterDeletesPerSecondKey)
|
|
burst := readNonNegativeInt(adminConfigValues, s3LifecycleClusterDeletesBurstKey)
|
|
if rps <= 0 {
|
|
// Operator hasn't configured a cluster cap; nothing to allocate.
|
|
// The worker treats missing metadata keys as "unlimited," which
|
|
// is the legacy behavior.
|
|
return cc
|
|
}
|
|
executors := r.registry.CountCapableExecutors(jobType)
|
|
if executors <= 0 {
|
|
// No executors means the job won't dispatch at all; metadata
|
|
// would be discarded. Log so the case is visible in ops.
|
|
glog.V(2).Infof("decorateClusterContext: %s rps=%d but no execute-capable workers; skipping allocation", jobType, rps)
|
|
return cc
|
|
}
|
|
perWorkerRps := float64(rps) / float64(executors)
|
|
perWorkerBurst := 0
|
|
if burst > 0 {
|
|
perWorkerBurst = burst / executors
|
|
if perWorkerBurst < 1 {
|
|
perWorkerBurst = 1
|
|
}
|
|
}
|
|
|
|
// Clone so we don't mutate the shared base context. The metadata
|
|
// map is small; a fresh allocation per ExecuteJob is fine.
|
|
out := cloneClusterContext(cc)
|
|
if out.Metadata == nil {
|
|
out.Metadata = map[string]string{}
|
|
}
|
|
out.Metadata[s3LifecycleMetadataDeletesPerSecond] = strconv.FormatFloat(perWorkerRps, 'f', -1, 64)
|
|
if perWorkerBurst > 0 {
|
|
out.Metadata[s3LifecycleMetadataDeletesBurst] = strconv.Itoa(perWorkerBurst)
|
|
}
|
|
glog.V(3).Infof("decorateClusterContext: %s rps=%d burst=%d executors=%d -> per-worker rps=%g burst=%d",
|
|
jobType, rps, burst, executors, perWorkerRps, perWorkerBurst)
|
|
return out
|
|
}
|
|
|
|
// cloneClusterContext returns a shallow-but-safe copy: the top-level
|
|
// fields are reassigned, and the Metadata map is duplicated so the
|
|
// caller can mutate it without racing other consumers of the input.
|
|
// Slices of strings (master/filer/volume/s3 addresses) are copied by
|
|
// reference — those are treated as immutable elsewhere in the codebase.
|
|
func cloneClusterContext(in *plugin_pb.ClusterContext) *plugin_pb.ClusterContext {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := &plugin_pb.ClusterContext{
|
|
MasterGrpcAddresses: in.MasterGrpcAddresses,
|
|
FilerGrpcAddresses: in.FilerGrpcAddresses,
|
|
VolumeGrpcAddresses: in.VolumeGrpcAddresses,
|
|
S3GrpcAddresses: in.S3GrpcAddresses,
|
|
}
|
|
if in.Metadata != nil {
|
|
out.Metadata = make(map[string]string, len(in.Metadata))
|
|
for k, v := range in.Metadata {
|
|
out.Metadata[k] = v
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// readNonNegativeInt reads an int64 admin config value, treating
|
|
// missing fields and non-int kinds as 0. Negative values are clamped
|
|
// to 0 since the AdminConfigForm declares MinValue=0 on both fields.
|
|
func readNonNegativeInt(values map[string]*plugin_pb.ConfigValue, field string) int {
|
|
v, ok := values[field]
|
|
if !ok || v == nil {
|
|
return 0
|
|
}
|
|
switch k := v.Kind.(type) {
|
|
case *plugin_pb.ConfigValue_Int64Value:
|
|
if k.Int64Value < 0 {
|
|
return 0
|
|
}
|
|
return int(k.Int64Value)
|
|
case *plugin_pb.ConfigValue_DoubleValue:
|
|
if k.DoubleValue < 0 {
|
|
return 0
|
|
}
|
|
return int(k.DoubleValue)
|
|
}
|
|
return 0
|
|
}
|