饱和降级:慢下来、排队、告警

更新于 · 在 sijie.xyz 查看原条目 ↗

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

饱和时慢下来、排队、告警,不丢数据、不崩进程、不拖垮访客请求。每种饱和一条 UT,用注入的故障模拟,断言降级行为。已实现的饱和套件覆盖:连接池耗尽、磁盘满且 relay 退避、端点 429 冷却、Meili 挂掉后排空、积压暴增告警、等待者上限、队列饥饿、任务超时。

饱和状态

stateDiagram-v2
  [*] --> normal
  normal --> saturated : pool / workers / rate limit / disk / downstream full
  state saturated {
    [*] --> slow_down
    slow_down --> queue : new jobs deferred, never dropped
    queue --> alert : backlog or oldest age over threshold
  }
  saturated --> normal : pressure eases, backlog drains by itself
  saturated --> writes_refused : disk full, transaction fails
  writes_refused --> normal : space recovered
  note right of writes_refused : business write and event fail together, readable error, never a half-write

场景、行为与 UT 断言

饱和场景 期望行为 UT 断言
连接池耗尽 worker 等连接有超时,到点记为 retryable;访客请求的连接余量始终保留 把池占满后投任务:任务不崩、转 retryable;预留的请求连接仍可用
worker 全忙 新任务排队,不开新 goroutine;各队列互不饥饿 webhook 队列全堵时 index 队列任务仍按时完成
下游限额打满(端点 429) snooze 到 Retry-After,不消耗次数;端点记上 failing_since,新投递等过 5 分钟冷却 连续 429 不会把任务耗到 discarded;冷却期内没有额外请求打出
我们自己的邮件限流命中 snooze 到窗口结束,不丢弃 超过限额的邮件在下个窗口全部发出,数量一致
磁盘满 / Postgres 拒绝写 业务写入和事件一起失败,返回可读错误;relay 退避(2 秒起翻倍,封顶 1 分钟)而不是忙转 模拟写失败:没有“改了没事件”;relay 重试间隔递增
Meili 整个挂了 索引任务退避重试(Meili 客户端自带重试已关);写入不受影响;恢复后积压自动排空 恢复后所有积压任务 completed,没有 discarded(在重试窗口内)
积压暴增(批量导入) 同一批内同主体的索引任务合并(webhook 不合并);未扇出超 1000 条或超 5 分钟触发 events_backlog 任务数 ≤ 上限;告警字段被置位
请求内等待者满了 超过上限的请求直接返回回执,不排队 第 65 个等待者立刻得到 indexed: false
CPU 饱和 / 任务超时 任务硬超时取消、转 retryable;不影响其它队列 超时任务被取消且可救回

每一行背后的机制在兄弟节点里:snooze、冷却与退避见 retry-has-one-owner;队列上限、连接池预算、超时和等待者上限见 concurrency-control;合并与告警阈值见 storage-bounds;业务写入和事件同一事务见 two-sources-of-events。这组 UT 计入 events-test-plan。