存储边界:每种失控都有硬上限
上级:events
状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。
队列在数据库里,最大的风险是存储失控。原则:每一种失控都有硬上限,并且看得见,不靠“应该会清理”。
每一行什么时候被删掉
失控点与控制
| 失控点 | 怎么会失控 | 控制 | 怎么保证 / 看见 |
|---|---|---|---|
events 只增不减 | 没人删;或 relay 卡住、未扇出的行堆积 | 周期任务 events retention 每小时删“已扇出且 ≥ 7 天”的行。未扇出和已隔离的行保留:它们就是积压。 | 总览显示积压数、隔离数、最老未扇出事件的年龄和表大小。未扇出超过 1000 条或最老超过 5 分钟告警 events_backlog;有隔离行告警 events_poisoned。告警在面板上,不发邮件:邮件本身就可能是失败的那一方。 |
river_job 完成行堆积 | 完成的任务行留在表里 | River 清理服务,由选出的主节点按默认值跑:completed 24 小时、discarded 7 天 | 总览显示表大小;存在 discarded 任务时告警 jobs_discarded |
| 死端点重试堆积 | 对方网站长期挂掉 | MaxAttempts 18;failing_since 有值期间新投递排到 5 分钟后;连续失败 5 天自动停用,停用后不再排新投递 | 端点状态与停用原因在后台可见 |
| 批量操作扇出爆炸 | Obsidian 一次导入 2000 篇 | relay 每轮最多 200 行;corpus.index 在同一批内按主体合并。webhook 不合并也不防抖:每条事件是一件事实,给每个订阅它的端点投一次 | e2e events-bulk-import-bound |
| 空更新也触发事件 | 只刷了 updated_at | 触发器加 WHEN:关心的列真变了才写 | spec 用“哨兵之前的事件序列”正向断言 |
| 一条坏事件卡死流水线 | relay 处理某条反复出错 | 失败的那批逐行重试,一行出错不阻塞其它行;同一行失败 5 次就隔离并告警;投递失败交给各自任务重试 | 同上,看积压数 |
| 死元组膨胀 | 队列表高频更新,MVCC 旧版本行等 vacuum | events 调低了 autovacuum 阈值(scale factor 0.02)。river_job 保持 River 的设置,因为那份 DDL 归 River。量本来就是一天几千行 | 总览显示表大小 |
积压数、最老事件年龄、表大小和告警都显示在 tasks-panel 的总览里。
先例
访客流量表原本也会无限增长,现在靠每 24 小时一次的保留期任务(visitor traffic retention)兜住;事件和任务表照这个模式来。
换别的方案也躲不开
- Redis 队列同样会堆积,且我们的 Redis 满了是静默淘汰。
- Kafka 自带按时间 / 大小保留,代价是多运维一个服务。
验收
积压 / 表大小在后台可见(e2e tasks-panel、tasks-panel-more);批量导入的上限有自己的 e2e(events-bulk-import-bound);保留期清理和积压告警有 UT。见 events-roadmap · events-test-plan。