feat(worker): 实现集群限流与自适应负载

保留平台模型 RPM、TPM 和并发策略语义,增加 PostgreSQL 集群级租约、饱和候选重选和多平台自动负载,避免突发任务固定等待首个平台。\n\n新增 Worker 实时负载采样、自适应 active/heavy 容量、心跳与管理端指标,并扩展本地 acceptance runner,覆盖三 Worker、同模型三平台 2/4/6 并发和 48 个带图视频突发任务。\n\n验证:go test ./...、go vet ./...、PostgreSQL 跨 Store 集成测试、gofmt、bash -n、ShellCheck 及本地集群 provider-burst 验收通过;48/48 成功,无越限、重复提交、重复计费、重复回调或终态资源泄漏。
This commit is contained in:
2026-08-03 00:13:46 +08:00
parent 9a01fd4657
commit c28bf74230
52 changed files with 3700 additions and 272 deletions
+381
View File
@@ -0,0 +1,381 @@
package workerload
import (
"context"
"errors"
"strings"
"sync"
"time"
)
type Phase string
const (
PhasePreparing Phase = "preparing"
PhaseWaitingUpstream Phase = "waiting_upstream"
PhaseFinalizing Phase = "finalizing"
)
type PressureState string
const (
PressureNormal PressureState = "normal"
PressureBusy PressureState = "busy"
PressureCritical PressureState = "critical"
)
const (
ModeAdaptive = "adaptive"
ModeLegacy = "legacy"
)
var ErrReleased = errors.New("worker load lease already released")
type Config struct {
Mode string
HardLimit int
InitialActive int
InitialHeavy int
HealthySamples int
}
type ResourceSample struct {
MemoryCurrentBytes int64
MemoryLimitBytes int64
CPUUtilization float64
CPUThrottled bool
DBConnections int32
DBMaxConnections int32
SampledAt time.Time
}
type Snapshot struct {
Mode string
ActiveLimit int
HeavyLimit int
ClaimLimit int
SafeCapacity int
ActiveTasks int
PreparingTasks int
WaitingUpstreamTasks int
FinalizingTasks int
PressureState PressureState
PressureReason string
MemoryUtilization float64
CPUUtilization float64
DBUtilization float64
SampledAt time.Time
}
type Controller struct {
mu sync.Mutex
mode string
hardLimit int
activeLimit int
heavyLimit int
claimLimit int
healthySamples int
healthyCount int
preparing int
waiting int
finalizing int
last Snapshot
wake chan struct{}
}
type Lease struct {
controller *Controller
phase Phase
released bool
}
func New(config Config) *Controller {
mode := strings.ToLower(strings.TrimSpace(config.Mode))
if mode != ModeLegacy {
mode = ModeAdaptive
}
if config.HardLimit < 1 {
config.HardLimit = 1
}
if config.InitialActive < 1 {
config.InitialActive = 4
}
if config.InitialHeavy < 1 {
config.InitialHeavy = 1
}
if config.HealthySamples < 1 {
config.HealthySamples = 3
}
active := min(config.InitialActive, config.HardLimit)
heavy := min(config.InitialHeavy, active)
if mode == ModeLegacy {
active = config.HardLimit
heavy = config.HardLimit
}
controller := &Controller{
mode: mode, hardLimit: config.HardLimit,
activeLimit: active, heavyLimit: heavy, claimLimit: active,
healthySamples: config.HealthySamples,
wake: make(chan struct{}, 1),
}
controller.last = controller.snapshotLocked(ResourceSample{SampledAt: time.Now()})
return controller
}
func (c *Controller) Observe(sample ResourceSample) Snapshot {
c.mu.Lock()
defer c.mu.Unlock()
if sample.SampledAt.IsZero() {
sample.SampledAt = time.Now()
}
memory := utilization(sample.MemoryCurrentBytes, sample.MemoryLimitBytes)
database := utilization(int64(sample.DBConnections), int64(sample.DBMaxConnections))
cpu := bounded(sample.CPUUtilization)
state, reason := pressure(memory, cpu, database, sample.CPUThrottled)
if c.mode == ModeLegacy {
c.activeLimit = c.hardLimit
c.heavyLimit = c.hardLimit
state = PressureNormal
reason = "legacy"
} else {
c.adjustLocked(state, memory, cpu, database)
}
c.last = c.snapshotLocked(sample)
c.last.PressureState = state
c.last.PressureReason = reason
c.last.MemoryUtilization = memory
c.last.CPUUtilization = cpu
c.last.DBUtilization = database
c.last.SafeCapacity = c.activeLimit
if state == PressureCritical {
c.last.SafeCapacity = 0
}
c.signalLocked()
return c.last
}
func (c *Controller) SetClaimLimit(limit int) Snapshot {
c.mu.Lock()
defer c.mu.Unlock()
if limit < 0 {
limit = 0
}
c.claimLimit = min(limit, c.hardLimit)
c.last = c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
c.signalLocked()
return c.last
}
func (c *Controller) Snapshot() Snapshot {
c.mu.Lock()
defer c.mu.Unlock()
return c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
}
func (c *Controller) TryStart() (*Lease, bool) {
c.mu.Lock()
defer c.mu.Unlock()
activeLimit := min(c.activeLimit, c.claimLimit)
if activeLimit <= 0 || c.activeLocked() >= activeLimit || c.preparing+c.finalizing >= c.heavyLimit {
return nil, false
}
c.preparing++
c.last = c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
return &Lease{controller: c, phase: PhasePreparing}, true
}
func (l *Lease) EnterWaiting() error {
if l == nil || l.controller == nil {
return nil
}
c := l.controller
c.mu.Lock()
defer c.mu.Unlock()
if l.released {
return ErrReleased
}
if l.phase == PhaseWaitingUpstream {
return nil
}
c.decrementPhaseLocked(l.phase)
c.waiting++
l.phase = PhaseWaitingUpstream
c.last = c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
c.signalLocked()
return nil
}
func (l *Lease) EnterFinalizing(ctx context.Context) error {
if l == nil || l.controller == nil {
return nil
}
c := l.controller
for {
c.mu.Lock()
if l.released {
c.mu.Unlock()
return ErrReleased
}
if l.phase == PhaseFinalizing {
c.mu.Unlock()
return nil
}
// Even under critical pressure, let one submitted task at a time finish
// and release its provider lease instead of deadlocking the drain path.
heavyLimit := max(c.heavyLimit, 1)
if c.preparing+c.finalizing < heavyLimit {
c.decrementPhaseLocked(l.phase)
c.finalizing++
l.phase = PhaseFinalizing
c.last = c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
c.mu.Unlock()
return nil
}
wake := c.wake
c.mu.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-wake:
}
}
}
func (l *Lease) Release() {
if l == nil || l.controller == nil {
return
}
c := l.controller
c.mu.Lock()
defer c.mu.Unlock()
if l.released {
return
}
c.decrementPhaseLocked(l.phase)
l.released = true
c.last = c.snapshotLocked(ResourceSample{SampledAt: c.last.SampledAt})
c.signalLocked()
}
func (l *Lease) Phase() Phase {
if l == nil || l.controller == nil {
return ""
}
l.controller.mu.Lock()
defer l.controller.mu.Unlock()
return l.phase
}
func (c *Controller) adjustLocked(state PressureState, memory, cpu, database float64) {
switch state {
case PressureCritical:
c.healthyCount = 0
c.activeLimit = max(1, c.activeLimit/2)
c.heavyLimit = 1
case PressureBusy:
c.healthyCount = 0
step := max(1, c.activeLimit/4)
c.activeLimit = max(1, c.activeLimit-step)
c.heavyLimit = min(c.heavyLimit, max(1, (c.activeLimit+3)/4))
default:
if memory > .60 || cpu > .65 || database > .65 || c.activeLocked() < min(c.activeLimit, c.claimLimit) {
c.healthyCount = 0
return
}
c.healthyCount++
if c.healthyCount < c.healthySamples {
return
}
c.healthyCount = 0
step := max(1, c.activeLimit/4)
c.activeLimit = min(c.hardLimit, c.activeLimit+step)
c.heavyLimit = min(c.activeLimit, max(1, (c.activeLimit+3)/4))
}
}
func (c *Controller) snapshotLocked(sample ResourceSample) Snapshot {
sampledAt := sample.SampledAt
if sampledAt.IsZero() {
sampledAt = c.last.SampledAt
}
safeCapacity := c.activeLimit
if c.last.PressureState == PressureCritical {
safeCapacity = 0
}
return Snapshot{
Mode: c.mode,
ActiveLimit: c.activeLimit,
HeavyLimit: c.heavyLimit,
ClaimLimit: c.claimLimit,
SafeCapacity: safeCapacity,
ActiveTasks: c.activeLocked(),
PreparingTasks: c.preparing,
WaitingUpstreamTasks: c.waiting,
FinalizingTasks: c.finalizing,
PressureState: c.last.PressureState,
PressureReason: c.last.PressureReason,
MemoryUtilization: c.last.MemoryUtilization,
CPUUtilization: c.last.CPUUtilization,
DBUtilization: c.last.DBUtilization,
SampledAt: sampledAt,
}
}
func (c *Controller) activeLocked() int { return c.preparing + c.waiting + c.finalizing }
func (c *Controller) decrementPhaseLocked(phase Phase) {
switch phase {
case PhasePreparing:
c.preparing = max(0, c.preparing-1)
case PhaseWaitingUpstream:
c.waiting = max(0, c.waiting-1)
case PhaseFinalizing:
c.finalizing = max(0, c.finalizing-1)
}
}
func (c *Controller) signalLocked() {
select {
case c.wake <- struct{}{}:
default:
}
}
func pressure(memory, cpu, database float64, throttled bool) (PressureState, string) {
switch {
case memory >= .90:
return PressureCritical, "memory"
case database >= .90:
return PressureCritical, "database"
case cpu >= .95 && throttled:
return PressureCritical, "cpu_throttled"
case memory >= .75:
return PressureBusy, "memory"
case database >= .80:
return PressureBusy, "database"
case cpu >= .80 || throttled:
return PressureBusy, "cpu"
default:
return PressureNormal, ""
}
}
func utilization(current, limit int64) float64 {
if current <= 0 || limit <= 0 {
return 0
}
return bounded(float64(current) / float64(limit))
}
func bounded(value float64) float64 {
if value < 0 {
return 0
}
if value > 1 {
return 1
}
return value
}