机审系统设计:任务编排、租约、未知结果与模型演进
每个内容平均产生 4 个模型子任务,峰值 4000 子任务/秒;供应商只允许 1000 并发——容量结论从算例得出,不从分区数猜测。本设计从持久任务、租约与聚合规则推导机审系统;人工通道的裁决与发布守卫见审核系统设计,租约与fencing的协议细节由租约和 fencing 令牌权威讲授。
本文使用教学工作负载。任务状态与条件更新按 MySQL 8.0 推演,消息语义以 Kafka 3.7.1 为参照;模型服务为外部系统,不纳入数据库事务。
题设与系统边界
每个内容需进行文字风险、图片识别及视频分析;规则决定哪些模型必需,哪些可选。系统需接受重复提交、处理模型超时、接收回调、进行人工兜底,并支持模型灰度。假设峰值 1000 个内容/秒,每个平均产生 4 个模型子任务,任务峰值为 4000/秒;视频采样数量分布很偏,平均扇出不能覆盖极端任务。
区分三个状态:请求已接收、外部模型已处理、业务审核已有结论。HTTP 202 只表示接收意图;模型返回分数也不等于内容已经可发布。模型分数与策略决策分离,便于相同结果按新策略重算,而不伪造一次新的模型执行。
父子任务与状态模型
父任务保存 content_version、review_round、policy_version;子任务保存 model_id/version、输入摘要、external_task_id、attempt、deadline、状态与结果引用。唯一业务约束是同一轮次、模型版本、输入摘要的有效逻辑任务;attempt 是执行尝试,不是可以随意新增业务效果的键。
子任务可走 NEW → READY → RUNNING → SUCCEEDED / RETRY_WAIT / UNKNOWN / FAILED / EXPIRED。UNKNOWN 表示客户端不能证明外部未执行,不能直接等价于 FAILED。回调要识别逻辑任务和外部尝试;任务重建后,旧 attempt 的成功是否可采纳由输入、模型版本和本轮策略判断,而不是一概覆盖或丢弃。
内容接收事务:保存父子任务 + outbox
→ 发布就绪通知(可重复)
→ worker领取短租约
→ 在数据库事务外调用模型
→ 记录结果/未知状态
→ 聚合器核对必需任务集合
→ 用版本守卫推进审核,或转人工队列是唤醒机制,数据库状态是任务事实源。丢一次唤醒可由扫描器恢复;把全部事实只放内存 Future 中,进程重启就失去恢复依据。
领取与 fencing:过期不等于旧进程消失
每次成功领取把 attempt 增加并保存 lease_until。使用数据库时间,减少不同机器壁钟影响。以下教学 SQL 需同时有输入状态/领取条件,生产还应配置合适索引与批量上限:
UPDATE machine_task
SET owner=?, attempt=attempt+1,
lease_until=CURRENT_TIMESTAMP(6)+INTERVAL 30 SECOND,
state='RUNNING'
WHERE task_id=?
AND (state IN ('READY','RETRY_WAIT')
OR (state='RUNNING' AND lease_until<CURRENT_TIMESTAMP(6)));worker 读回自己领取的 attempt,完成写使用 task_id、owner、attempt 与允许状态共同守卫。A 停顿、租约过期、B 接管后,A 恢复不允许覆盖 B 的结果。只在 Redis 中判断租约有效但落库无条件更新,不能拒绝迟到写。
这类 fencing 保护的是检查 token 的资源端。若模型调用本身产生不可逆外部费用或动作,数据库拒绝旧回写并不能撤销外部动作;需模型支持稳定 idempotency key、结果查询或用调用代理协调。不得把本地租约包装成外部 exactly-once。
外部调用的超时与恢复
调用前生成稳定 logical_request_id 并持久化,再发请求。收到明确拒绝可按业务/技术错误分类;网络断开或读超时进入 UNKNOWN。优先按外部任务号查状态,供应商无查询与幂等能力时,应定义可重复推理的成本上限和人工处理路径。
| 时间点 | 可能状态 | 选择 |
|---|---|---|
| 连接建立前失败 | 大概率未到服务,但需按客户端证明范围分析 | 有预算时同键重试 |
| 已写请求、未收到响应 | 对方可能执行 | 查询或保留UNKNOWN |
| 外部完成、数据库写回失败 | 结果存在但本地缺失 | 回调或扫描补齐,勿盲目新建逻辑任务 |
| 租约过期且旧回调晚到 | 已有新attempt | 校验输入与轮次,按策略采纳或记为过期 |
尝试次数应按一个端到端 deadline 与总重试预算控制。若每层都重试 3 次,三层最坏会把一次请求放大到 27 次外部调用。退避加抖动可以分散同步重试,但不能创造模型容量;还需并发限额、入口降载与可选模型降级。
聚合:完成条件是业务规则
聚合器需要必需模型集合、可选集合与策略版本。必需模型全部成功且未超过期限,才能作自动结论;高风险结果可提前转人工,但不能在另一个必需模型尚未完成时提前批准。可选模型超时如何处理需要明确产品约束。
aggregate(snapshot):
if snapshot.version != current_case.version: 记过期
if any mandatory result requires human: 转人工
else if all mandatory succeeded: 按固定policy决策
else if deadline exhausted: 转人工或失败,按策略
else: 等待,可查询当前缺失集合这段是设计伪代码。真正实现要将"读任务集合—判断—推进"放在有版本校验的协议内:例如父任务行版本CAS,结果每次追加后触发重算,终态幂等。一个简单 count 等于总任务数不够,因为重复结果、旧模型和被取消子任务可能污染计数。
源码阅读:异步完成不是可靠任务存储
KafkaProducer 3.7.1 的 send 路径先将记录追加到本地 accumulator,由 Sender 后台发出;调用获得 Future 不表示业务结果已持久化,发送完成回调也只说明所配置 Kafka 确认范围。可沿 KafkaProducer.doSend阅读本地缓冲与异步返回,再对照消息交付语义区分消息与模型副作用。
对于本地 CompletableFuture.allOf,同样不能把线程内完成图当作跨进程调度器。应比较重启后还能恢复哪些状态、取消能否终止远端推理、回调线程是否被数据库访问阻塞。需要持久任务协议时,应以任务表/工作流引擎保存事实,Future 只用于一次进程内等待。
容量与隔离算例
4000 子任务/秒,平均外部耗时 0.8 秒,稳态在途约 3200。若供应商只允许 1000 并发,理论服务能力约 1250/秒;入口持续 4000 就每秒增加约 2750 个积压,10 分钟增加 165 万,不能用无界队列吸收。
分模型设置并发池与排队上限,视频任务不挤占文字短任务。批量推理节省单次开销,却增加等待凑批与尾延迟;应按内容截止时间而非固定 batch size 一刀切。模型限流可用令牌与并发双重限制,容量单位要区分每内容、每帧、每token和每请求。
若故障恢复后到达率为 800/秒、处理率1500/秒,积压42万的理想清理时间为600秒;重试、数据库回写和人工转单会改变真实处理率,清积压需预留在线业务能力。指标关注最老任务年龄、期限违约比例、UNKNOWN 数、回调迟到、每模型成功率与单内容成本。
灰度、质量与可解释性
模型灰度按稳定内容或租户分桶,保存模型与策略版本。影子模型只记录结果,不应同时执行正式发布副作用。比较准确性时需要有代表性的人工标注集、误杀/漏放定义及样本选择偏差;较低接口错误率不能证明识别质量更高。
回滚模型时不能只切路由:旧队列任务、缓存结果、输入特征版本与结果解释也需兼容。为不可兼容输入创建新任务或迁移流程,保留可追溯旧记录。敏感内容与模型日志采用最小权限和限期保留,不把完整内容写进普通trace。
实验与验收产物
准备A/B两个worker与一个可控制延迟的假模型:A领取后暂停,B在租约过期后领取并完成,再恢复A,验证旧attempt写失败。然后令模型已处理但隐藏响应,断言本地进入UNKNOWN且同逻辑请求不新增业务决策。最后以重复回调和乱序结果驱动聚合器,检查未满足必需集合时不得批准。
提交事件时间线、数据库状态快照、条件更新行数、外部请求键及一次最小恢复步骤。这些是预期验证步骤,不宣称已经运行多节点Kafka或真实模型服务。
模型演进的容量重算规则
本页算过 4,000 子任务/秒与供应商 1,000 并发下的积压,但模型会演进:上线两个模型变体做灰度、输入特征版本变化需要重算、供应商并发配额调整。任一因素变化都要重算容量矩阵,否则灰度本身会把系统压垮。
三因素乘法:模型变体数、输入特征重生成比例、供应商并发上限。这里按最坏情况算:两个变体各自全量双跑(影子比对的常见形态),20% 输入需要特征重算,有效需求 4,000×2×1.2=9,600/秒;若灰度只按 10% 分流、不做全量双跑,放大约为 1+0.1+0.2 倍、即 5,200/秒,仍超能力。供应商 1,000 并发、单次 0.8 秒,基础能力 1,000/0.8=1,250/秒;净积压 9,600−1,250=8,350/秒。灰度本意是"只放 10% 流量",但只要两个变体同时全量双跑(含影子比对),再叠加特征重算,实际负载就是原来的 2.4 倍——放大来自双跑与重算,不是流量百分比本身。
重算规则要写进灰度流程:任何新模型上线前,必须给出"变体数 × 特征重算比例 × 新并发上限"下的容量矩阵与积压曲线;灰度比例不能只看流量百分比,要看有效需求。回滚同样要重算:回滚不是简单切路由,旧变体的队列任务、缓存结果与特征版本仍要兼容。
矩阵的另一种用法是容量预算:若供应商只保证 1,000 并发,那么变体数超过 1 或特征重算超过 20% 就需要额外配额或排队上限;否则积压会在几分钟内把期限全部击穿。容量数字不是"跑一次压测",是随版本变化的表。
执行 python3 labs/system-design/design_review_models.py machine_capacity_recalc:预期输出 subtasks_per_s=4000、base_capacity_per_s=1250.0、effective_demand_per_s=9600.0、backlog_per_s=8350.0。脚本只做乘法与除法,不模拟真实供应商限流。
两个合理选择:A 灰度只按流量百分比切分,简单但忽略变体与特征放大;B 按有效需求重算容量矩阵,复杂但能防止灰度压垮系统。本例选 B,并规定容量矩阵是灰度发布的准入材料,缺矩阵不允许放量。
证据留存与可解释性成本
本页讲了"合规要求审核结论可解释:保存输入摘要、模型版本、策略版本与证据引用",但没有算这笔证据要花多少钱、保留多久、怎么查询。证据留存是一个独立的数据产品,有自己的容量与访问控制。
输入:每天 864 万次推理、每次证据 64KiB(输入摘要、分数、模型与策略版本、证据引用),日增约 527.3GiB;保留 30 天约 15.45TiB;按每 TiB 月 120 元计,存储约 1,853.9 元/月;查询每天 5 万次、每次 0.0002 元,查询约 300 元/月;合计约 2,153.9 元/月。数字不大,但这是"每次推理都留证据"的基线;视频帧级证据或全量日志会让它上升一个量级。
留存设计三件事:字段最小化(不存原始媒体与凭证,只存摘要与引用)、保留期分层(近期可查询、远期归档、超期销毁)、访问控制(谁能查、查询是否审计)。可解释性不是"把所有东西存下来",而是"在需要时能回答为什么"——字段最小化与保留期分层正是让这个回答可持续。
查询成本常被忽略:合规审计、申诉处理、模型复盘都会查询证据;没有索引与查询预算,证据库会变成慢查询与高账单的来源。查询接口要有版本与限流,不能直接开一个 SQL 入口。
执行 python3 labs/system-design/design_review_models.py evidence_retention_cost:预期输出 daily_gib=527.3、retention_tib=15.45、存储约 1,853.9、查询 300、月合计约 2,153.9。脚本只做容量与成本算术,价格与证据大小为教学输入。
两个合理选择:A 全量留存、永久保留,简单但成本与合规风险都高;B 字段最小化 + 30 天可查询 + 归档与销毁,复杂但可持续。本例选 B,并规定保留期变更必须同时重算存储与查询预算。
架构师变化条件
把模型从单一供应商改为多供应商分片路由:容量池、UNKNOWN 查询路径与幂等键按供应商区分,回滚路由要兼容各队列旧任务。视频分析从抽帧升级为全量逐帧:容量单位从每内容变为每帧,峰值扇出分布重算。合规要求审核结论可解释:保存输入摘要、模型版本、策略版本与证据引用成为不变量的一部分。每次变化重算"峰值×扇出×耗时 vs 供应商并发"矩阵。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。