返回文章列表

AiVedio:整体系统架构

为 AI 生成视频与在线视频剪辑平台建立控制面、执行面、媒体数据面、供应商适配、异步事件、对象存储和多租户调度的整体架构。

第 03 章:整体系统架构

本章定位:为“AI 生成视频与在线视频剪辑平台”建立一套可落地、可扩展、可面试表达的总体架构。重点不是罗列服务名称,而是解释每个边界为什么存在、哪些数据必须强一致、哪些流程只能最终一致,以及第三方模型和大媒体文件如何被安全地纳入统一工作流。


3.1 本章要解决的业务问题

AI 视频平台表面上是“输入提示词,返回视频”,工程上却同时具备以下特征:

  1. 请求短、任务长:创建任务的 HTTP 请求应在秒级返回,但模型生成可能持续数分钟。
  2. 单次执行有真实成本:一次重复提交可能造成重复生成、重复供应商扣费和用户重复计费。
  3. 第三方结果弱确定:会出现限流、超时、回调丢失、回调乱序、任务状态未知、临时结果地址过期等情况。
  4. 媒体体积远大于元数据:参考视频、生成结果、代理视频、HLS 切片和最终成品不应穿过普通 Go API 服务。
  5. 资源类型差异显著:API 服务主要消耗连接和数据库;媒体处理消耗 CPU、GPU、显存和临时磁盘;供应商调用受外部并发配额约束。
  6. 业务事实跨多个阶段形成:任务状态、额度预占、供应商作业、媒体资产、项目版本和最终渲染不能由单一缓存或单条消息决定。
  7. 系统需要支持多租户与多供应商:普通用户、付费用户和企业租户需要不同的额度、优先级、隔离级别和合规路由。
  8. 生成只是前半程:结果还要回源、探测、转码、审核、分发、在线编辑和最终渲染。

因此,本章要形成的架构结论是:

平台不是一个“同步调用模型 API”的应用,而是一个由数据库事实、异步事件、供应商执行和媒体流水线共同组成的长任务工作流系统。

本章的交付目标包括:

  • 明确控制面、执行面和媒体数据面的边界。
  • 明确每个逻辑组件的单一职责和禁止承担的职责。
  • 明确同步调用、异步消息和媒体直传分别用于什么场景。
  • 明确 PostgreSQL、Redis、RocketMQ、对象存储和 CDN 的权威边界。
  • 给出任务创建、供应商执行、结果回源、媒体处理和通知的完整调用关系。
  • 为后续状态机、数据库、消息队列、调度、媒体处理和高可用章节建立统一上下文。

3.2 核心设计原则

3.2.1 控制面与媒体数据面分离

控制面处理小而关键的数据:

  • 身份、租户和权限。
  • 任务创建、取消、查询和状态转换。
  • 资产元数据、项目版本和时间轴 JSON。
  • 调度、供应商路由、额度、计费和审核结论。
  • 消息事件、审计记录和通知元数据。

媒体数据面处理大字节流:

  • 图片、音频、视频上传。
  • 第三方生成结果下载。
  • 转码、切片、缩略图、波形和代理视频。
  • 最终渲染、中间文件和成品分发。

核心规则是:

Go 业务服务传递对象标识、元数据和短期授权,不代理传输整段视频。

客户端应通过预签名地址直接上传对象存储;大文件可使用分片上传,失败时只重传失败分片,而不是从头上传。5

3.2.2 PostgreSQL 管事实,Redis 管速度,RocketMQ 管异步,对象存储管字节

基础组件应承担的职责不应承担的职责
PostgreSQL任务、资产、供应商作业、项目版本、计费流水、Outbox、审核记录等事实数据高频百分比进度、大文件内容、无界日志流
RedisAPI 限流、短期缓存、并发槽位、租约、热点状态、SSE/WebSocket 通知加速余额、核心任务状态、唯一任务记录的权威来源
RocketMQ长任务解耦、削峰、消费重试、延迟调度、死信和跨组件事件传递天然 exactly-once、业务状态事实、媒体文件传输
对象存储原始素材、供应商回源结果、代理文件、切片、缩略图、中间文件和成品复杂事务、任务状态机、租户业务授权判断
CDN边缘缓存、范围请求、播放分发、签名访问永久业务授权事实、源文件唯一副本

Redis Pub/Sub 是至多一次投递,订阅者断线期间的消息可能永久丢失,因此它适合实时提示,不适合作为任务完成事件的唯一事实来源。2

RocketMQ 在消费失败或消费确认超时时会触发重新投递,并可在超过重试次数后进入死信队列;这意味着消费者必须按“可能重复收到”设计。3

3.2.3 同步只处理有界短操作,异步承载长操作

适合同步处理:

  • 鉴权、租户解析和参数校验。
  • 查询资产元数据和任务快照。
  • 费用估算、余额检查和额度预占。
  • 输入侧快速审核。
  • 获取预签名上传或播放地址。

适合异步处理:

  • 模型排队与提交。
  • 供应商状态轮询。
  • 结果回源。
  • FFmpeg/GPU 媒体处理。
  • 最终渲染。
  • 通知扇出、对账、补偿和清理。

判断标准不是“代码是否容易写”,而是:

  • 操作是否可能超过 API 超时时间。
  • 是否需要独立重试。
  • 是否受外部配额或资源槽位限制。
  • 是否应在系统高压时被排队或降级。
  • 是否会因失败而阻塞数据库事务。

3.2.4 用状态机驱动工作流,而不是依赖消息到达顺序

消息可能重复、延迟或乱序,回调和轮询也可能同时到达。系统不能假设:

  • “先发送的消息一定先消费”。
  • “回调一定只到一次”。
  • “取消请求一定早于成功回调生效”。
  • “Redis 中的进度一定比 PostgreSQL 新”。

每次状态变更都必须经过允许的状态转换表,并使用数据库条件更新或版本号进行并发控制。消息只表达“发生了什么”或“请尝试做什么”,最终是否允许落库由当前任务事实决定。

3.2.5 不追求基础设施层 exactly-once,追求业务上的一次效果

业务一次效果由多层机制共同实现:

  • API Idempotency-Key
  • PostgreSQL 唯一约束。
  • Outbox 与 Inbox 去重。
  • 状态版本号和条件更新。
  • 供应商幂等请求键。
  • 账本唯一业务键。
  • 确定性的对象键和媒体作业键。
  • 对账与补偿任务。

PostgreSQL 唯一约束可保证一列或一组列在表内唯一,适合承载任务幂等键、供应商任务标识和账本业务键等最终防线。1

3.2.6 第三方供应商必须通过适配层接入

业务层不直接散落 if provider == ...。统一适配接口至少包括:

ValidateCapability
PrepareInput
Submit
Query
Cancel
ParseCallback
ClassifyError
FetchOutput
EstimateCost

适配层输出统一结构:

provider_job_id
normalized_status
progress
retry_class
provider_error_code
output_assets
estimated_cost
actual_cost
raw_response_ref

适配层负责“协议差异”,调度器负责“选谁执行”,任务服务负责“业务状态”,计费服务负责“钱”,不能混为一个巨型 Provider Service。

3.2.7 逻辑组件不等于必须独立部署

本章列出的服务首先是逻辑边界。是否拆成独立进程,应由以下条件决定:

  • 是否需要独立扩缩容。
  • 是否需要故障隔离。
  • 是否有明显不同的安全边界。
  • 是否有不同的发布频率或团队归属。
  • 是否会破坏关键本地事务。

早期阶段可以将 Generation、Asset Metadata、Billing Reservation 和 Moderation Orchestration 作为同一控制面应用中的模块;Scheduler、Provider Worker、Callback/Polling、Media Worker、Render Worker 和 Notification Gateway 则更适合独立部署。这样既保留清晰边界,又避免过早引入大量跨服务事务。

3.2.8 架构必须内置对账和恢复能力

长任务系统不能只设计正常路径。必须能回答:

  • 数据库有任务但 MQ 没消息怎么办?
  • MQ 重投后会不会再次调用供应商?
  • 供应商已受理但本地超时怎么办?
  • 回调丢失后如何恢复?
  • Redis 并发槽位泄漏如何修复?
  • 输出 URL 过期前没有回源怎么办?
  • 供应商扣费与用户账本不一致怎么办?

因此,Outbox Relay、轮询、过期任务扫描、账单对账、资产完整性扫描和人工重放工具都属于主架构,而不是上线后的补丁。

3.2.9 必须坚持的架构不变量

  1. 任何核心业务对象都能仅依靠 PostgreSQL 重建当前事实。
  2. 任何消息都允许重复消费,消费者不能依赖“只来一次”。
  3. 任何第三方 Submit 超时都不能被默认解释为失败。
  4. 任何媒体对象在业务表中保存的是内部 asset_id/object_key,不是临时公开 URL。
  5. 任何计费动作都有唯一业务键,并以追加账本记录表达。
  6. 终态不能被旧事件回退到非终态。
  7. 通知丢失不影响任务事实;客户端重连后重新读取快照。
  8. API、供应商调用、媒体处理和渲染使用不同的并发池和资源配额。

3.3 详细架构与组件职责

3.3.1 完整文字架构图

┌────────────────────────────── 客户端层 ──────────────────────────────┐
│ Browser / Mobile / Open API Client                                  │
│  ├─ HTTPS:创建任务、查询状态、编辑项目                              │
│  ├─ SSE/WebSocket:接收状态提示                                      │
│  ├─ Presigned PUT:直接上传对象存储                                  │
│  └─ CDN GET/Range:播放代理视频、HLS 和最终成品                       │
└────────────────────────────────┬────────────────────────────────────┘


┌────────────────────────────── 边缘接入层 ────────────────────────────┐
│ API Gateway / WAF / Auth / Tenant Context / Rate Limit              │
│ Notification Gateway(SSE/WebSocket)                               │
└───────────────┬───────────────────────────────────┬──────────────────┘
                │短同步请求                          │实时通知
                ▼                                    ▲
┌────────────────────────────── 控制面 ────────────────────────────────┐
│ Generation Service                                                   │
│  ├─ create/query/cancel                                              │
│  ├─ 参数归一化、状态机命令                                           │
│  └─ task + reservation + outbox 事务                                 │
│                                                                       │
│ Asset Service       Billing Service       Moderation Service          │
│  ├─ 资产元数据       ├─ quote/reserve      ├─ 输入审核                │
│  ├─ 预签名地址       ├─ settle/release     ├─ 输出审核                │
│  └─ 租户 ACL         └─ refund/reconcile   └─ 策略版本                │
│                                                                       │
│ PostgreSQL:task / asset / ledger / provider_job / outbox / audit    │
│ Redis:限流 / 槽位 / 租约 / 热点进度 / 通知加速                      │
└──────────────────────────┬───────────────────────────────────────────┘
                           │ Outbox Relay

┌────────────────────────── RocketMQ 异步总线 ─────────────────────────┐
│ generation.dispatch │ provider.poll │ output.fetch │ media.process   │
│ render.execute      │ notify        │ billing      │ retry / DLQ     │
└───────────────┬───────────────────┬───────────────────┬───────────────┘
                │                   │                   │
                ▼                   ▼                   ▼
┌──────────────────────┐  ┌──────────────────────┐  ┌──────────────────┐
│ Scheduler            │  │ Callback/Polling     │  │ Render Service   │
│ ├─ 租户公平           │  │ ├─ 签名验证           │  │ ├─ 时间轴校验      │
│ ├─ 优先级与 aging     │  │ ├─ 回调归档/去重       │  │ ├─ 编译 Render DAG │
│ ├─ 平台/租户/模型槽位 │  │ ├─ 状态查询与退避       │  │ └─ 创建渲染任务    │
│ └─ 供应商路由         │  │ └─ 归一化状态事件       │  └────────┬─────────┘
└──────────┬───────────┘  └──────────┬───────────┘           │
           ▼                         │                       ▼
┌──────────────────────┐             │              ┌──────────────────┐
│ Provider Adapter     │◄────────────┘              │ Render Worker    │
│ ├─ Submit/Query      │                            │ FFmpeg/GPU       │
│ ├─ Cancel/Callback   │                            └────────┬─────────┘
│ └─ Error Normalize   │                                     │
└──────────┬───────────┘                                     │
           ▼                                                  │
┌──────────────────────── 第三方/自建模型执行面 ───────────────┐         │
│ Runway / Veo / 其他供应商 / 自建推理集群                    │         │
└──────────┬───────────────────────────────────────────────────┘         │
           │ 临时输出 URL / 对象位置                                    │
           ▼                                                            │
┌──────────────────────────── 媒体数据面 ────────────────────────────────┐
│ Output Fetch Worker                                                    │
│  ├─ 安全下载、限速、校验、checksum                                    │
│  └─ 写入自有对象存储                                                   │
│            │                                                           │
│            ▼                                                           │
│ Media Worker                                                           │
│  ├─ ffprobe / 转码 / HLS / 缩略图 / 波形 / 代理视频                    │
│  └─ 输出复审                                                           │
│            │                                                           │
│            ▼                                                           │
│ Object Storage ───────────────► CDN ───────────────► Browser           │
│ 原始素材/回源结果/变体/中间文件/最终成品                              │
└────────────────────────────────────────────────────────────────────────┘

横切能力:Trace、Metrics、Logs、Audit、Secrets、Feature Flags、Reconcile

这张图强调三点:

  1. 浏览器上传和播放走对象存储/CDN,不走 Generation Service。
  2. 供应商提交、轮询、回调、回源和媒体处理都不占用用户请求线程。
  3. 所有异步组件最终都通过受控状态转换回到 PostgreSQL 事实,而不是互相覆盖状态。

3.3.2 控制面、执行面与数据面

平面典型组件主要数据设计重点
边缘接入层API Gateway、Notification GatewayToken、租户上下文、请求元数据、连接状态认证、限流、防滥用、连接管理
控制面Generation、Asset、Billing、Moderation、Scheduler任务、资产元数据、额度、策略、路由决策强事务边界、状态机、幂等、公平调度
供应商执行面Provider Adapter、Callback Gateway、Polling Serviceprovider_job_id、供应商状态、原始错误弱确定性处理、协议归一化、熔断降级
媒体数据面Output Fetch、Media Worker、Render Worker、对象存储、CDN视频字节、切片、缩略图、中间文件流式 IO、CPU/GPU 隔离、临时盘、生命周期
横切治理面观测、审计、密钥、对账、人工重放trace、事件、账单、操作记录可排障、可恢复、可证明

3.3.3 组件职责清单

1. API Gateway

负责:

  • TLS 终止、WAF、身份认证和租户上下文注入。
  • API 级速率限制、请求体上限和基础 schema 校验。
  • 透传并规范化 Idempotency-Keytrace_id、客户端版本。
  • 路由到 Generation、Asset、Render 等控制面服务。

不负责:

  • 不创建业务任务记录。
  • 不持有用户余额事实。
  • 不代理大文件上传、下载或供应商输出回源。
  • 不通过网关重试非幂等的生成请求。

2. Generation Service

负责:

  • 创建、查询、取消生成任务。
  • 参数归一化、能力初筛和引用资产检查。
  • 编排输入审核、费用估算和额度预占。
  • 在一个本地事务中写入任务、额度预占流水和 Outbox。
  • 作为任务状态机命令的唯一业务入口,执行条件状态转换。
  • 向客户端返回稳定的内部 task_id

不负责:

  • 不在 HTTP 请求中同步等待供应商完成。
  • 不直接执行 FFmpeg。
  • 不把供应商原始错误和临时输出 URL 直接返回用户。
  • 不把高频进度每 1% 写入 PostgreSQL。

3. Asset Service

负责:

  • 创建资产占位记录和对象键。
  • 生成带租户范围、过期时间、大小约束的预签名上传地址。
  • 上传完成确认、对象存在性检查、checksum 和媒体元数据登记。
  • 管理原始资产与代理、缩略图、波形、HLS 等变体的关系。
  • 执行租户 ACL、生命周期和删除策略。

不负责:

  • 不让素材先进入 Go 内存再转发对象存储。
  • 不因客户端声明 Content-Type 就信任文件类型。
  • 不把对象存储 bucket/key 直接作为跨租户授权依据。

预签名 URL 能在不向客户端暴露存储凭据的情况下授权特定对象操作;该授权继承签发者的权限,因此对象键、有效期和条件必须收紧。5

4. Scheduler

负责:

  • 从待调度事件中选择可执行任务。
  • 实施平台、租户、供应商和模型四级并发准入。
  • 按租户公平、付费等级、优先级和等待时长进行排序。
  • 根据模型能力、健康度、延迟、成本、地区和合规要求选择供应商。
  • 获取短期调度租约,并创建新的 task_attempt
  • 在容量不足时延迟重投或保持排队,而不是无限拉取。

不负责:

  • 不把 Redis 槽位数当作最终任务状态。
  • 不直接结算用户费用。
  • 不在一次消费中无限等待供应商。
  • 不保证全局严格 FIFO。

5. Provider Adapter

负责:

  • 将内部标准请求转换为供应商参数。
  • 实现提交、查询、取消、回调解析和错误分类。
  • 保存供应商原始请求标识、响应摘要和版本信息。
  • 将供应商状态映射为统一内部状态。
  • 在供应商支持时传递稳定幂等键。

不负责:

  • 不决定租户优先级。
  • 不直接修改余额。
  • 不自行将未知超时判断为失败并盲目重提。
  • 不执行跨供应商业务补偿。

6. Callback Gateway

负责:

  • 提供公网回调入口。
  • 验证签名、时间戳、nonce、来源配置和防重放条件。
  • 原样归档必要的回调摘要或对象引用。
  • 通过供应商事件 ID 或内容哈希去重。
  • 快速返回成功响应,再异步解析和更新状态。

不负责:

  • 不在回调线程中下载视频或转码。
  • 不因为回调携带 SUCCEEDED 就无条件覆盖本地终态。
  • 不把回调作为唯一完成检测机制。

7. Polling Service

负责:

  • 对没有回调、回调不稳定或状态未知的任务执行查询。
  • 使用指数退避、抖动和供应商级 QPS 限制。
  • 根据任务阶段动态调整轮询频率。
  • 终态后停止轮询,并回收计划记录。
  • 扫描长时间无变化的任务,触发对账或人工检查。

不负责:

  • 不通过轮询再次提交生成。
  • 不以高频轮询伪造实时进度。
  • 不在多个实例之间无租约地重复轮询同一任务。

8. Output Fetch Worker

负责:

  • 在供应商任务成功后尽快回源临时结果。
  • 使用流式 IO 下载,限制响应大小、超时、速率和重定向次数。
  • 验证协议、主机、解析 IP、MIME、magic number 和 checksum。
  • 将结果写入自有对象存储并创建内部 Asset。
  • 记录来源、下载时间、供应商作业和完整性信息。

不负责:

  • 不把供应商临时 URL 直接发给前端。
  • 不允许任意 URL 下载,避免 SSRF。
  • 不一次性 ReadAll 大视频到内存。

部分供应商明确说明生成结果 URL 是临时的,应下载到自有存储而不是直接暴露给产品用户。8

9. Media Worker

负责:

  • 使用 ffprobe 获取编码、时长、帧率、分辨率和音轨信息。
  • 生成标准播放文件、代理视频、缩略图、波形和 HLS/CMAF 切片。
  • 执行输出侧内容审核、水印或格式规范化。
  • 将每个变体登记为独立 Asset,并关联源资产。
  • 在隔离容器中限制 CPU、内存、GPU、临时盘和执行时长。

不负责:

  • 不与 API 服务共享无上限的 CPU/内存池。
  • 不拼接未经转义的 shell 命令。
  • 不把本地临时盘当作永久存储。

10. Render Service

负责:

  • 接收项目版本和时间轴快照。
  • 校验引用资产和编辑参数。
  • 将时间轴编译为可执行 Render DAG。
  • 生成确定性的渲染作业键和缓存键。
  • 调度 FFmpeg/GPU Render Worker。
  • 跟踪最终成品、失败节点和可复用中间结果。

不负责:

  • 不读取“正在变化的项目草稿”直接渲染。
  • 不让浏览器预览结果成为最终成品事实。
  • 不在 API Pod 内启动长时间 FFmpeg 进程。

11. Notification Gateway

负责:

  • 管理 SSE/WebSocket 长连接。
  • 按用户、租户和项目进行消息扇出。
  • 将状态变化、排队提示和资产就绪提示发送给在线客户端。
  • 支持连接心跳、限速、背压和多实例路由。
  • 客户端重连时引导其重新读取任务快照。

不负责:

  • 不作为任务事实源。
  • 不保证断线期间每条 UI 提示都可重放。
  • 不因通知失败回滚任务成功状态。

12. Billing Service

负责:

  • 估价、额度预占、实际结算、释放、退款和补偿。
  • 使用追加式账本记录每个资金动作。
  • 区分供应商成本、平台内部成本和用户收费。
  • 按供应商账单、任务记录和用户账本执行每日对账。
  • 对每个业务动作使用唯一业务键。

不负责:

  • 不用 Redis 余额作为最终余额。
  • 不覆盖或删除历史账本来“修正余额”。
  • 不因消息重投重复结算。

13. Moderation Service

负责:

  • 对提示词、参考素材和生成结果执行审核。
  • 输出标准化决策:允许、拒绝、人工复核、降级模型或加水印。
  • 记录策略版本、模型版本、命中规则和证据引用。
  • 区分不可重试的内容拒绝与可重试的审核基础设施故障。

不负责:

  • 不把供应商的单一审核结果当作平台完整政策。
  • 不对明确内容违规无限自动重试。
  • 不在普通业务日志中写入完整敏感提示词或媒体内容。

14. Outbox Relay 与 Inbox 去重中间件

负责:

  • Relay 扫描已提交但未发布的 Outbox 事件并发送 RocketMQ。
  • 发送成功后更新发布状态;发送结果未知时允许再次发送。
  • 消费者在本地事务中记录 (consumer_name, event_id),防止同一业务事件重复生效。
  • 提供死信重放、事件追踪和 schema 版本管理。

不负责:

  • 不把“MQ 返回成功”解释为下游业务已完成。
  • 不删除尚未被审计或超过保留期的事件证据。

3.3.4 推荐的物理部署方式

逻辑组件可以按资源与故障域分为以下部署单元:

部署单元可包含的逻辑模块拆分原因
control-apiGeneration、Asset Metadata、Billing Reservation、Moderation Orchestration共享低延迟事务边界,避免任务与预占跨服务不一致
outbox-relayOutbox 扫描与 RocketMQ Producer独立扩缩、可暂停、便于监控积压
scheduler-workerScheduler、Provider Adapter Submit受供应商配额约束,与 API 资源隔离
provider-state-workerCallback Processor、Polling Service、状态归一化独立处理外部回调和查询压力
output-fetch-worker安全下载与对象存储写入网络带宽、连接和 egress 策略独立
media-workerffprobe、转码、切片、缩略图、输出审核CPU/GPU/临时盘重负载
render-api时间轴校验、Render DAG、作业创建与编辑器协议演进一致
render-workerFFmpeg/GPU 渲染执行高资源、长时任务、独立队列
notification-gatewaySSE/WebSocket大量长连接、网络型负载

这里最关键的取舍是:

任务创建、额度预占和 Outbox 写入最好处在同一个 PostgreSQL 本地事务中。

若一开始就把 Billing 拆成独立数据库微服务,则只能通过 Saga 实现:先预占,再创建任务;创建失败后释放预占;任何中间故障都需要补偿和对账。对于团队规模和业务复杂度尚未要求独立账务域的阶段,保留一个明确的本地事务边界通常更稳妥。后续若因组织、安全或合规必须拆分,再接受 Saga 带来的中间状态和运维成本。

3.3.5 服务调用关系

调用方被调用方方式典型超时/失败策略
ClientAPI GatewayHTTPS客户端可按相同幂等键重试创建请求
ClientObject StoragePresigned PUT/Multipart分片独立重试;完成后调用 Asset finalize
ClientCDNHTTPS Range/HLSCDN 失败可回源或切备用域名
GatewayGeneration/Asset/Render同步 HTTP/gRPC只做短操作;设置严格 deadline
GenerationPostgreSQL本地事务失败整体回滚,不调用供应商
GenerationModeration同步短调用或内部模块超时按政策 fail-closed 或转待审核
Outbox RelayRocketMQ异步发送发送结果未知时允许重复发送
SchedulerRedis槽位/租约Redis 故障时采取保守准入或暂停新调度
SchedulerProvider Adapter内部调用创建 attempt 后提交;记录提交未知状态
Provider AdapterProvider APIHTTPSSubmitQuery 使用不同重试策略
ProviderCallback GatewayHTTPS Webhook快速验签、落事件、返回 2xx
Polling ServiceProvider APIHTTPS GET指数退避、抖动、供应商级限流
State HandlerPostgreSQL条件更新事务终态保护、版本号、事件记录
State HandlerRocketMQOutbox 事件驱动 output fetch、media、notify
Output FetchProvider URL/Object流式 HTTPSSSRF 防护、大小限制、可恢复重试
Media/Render WorkerObject Storage流式读写临时盘配额、分阶段 checkpoint
Notification GatewayClientSSE/WebSocket丢失可接受;重连后读取任务快照

3.4 文字版时序图

下面展示从参考素材上传到视频可播放的主流程。为突出架构关系,计费结算和渲染导出在主链路后补充说明。

Browser

  │ 1. POST /assets/uploads

API Gateway ──► Asset Service

                  │ 2. 创建 asset 占位,生成 object_key
                  │ 3. 返回 presigned multipart 信息

Browser ─────────────────────────► Object Storage
  │                                  │
  │ 4. 直接分片上传                   │
  │ 5. POST /assets/{id}/complete     │
  ▼                                  │
Asset Service ── HEAD/checksum ──────┘

  │ 6. 标记素材 READY

Browser

  │ 7. POST /generation-tasks + Idempotency-Key

API Gateway ──► Generation Service

                  ├─ 8. 权限、参数、资产状态检查
                  ├─ 9. Moderation 输入审核
                  ├─ 10. Billing 估价与额度预占

                  ├─ 11. PostgreSQL 单事务:
                  │      generation_task
                  │      credit_ledger(RESERVE)
                  │      outbox_event(TaskQueued)

                  └─ 12. 返回 task_id 与 QUEUED

Outbox Relay
  │ 13. 发布 TaskQueued

RocketMQ


Scheduler
  ├─ 14. 租户公平与优先级选择
  ├─ 15. 获取 provider/model 槽位
  ├─ 16. 创建 task_attempt,状态改为 DISPATCHING

Provider Adapter
  │ 17. Submit(provider_request_key)

Third-party Provider

  ├─ 18a. 正常返回 provider_job_id
  │       Adapter 落 provider_job,任务转 SUBMITTED/RUNNING

  └─ 18b. 或 HTTP 超时、结果未知
          任务转 SUBMIT_UNKNOWN,进入查询/对账,不盲目重提

Third-party Provider

  ├─ 19a. Callback ─────────► Callback Gateway
  │                             ├─ 验签、防重放、归档
  │                             └─ 发布 ProviderStatusObserved

  └─ 19b. 无可靠回调时 ◄──── Polling Service 定时 Query

State Handler
  ├─ 20. 按状态机和 version 条件更新 PostgreSQL
  ├─ 21. 若成功,写 Outbox(OutputFetchRequested)
  └─ 22. 若失败,分类为可重试、最终失败或待人工确认

RocketMQ
  │ 23. OutputFetchRequested

Output Fetch Worker
  ├─ 24. 安全流式下载供应商临时结果
  ├─ 25. checksum、媒体初检
  ├─ 26. 写入自有对象存储并登记源资产
  └─ 27. 发布 MediaProcessRequested

Media Worker
  ├─ 28. ffprobe、转码、代理视频、HLS、缩略图、波形
  ├─ 29. 输出侧审核
  ├─ 30. 登记 asset variants
  └─ 31. 状态转 SUCCEEDED/PLAYABLE

Billing Service
  ├─ 32. 按业务政策结算、释放差额或退款
  └─ 33. 写唯一业务键账本

Notification Gateway
  │ 34. 推送“视频可播放”提示

Browser
  │ 35. 重新 GET task 快照,获取内部 asset_id
  │ 36. 获取 CDN 签名播放地址

CDN ──► 播放代理视频/HLS

最终导出是另一条异步链路:

Browser 提交 project_revision
→ Render Service 校验并编译 Render DAG
→ PostgreSQL 写 render_job + outbox
→ RocketMQ render.execute
→ Render Worker 读取对象存储素材并执行 FFmpeg/GPU
→ 成品写对象存储
→ CDN 分发
→ Notification Gateway 提示导出完成

3.5 关键数据结构、数据库表与消息字段

3.5.1 核心数据库表

generation_tasks

id                    UUID PK
tenant_id             UUID NOT NULL
user_id               UUID NOT NULL
project_id            UUID NULL
idempotency_key       VARCHAR NOT NULL
mode                  VARCHAR NOT NULL
requested_model       VARCHAR NULL
normalized_params     JSONB NOT NULL
status                VARCHAR NOT NULL
status_version        BIGINT NOT NULL DEFAULT 0
priority              INT NOT NULL
current_attempt_id    UUID NULL
estimated_cost        NUMERIC
reserved_amount       NUMERIC
result_asset_id       UUID NULL
failure_class         VARCHAR NULL
failure_code          VARCHAR NULL
created_at            TIMESTAMPTZ
updated_at            TIMESTAMPTZ
terminal_at           TIMESTAMPTZ NULL

UNIQUE (tenant_id, idempotency_key)

status_version 用于乐观并发控制。状态转换应类似:

UPDATE generation_tasks
SET status = $new_status,
    status_version = status_version + 1,
    updated_at = now()
WHERE id = $task_id
  AND status = ANY($allowed_from_states)
  AND status_version = $expected_version;

受影响行数为 0 时,说明事件已过期、状态不允许或存在并发竞争,必须重新读取事实,而不是直接覆盖。

task_attempts

id                    UUID PK
task_id               UUID NOT NULL
attempt_no            INT NOT NULL
provider              VARCHAR NOT NULL
model                 VARCHAR NOT NULL
dispatch_token        VARCHAR NOT NULL
status                VARCHAR NOT NULL
submit_started_at     TIMESTAMPTZ
submit_finished_at    TIMESTAMPTZ NULL
unknown_since         TIMESTAMPTZ NULL
error_class           VARCHAR NULL
error_code            VARCHAR NULL
created_at            TIMESTAMPTZ

UNIQUE (task_id, attempt_no)
UNIQUE (dispatch_token)

一次业务任务可以有多个 attempt,但同一 attempt 的 Provider Submit 只能产生一次业务效果。

provider_jobs

id                    UUID PK
task_id               UUID NOT NULL
attempt_id            UUID NOT NULL
provider              VARCHAR NOT NULL
provider_job_id       VARCHAR NOT NULL
provider_request_key  VARCHAR NULL
normalized_status     VARCHAR NOT NULL
raw_status            VARCHAR NULL
last_observed_at      TIMESTAMPTZ
next_poll_at          TIMESTAMPTZ NULL
output_ref            JSONB NULL
raw_response_ref      VARCHAR NULL
created_at            TIMESTAMPTZ
updated_at            TIMESTAMPTZ

UNIQUE (provider, provider_job_id)

assets

id                    UUID PK
tenant_id             UUID NOT NULL
project_id            UUID NULL
kind                  VARCHAR NOT NULL
source_asset_id       UUID NULL
object_bucket         VARCHAR NOT NULL
object_key            VARCHAR NOT NULL
content_type          VARCHAR
size_bytes            BIGINT
checksum_algo         VARCHAR
checksum_value        VARCHAR
status                VARCHAR NOT NULL
width                 INT NULL
height                INT NULL
duration_ms           BIGINT NULL
frame_rate_num        INT NULL
frame_rate_den        INT NULL
codec                  VARCHAR NULL
variant_profile       VARCHAR NULL
created_at            TIMESTAMPTZ

UNIQUE (object_bucket, object_key)

业务接口返回 asset_id,播放时再根据权限生成短期 CDN 签名 URL。CDN 签名 URL 可以附带过期时间和访问策略,但它是传输授权,不是业务所有权事实。7

credit_ledger

id                    UUID PK
tenant_id             UUID NOT NULL
user_id               UUID NOT NULL
task_id               UUID NULL
business_type         VARCHAR NOT NULL
business_id           VARCHAR NOT NULL
type                  VARCHAR NOT NULL
amount                NUMERIC NOT NULL
currency_or_credit    VARCHAR NOT NULL
status                VARCHAR NOT NULL
created_at            TIMESTAMPTZ

UNIQUE (business_type, business_id)

type 可包括:

RESERVE
SETTLE
RELEASE
REFUND
COMPENSATE

outbox_events

id                    UUID PK
aggregate_type        VARCHAR NOT NULL
aggregate_id          UUID NOT NULL
event_type            VARCHAR NOT NULL
payload                JSONB NOT NULL
schema_version        INT NOT NULL
trace_id              VARCHAR
causation_id          UUID NULL
status                VARCHAR NOT NULL
available_at          TIMESTAMPTZ NOT NULL
published_at          TIMESTAMPTZ NULL
attempt_count         INT NOT NULL DEFAULT 0
created_at            TIMESTAMPTZ

inbox_dedup

consumer_name         VARCHAR NOT NULL
event_id              UUID NOT NULL
processed_at          TIMESTAMPTZ NOT NULL
result_code           VARCHAR NULL

PRIMARY KEY (consumer_name, event_id)

其他关键表

tenants
users
projects
project_revisions
moderation_records
callback_events
task_events
media_jobs
render_jobs
notification_events
provider_capabilities
provider_health_snapshots
reconciliation_cases

3.5.2 统一消息信封

{
  "event_id": "uuid",
  "event_type": "GenerationTaskQueued.v1",
  "aggregate_type": "generation_task",
  "aggregate_id": "task_uuid",
  "tenant_id": "tenant_uuid",
  "occurred_at": "2026-06-23T03:00:00Z",
  "schema_version": 1,
  "trace_id": "trace-id",
  "causation_id": "previous-event-id",
  "correlation_id": "task-id",
  "partition_key": "task-id",
  "retry_count": 0,
  "payload": {
    "task_id": "task_uuid",
    "attempt_id": null,
    "priority": 50
  }
}

设计要求:

  • 消息只携带定位业务对象所需的小字段,不携带视频二进制。
  • event_id 全局唯一,供 Inbox 去重。
  • correlation_id 通常使用 task_id,方便全链路检索。
  • causation_id 连接因果链,便于追踪“哪个事件触发了哪个事件”。
  • schema 采用显式版本,不在不兼容修改后复用旧事件名。
  • 只有确实需要单任务顺序的 Topic 才使用 task_id 作为顺序键;不要求全局顺序。

3.5.3 Topic 建议

generation.dispatch
provider.status.observe
provider.poll
output.fetch
media.process
render.execute
billing.command
notification.event
reconcile.command

应按以下维度拆分 Topic 或消费者组:

  • 资源类型不同:网络下载、CPU 转码、GPU 渲染。
  • SLA 不同:用户状态通知与离线清理。
  • 重试语义不同:Provider Submit 与 Query。
  • 安全权限不同:公网回调处理与内部媒体处理。
  • 积压隔离不同:渲染积压不应阻塞生成状态更新。

3.5.4 Redis 键示例

rate:api:{tenant_id}:{route}
concurrency:tenant:{tenant_id}
slot:provider:{provider}:{model}
lease:schedule:{task_id}
lease:poll:{provider_job_id}
task:progress:{task_id}
notify:user:{user_id}
provider:health:{provider}:{model}

所有槽位和租约必须具备:

  • TTL。
  • owner token。
  • 续租规则。
  • 释放时校验 owner。
  • 定期与 PostgreSQL 活跃任务对账。

即使 Redis 计数泄漏,也只能影响短期准入,不能改变 PostgreSQL 中任务是否已成功、是否已计费的事实。


3.6 正常流程

3.6.1 素材上传

  1. 客户端请求 Asset Service 创建上传会话。
  2. Asset Service 校验租户配额、文件声明和项目权限。
  3. PostgreSQL 创建 UPLOADING 资产记录和唯一对象键。
  4. 返回预签名地址或分片上传参数。
  5. 客户端直接上传对象存储。
  6. 客户端提交完成确认,Asset Service 校验对象存在、大小和 checksum。
  7. 媒体探测任务异步执行,完成后资产变为 READY

3.6.2 创建生成任务

  1. Gateway 完成认证、限流和租户上下文注入。
  2. Generation Service 以 (tenant_id, idempotency_key) 查询或创建任务。
  3. 校验模式、模型、时长、宽高比、引用资产状态和租户权限。
  4. Moderation Service 对提示词和输入素材执行前置审核。
  5. Billing Service 计算估价并确认可预占额度。
  6. 在一个 PostgreSQL 事务中写入:
    • generation_tasks
    • credit_ledger(RESERVE)
    • outbox_events(TaskQueued)
  7. 事务提交后立即向客户端返回 task_idQUEUED

3.6.3 异步调度与供应商提交

  1. Outbox Relay 将 TaskQueued 发布到 RocketMQ。
  2. Scheduler 消费后先执行 Inbox 去重。
  3. 按租户公平和优先级选择任务。
  4. 检查平台、租户、供应商和模型槽位。
  5. 从能力矩阵中过滤不支持当前请求的供应商。
  6. 根据成本、可用性、延迟、地区和用户锁定策略选择路由。
  7. 创建 task_attempt,将任务从 QUEUED 条件更新为 DISPATCHING
  8. Provider Adapter 以稳定请求键调用供应商 Submit。
  9. 成功获得 provider_job_id 后创建 provider_jobs,任务进入 SUBMITTED/RUNNING

3.6.4 状态观测

  1. Callback Gateway 接收供应商回调,完成验签、防重放和事件归档。
  2. 对没有可靠回调的供应商,Polling Service 按计划查询。
  3. 两类观测统一转换为 ProviderStatusObserved
  4. 状态处理器读取 PostgreSQL 当前任务和 attempt。
  5. 根据状态机、事件新旧和版本号执行条件更新。
  6. 非终态变化可更新 Redis 热点进度;关键阶段写 PostgreSQL 事件。

3.6.5 输出回源和媒体处理

  1. 供应商成功后写入 OutputFetchRequested Outbox。
  2. Output Fetch Worker 下载临时输出,校验后存入自有对象存储。
  3. 创建源结果 Asset,并将任务转入 POST_PROCESSING
  4. Media Worker 生成代理、缩略图、HLS、波形等变体。
  5. Moderation Service 执行输出侧审核。
  6. 所有必须变体就绪后,将任务转为 SUCCEEDEDPLAYABLE
  7. Billing Service 根据计费政策结算并释放预占差额。
  8. Notification Gateway 向客户端发送状态提示。
  9. 客户端重新读取任务快照,再获取 CDN 签名播放地址。

3.6.6 在线编辑与最终渲染

  1. 浏览器以非破坏方式保存时间轴和项目修订版本。
  2. 用户提交导出时固定一个不可变 project_revision
  3. Render Service 将时间轴编译成 Render DAG。
  4. 创建 render_job + outbox,异步进入渲染队列。
  5. Render Worker 读取对象存储素材,执行 FFmpeg/GPU 渲染。
  6. 成品写入对象存储并通过 CDN 分发。
  7. 渲染失败只影响导出作业,不回滚已经成功的生成资产。

3.7 异常流程和竞态条件

场景主要风险处理方式
客户端因超时重复创建任务重复任务、重复预占同一租户使用相同 Idempotency-Key;数据库唯一约束返回原任务
Outbox 发送结果未知后再次发送MQ 重复消息消费者 Inbox 去重;状态更新使用条件语句
Scheduler 消费超时被 MQ 重投重复 Provider Submitattempt/dispatch token 唯一;提交前检查 attempt 状态;未知提交不盲重试
供应商已受理但本地 HTTP 超时重复生成和重复供应商成本标记 SUBMIT_UNKNOWN;按供应商幂等键查询或对账;确认未受理前不新建 attempt
回调早于本地保存 provider_job无法关联回调先归档 callback_events;按 request key/provider job id 延迟重放匹配
回调与轮询同时报告状态重复转换、终态被覆盖统一进入状态处理器;版本号、允许转换表和事件时间共同判断
旧 RUNNING 回调晚于 SUCCEEDED状态回退终态保护;非终态事件不得覆盖终态
取消与成功同时发生用户看到取消但供应商已完成,计费争议PostgreSQL 条件更新决定谁先赢;取消仅是请求,需区分 CANCEL_REQUESTEDCANCELED
输出 URL 过期已生成但无法取得媒体成功后立即高优先级回源;监控 URL 剩余寿命;必要时向供应商刷新链接或人工补偿
Redis 槽位泄漏任务长期无法调度或超额调度TTL、owner token、续租、定期按数据库活跃任务重建
Media Worker 重复消费重复转码、重复对象和资源浪费(source_asset_id, profile, pipeline_version) 唯一作业键;存在有效成品即返回成功
结算与退款并发余额错误账本业务键唯一;状态机规定可执行的账务动作;冲突后重读
通知服务断线UI 没收到完成提示通知非事实;客户端重连后 GET 任务快照

3.7.1 “供应商已成功但本地超时”的决策树

这是本系统最重要的异常之一。

Submit 超时

  ├─ 供应商支持 idempotency key?
  │      ├─ 是:用同一 key 查询或重试同一请求
  │      └─ 否

  ├─ 是否拿到 request-id / provider-job-id / 可查询句柄?
  │      ├─ 是:进入查询流程
  │      └─ 否

  ├─ 是否能按外部请求标签、时间窗口或账单记录对账?
  │      ├─ 是:标记 SUBMIT_UNKNOWN,延迟确认
  │      └─ 否:进入人工或保守等待窗口

  └─ 只有确认第一次未受理,才允许新建下一 attempt

不能简单写成:

if err != nil {
    retrySubmit()
}

因为网络超时只说明“本地没有拿到响应”,不说明“对方没有执行”。第二次 Submit 可能产生第二个收费任务。

3.7.2 取消与完成竞态

建议语义:

  • CANCEL_REQUESTED:平台已收到取消意图,但供应商是否真正停止尚未确认。
  • CANCELED:确认未提交、供应商取消成功,或按业务规则可视为取消完成。
  • SUCCEEDED:供应商已完成且结果有效,即使取消请求稍晚到达,也不能回退。

处理方式:

  1. 用户取消时执行条件更新,例如仅允许从 QUEUED/DISPATCHING/SUBMITTED/RUNNING 进入 CANCEL_REQUESTED
  2. 若任务仍在 QUEUED,可阻止提交并释放预占。
  3. 若已提交,则异步调用供应商 Cancel。
  4. 若成功回调先落库,取消请求受影响行数为 0,向用户解释任务已完成。
  5. 若取消先落库,后续成功回调仍需按供应商真实成本和产品政策决定:保留结果、隐藏结果、部分收费或退款,不能仅凭 UI 状态决定账务。

3.8 幂等、一致性、重试与补偿设计

3.8.1 分层幂等

层次幂等键/机制防止的问题
API 创建(tenant_id, idempotency_key)客户端超时重试导致重复任务
任务调度dispatch_tokenattempt_noMQ 重投导致重复 attempt
供应商提交provider_request_keySubmit 重试导致供应商重复作业
回调处理provider_event_id 或事件哈希供应商重复回调
MQ 消费(consumer_name, event_id)同一事件重复生效
状态转换status_version + allowed states并发事件覆盖和状态回退
计费(business_type, business_id)重复预占、结算、退款
媒体处理(source_asset_id, profile, pipeline_version)重复转码和重复变体
渲染(project_revision_id, render_profile, idempotency_key)用户重复点击导出
对象写入确定性 object key + checksum重试生成多个等价对象

3.8.2 一致性边界

强一致范围:

  • 创建任务、额度预占、Outbox 事件。
  • 单次任务状态转换与对应任务事件。
  • 账本动作与其唯一业务键。
  • 资产元数据与“内部对象已确认存在”的登记。

这些应尽量在单个 PostgreSQL 本地事务中完成。

最终一致范围:

  • PostgreSQL 任务与 MQ 消费进度。
  • 本地任务与供应商真实状态。
  • 供应商成功与结果回源。
  • 源资产与媒体变体。
  • PostgreSQL 状态与 Redis 进度缓存。
  • 任务事实与在线通知。
  • 平台账本与供应商账单。

最终一致不等于“随便异步”,必须有:

  • 明确的中间状态。
  • 可观察的超时阈值。
  • 可重复执行的恢复命令。
  • 定期对账任务。
  • 人工处理入口。

3.8.3 Outbox 与 RocketMQ 事务消息的取舍

RocketMQ 支持事务消息,用于协调本地事务与消息提交,但消费端仍会面临重试和重复处理,不能因此省略业务幂等。4

本项目优先选择 Outbox 的理由:

  • 任务、账本和待发布事件能在同一 PostgreSQL 事务中审计。
  • MQ 暂时不可用时,事件仍保留在数据库。
  • 可按业务条件查询、重放和修复。
  • 对 Go 服务和多类业务事件的理解成本较低。

代价是:

  • 需要 Relay、归档和积压监控。
  • Relay 可能重复发送。
  • Outbox 表需要索引、清理和分区策略。

因此正确表述是:

Outbox 解决“数据库已提交但消息未可靠留下”的原子性缺口;Inbox、唯一约束和状态机解决“消息可能重复”的业务效果问题。

3.8.4 重试分类

操作默认是否可重试条件
PostgreSQL 只读查询短暂连接错误,设置次数和 deadline
RocketMQ 发布 Outbox同一 event_id,允许重复消息
MQ 消费业务处理处理逻辑幂等;失败分类明确
Provider Query通常可限流退避、抖动、供应商 QPS 保护
Provider Submit不可盲重试仅在幂等键可靠或确认首次未受理时重试
Provider Cancel条件可重试取消本身应有请求键;终态后停止
对象存储上传分片同一 upload/part,完成时校验
Output Fetch条件可重试临时 URL 未过期、支持 Range 或重新下载;对象键幂等
Media Process作业键唯一,输出可验证
内容违规不可属于业务拒绝,重试会重复触发审核风险
余额不足不可自动用户充值或调整请求后再建新任务

RocketMQ 的消费重试会在失败后重新投递,达到上限后可进入死信队列;生产上应按错误类别决定“立即重试、延迟重试、最终失败或人工处理”,而不是对所有异常使用同一重试次数。3

3.8.5 补偿策略

补偿不是把历史数据改回去,而是追加新的业务动作:

  • 任务创建失败:追加 RELEASE 释放预占。
  • 供应商失败且未产生费用:释放全部预占。
  • 供应商已收费但平台处理失败:记录供应商成本,按用户政策退款或补偿。
  • 用户重复扣费:追加 REFUND/COMPENSATE,保留原流水。
  • 媒体回源失败:保持任务在可恢复状态,优先重取,不立即把供应商成功改为完全失败。
  • Redis 槽位错误:按 PostgreSQL 活跃 attempt 重建,不修改任务事实。
  • 回调遗漏:由轮询和超时扫描补齐。

3.9 性能瓶颈与容量估算方法

3.9.1 先估算长任务在途量

AI 视频系统的核心不是普通 API QPS,而是任务到达率、平均处理时间和外部并发配额。

使用 Little’s Law:

平均在途任务数 N = 任务到达率 λ × 平均处理时间 W

例如峰值创建速率为 3 tasks/s,平均供应商执行时间为 180 s

N_avg = 3 × 180 = 540 个在途任务

若 P95 执行时间为 420 s,在持续峰值下需要观察:

N_p95_reference = 3 × 420 = 1260

这不代表系统一定同时向供应商提交 1260 个任务。真实可运行量还受供应商并发槽位限制,其余任务会进入平台队列。

3.9.2 关键容量公式

供应商在途量 ≈ 到达率 × 供应商平均耗时
输出回源带宽 ≈ 到达率 × 成功率 × 平均输出大小
媒体 CPU 核需求 ≈ 到达率 × 成功率 × 单任务 CPU 秒
每日原始存储 ≈ 到达率 × 成功率 × 平均输出大小 × 86400
每日总存储 ≈ 每日原始存储 × 变体放大系数
MQ 吞吐 ≈ 到达率 × 每任务消息数
DB 核心写入 ≈ 到达率 × 每任务关键状态写次数
在线连接数 ≈ 日活用户 × 同时在线比例 × 平均页面连接数

假设:

峰值生成到达率:3 tasks/s
成功率:85%
平均输出大小:24 MB
每任务媒体 CPU:18 core-seconds
媒体变体放大系数:2.5
每任务 MQ 消息:12 条
每任务关键 DB 写:10 次

得到:

输出回源平均带宽:3 × 0.85 × 24 ≈ 61.2 MB/s
媒体 CPU:3 × 0.85 × 18 ≈ 45.9 cores
MQ:3 × 12 = 36 messages/s
核心 DB 写:3 × 10 = 30 writes/s
每日原始输出:约 5.29 TB/day
含变体:约 13.2 TB/day

该示例说明:即使控制面数据库写入并不高,媒体带宽、CPU 和存储也可能先成为成本瓶颈。真实规划必须替换为业务实际的时长、码率、保留期和模型成功率。

3.9.3 主要瓶颈与应对方式

1. 供应商配额

通常是最先出现的硬边界:

  • 每模型并发。
  • 组织级并发。
  • RPM/QPS。
  • 账户余额和分钟成本上限。

应使用平台侧排队、租户公平、槽位控制和路由,而不是把所有任务瞬间打到供应商。

2. PostgreSQL 热点与连接池

风险:

  • 所有 Pod 总连接数超过数据库承载。
  • 高频进度更新造成 WAL、死元组和热行竞争。
  • Outbox 扫描不当导致表扫描和锁竞争。

措施:

  • 关键阶段才写任务表,进度放 Redis 并周期快照。
  • Outbox 使用 (status, available_at) 索引和小批量锁定。
  • 限制每实例连接池,统一计算全局连接预算。
  • 活跃任务使用部分索引;历史事件分区归档。

3. RocketMQ 积压

不能只看消息条数,要看:

  • oldest_message_age
  • 各 Topic 的消费速率差。
  • 重试队列增长。
  • 死信增长。
  • 单任务停留时间。

自动扩容应结合队列年龄和资源槽位,而不是只看 Worker CPU。供应商已满时,盲目增加 Scheduler 实例没有意义。

4. Output Fetch 网络带宽

措施:

  • 流式下载和上传。
  • 限制单任务带宽和最大文件大小。
  • 独立 egress 网络与连接池。
  • 尽量选择与供应商和对象存储网络距离合理的区域。
  • 对临时 URL 设置高优先级队列。

5. Media/Render CPU、GPU 与临时盘

措施:

  • 按 CPU、GPU、临时盘类型分队列。
  • 有界 worker pool,禁止无限制启动 FFmpeg。
  • 任务级 cgroup/容器配额和最大执行时长。
  • 监控临时盘高水位,达到阈值停止拉取。
  • 中间结果 checkpoint 和可复用缓存。

6. Notification 长连接

措施:

  • Gateway 无状态化,连接路由放短期缓存或一致性哈希。
  • 心跳、连接上限、慢消费者断开。
  • 事件合并,例如 500 ms 内的进度变化只发送最后一次。
  • 重连后重新 GET 快照,不维护无限离线消息。

3.9.4 背压策略

背压从下游向上游传播:

供应商槽位满
→ Scheduler 不再提交
→ generation.dispatch 积压
→ 达到租户/平台排队阈值
→ 新请求返回排队等级、429 或 503 + Retry-After
Media Worker 临时盘或 CPU 满
→ 暂停 media.process 消费
→ 生成任务保持 POST_PROCESSING
→ 可选择先提供原始结果或延迟播放
→ 不拖垮 API 和供应商状态处理

高级表达:

高并发不是无限接受工作,而是在成本、外部配额和资源边界内进行准入、排队、公平和有序退让。


3.10 高可用与降级方式

组件故障业务表现降级/恢复策略
API Gateway/Go 服务实例故障短请求失败或连接中断无状态多副本、跨可用区、readiness、优雅退出、客户端幂等重试
PostgreSQL 主库故障无法创建或推进任务自动/人工故障转移、PITR、明确 RPO;故障期间暂停写操作,不把 Redis 升级为事实源
Redis 故障限流、进度、槽位和通知受影响查询回源 PostgreSQL;采用保守并发或暂停新调度;恢复后按 DB 重建派生状态
RocketMQ 故障新事件暂时无法传递任务与 Outbox 仍可原子提交;Relay 积压;超过安全阈值后停止接收昂贵新任务
Scheduler 故障任务排队不执行多实例竞争租约;MQ 保留事件;恢复后继续消费
单一供应商故障429、5xx、延迟上升熔断、降权、切换兼容供应商;用户锁定模型时保持排队或明确失败
Callback Gateway 故障回调暂时丢失或供应商重试多副本、快速落事件;Polling 作为恢复通道
Polling Service 故障无回调任务状态停滞回调继续工作;恢复后扫描 next_poll_at 和超时任务
Output Fetch 故障供应商成功但暂不可播放高优先级重试、URL 过期监控、刷新链接或人工补偿
Object Storage 故障上传、回源、播放和渲染受阻重试、区域冗余/复制、延迟任务;不丢失 PostgreSQL 任务事实
Media Worker 过载任务停在后处理独立队列和资源池;延迟代理/HLS;必要时提供安全原始文件
Render Worker 过载导出排队与生成和媒体处理隔离;按付费等级排队;不影响已有资产播放
Notification 故障UI 无实时提示客户端轮询和重连读取快照;任务继续推进
Billing/Moderation 不可用无法安全接收新付费任务查询类接口保持可用;新任务 fail-closed 或进入待审核,不先调用供应商

3.10.1 降级优先级

发生系统性故障时,建议按以下顺序保护:

  1. 保证 PostgreSQL 事实不被破坏。
  2. 保证已被供应商受理的任务能继续观测和回源。
  3. 保证账务不重复、不丢失。
  4. 保证查询任务状态可用。
  5. 限制或暂停新任务创建。
  6. 延迟非关键变体、通知和离线清理。

不应优先牺牲状态处理和回源去维持“还能继续创建新任务”的表面可用性。

3.10.2 可恢复性设计

每个阶段都应存在扫描器:

QUEUED 超时扫描
DISPATCHING 超时扫描
SUBMIT_UNKNOWN 对账扫描
RUNNING 长时间无观测扫描
OUTPUT_FETCHING 超时扫描
POST_PROCESSING 卡住扫描
CANCEL_REQUESTED 超时扫描
Outbox 未发布扫描
账本与供应商账单对账
对象存储孤儿和数据库悬空资产扫描

扫描器不能直接“把状态改成成功”,而应产生受审计的恢复命令或对账案例。


3.11 安全风险

风险攻击面设计措施
越权访问资产猜测 asset_id、项目 ID、对象键每次控制面请求校验 tenant/user ACL;对象存储私有;短期 CDN 签名授权
预签名 URL 滥用上传任意对象、覆盖他人对象、长期传播唯一对象键、短有效期、限制方法/大小/checksum、上传完成后二次校验
伪造供应商回调公网 WebhookHMAC/非对称签名、时间戳、nonce、防重放、事件去重;IP allowlist 仅作辅助
SSRFOutput Fetch 下载供应商或用户提供的 URL仅允许 HTTPS、供应商域名白名单、DNS 与重定向复检、阻断私网/元数据地址、限制大小和超时
恶意媒体文件ffprobe/FFmpeg 漏洞、压缩炸弹、异常容器magic number、大小/时长限制、沙箱容器、只读根文件系统、最小网络权限、及时升级
Shell 注入拼接 FFmpeg 命令直接传参数数组;禁止执行未经转义的 shell 字符串
供应商密钥泄露前端、日志、错误响应密钥管理系统、短期凭据、按供应商最小权限、日志脱敏、定期轮换
跨租户消息污染MQ payload 中 tenant 不可信消费者从数据库重新确认 tenant 与对象归属;内部服务身份认证
提示词和媒体隐私日志、追踪、供应商数据使用敏感字段脱敏、按政策最小化保存、供应商数据处理条款、可删除和审计
内容与版权风险输入/输出生成内容输入和输出双重审核、肖像/版权政策、人工复核、申诉与下架流程
账务篡改重复消息、管理操作追加账本、唯一业务键、审计日志、双人审批高风险补偿
CDN 链接传播签名 URL 被转发短有效期、签名 Cookie/URL、必要时绑定路径/IP、下载审计

特别注意:预签名 URL 是 bearer capability,拿到地址的人通常可在有效期内执行被授权的操作,因此不能把它当作永久公开链接。5


3.12 常见错误设计及其后果

错误设计后果
HTTP 请求同步等待模型完成连接占用、超时、网关重试、用户重复提交、难以扩容
在数据库事务中调用供应商长事务、锁占用、连接池耗尽;外部超时使事务结果难判断
浏览器先把视频上传 Go,再由 Go 转存双倍带宽、内存/临时盘压力、API Pod 被大文件拖垮
将 Redis 当任务或余额唯一事实源故障切换、淘汰或数据丢失后无法审计和恢复
认为 RocketMQ 保证 exactly-once重投后重复生成、重复转码、重复计费
Provider Submit 遇到超时立即重试对方已受理时产生第二次生成和第二笔成本
回调处理器直接覆盖任务状态乱序回调导致终态回退;取消与完成竞态失控
只依赖回调,不做轮询和扫描回调丢失后任务永久卡住
把临时供应商 URL 直接交给用户URL 过期、权限失控、供应商切换困难、无法统一审核与 CDN
API 与 FFmpeg/GPU Worker 混部媒体任务抢占 CPU/内存/磁盘导致控制面雪崩
所有异步任务共用一个 Topic 和消费池渲染积压阻塞状态更新和结果回源,无法按资源独立扩容
为每个逻辑方框立即建独立数据库微服务任务、额度和 Outbox 跨服务一致性复杂化,补偿成本远大于收益
依赖通知事件恢复页面状态SSE/WebSocket 断线后 UI 与真实任务不一致
每 1% 进度更新 PostgreSQLWAL 和表膨胀、热行竞争、Autovacuum 压力
没有对账和人工重放工具生产故障只能手工改库,无法证明数据正确性

3.13 面试官可能追问的 10 个问题

  1. 为什么必须区分控制面和媒体数据面?
  2. 为什么不让 Generation Service 直接调用供应商并等待结果?
  3. PostgreSQL、Redis 和 RocketMQ 的边界如何划分?
  4. 为什么选择 Outbox,而不是只依赖 RocketMQ 事务消息?
  5. MQ 重复投递时,如何避免重复生成和重复计费?
  6. 供应商已经受理,但本地 Submit 超时,系统怎样处理?
  7. 回调和轮询同时到达、顺序相反时,如何保证状态正确?
  8. 这些组件是否都要拆成独立微服务?
  9. Redis、RocketMQ或某个供应商不可用时,系统如何降级?
  10. 这个系统怎样做容量规划,最先达到瓶颈的通常是什么?

3.14 每个追问的资深回答

问题 1:为什么必须区分控制面和媒体数据面?

回答:

两者的负载模型完全不同。控制面是小请求、高正确性,主要受数据库连接、事务和外部 API 延迟影响;媒体数据面是大字节流和重计算,主要受网络带宽、CPU、GPU、临时磁盘和对象存储吞吐影响。若让 Go API 代理上传、下载或转码,媒体高峰会直接耗尽 API 连接和资源,导致连任务查询都不可用。因此客户端直传对象存储、CDN 直接分发,Output Fetch、Media Worker 和 Render Worker独立部署。这样不仅性能更好,也让故障域和扩缩容策略与资源类型匹配。

问题 2:为什么不让 Generation Service 直接调用供应商并等待结果?

回答:

供应商生成是分钟级任务,远超普通 HTTP 超时。同步等待会长期占用连接,网关或客户端超时后还可能自动重试,引发重复生成。正确做法是请求内只完成校验、审核、预占、任务与 Outbox 的事务,然后返回 task_id。供应商调用由 Scheduler 和 Adapter 异步执行,状态通过回调或轮询收敛。这样可以独立做配额调度、重试、背压和故障恢复。

问题 3:PostgreSQL、Redis 和 RocketMQ 的边界如何划分?

回答:

PostgreSQL 是事实源,保存任务、资产、账本、供应商作业和 Outbox;Redis 是派生加速层,保存限流、租约、槽位、热点进度和通知路由;RocketMQ 负责至少一次风格的异步传递、削峰、重试和死信。Redis 丢失后可以从数据库重建;MQ 重投后消费者可幂等处理;任何任务是否成功、是否扣费最终都以 PostgreSQL 为准。媒体字节则进入对象存储和 CDN,而不是这三者。

问题 4:为什么选择 Outbox,而不是只依赖 RocketMQ 事务消息?

回答:

两者都可以缩小本地事务与发消息之间的缺口。Outbox 的优势是任务、账本和待发布事件都在同一个 PostgreSQL 事务内,可查询、审计、补发和人工重放;缺点是需要 Relay、清理和积压治理。RocketMQ 事务消息减少了自建 Relay 的一部分工作,但消费端仍会重试,业务幂等仍不可少,而且事务回查与业务数据库的耦合需要仔细实现。对于需要强审计和多类补偿的任务平台,我会优先 Outbox,并明确它只能保证事件最终留下,不能保证下游只执行一次。

问题 5:MQ 重复投递时,如何避免重复生成和重复计费?

回答:

不能只靠 MQ。调度消费先用 (consumer_name, event_id) 做 Inbox 去重;创建 attempt 使用 (task_id, attempt_no)dispatch_token 唯一约束;状态更新带允许来源状态和 status_version;供应商支持时传 provider_request_key;计费每个预占、结算、退款都有唯一业务键。即使两个消费者并发,最终也只有一个数据库条件更新和唯一键插入成功。重复消费可以返回成功,但不再次产生外部副作用。

问题 6:供应商已经受理,但本地 Submit 超时,系统怎样处理?

回答:

我不会把网络超时直接标成失败,也不会立即重提。我会把 attempt 标成 SUBMIT_UNKNOWN。若供应商支持幂等键,就用同一键查询或重试;若拿到了 request-id,就按句柄查询;否则通过时间窗口、外部标签、回调或账单对账确认。只有证明第一次没有受理,才创建下一 attempt。因为盲目重提的代价不是普通重复消息,而是第二次模型执行和第二笔真实成本。

问题 7:回调和轮询同时到达、顺序相反时,如何保证状态正确?

回答:

Callback Gateway 和 Polling Service都只产生标准化的“状态观测事件”,不直接无条件更新任务。统一状态处理器读取当前状态,用允许转换表、终态保护、供应商事件时间和 status_version 做条件更新。旧的 RUNNING 事件不能覆盖 SUCCEEDED,重复 SUCCEEDED 只会被幂等忽略。取消与成功竞争也由数据库条件更新决定,而不是靠消息顺序。

问题 8:这些组件是否都要拆成独立微服务?

回答:

不需要。服务名称首先表达职责。物理拆分由独立扩容、资源隔离、安全边界和团队所有权决定。任务创建、额度预占和 Outbox 需要强本地事务,早期更适合作为同一控制面应用内的模块;Scheduler、Provider Worker、Output Fetch、Media、Render 和 Notification 因资源模型差异明显,适合独立部署。过早将 Billing 拆成独立数据库会把简单本地事务变成 Saga,只有组织或合规收益足够大时才值得。

问题 9:Redis、RocketMQ或某个供应商不可用时,系统如何降级?

回答:

Redis 不可用时,任务事实仍在 PostgreSQL;查询回源数据库,限流和并发采用保守策略或暂停新调度。RocketMQ 不可用时,任务与 Outbox 仍能原子提交,Relay 等待恢复;若 Outbox 积压超过安全阈值,则停止接收昂贵新任务。供应商不可用时,调度器熔断并选择兼容供应商;用户锁定模型时保持排队或明确失败。降级原则是优先保护事实、账务、已受理任务的观测和回源,而不是维持无限创建新任务。

问题 10:这个系统怎样做容量规划,最先达到瓶颈的通常是什么?

回答:

先用 Little’s Law 估算在途任务:到达率乘平均耗时。再分别计算供应商并发、输出回源带宽、媒体 CPU/GPU、临时盘、每日存储、MQ 消息率、数据库关键写入和在线连接数。多数情况下最先受限的不是 Go API CPU,而是供应商模型配额、媒体处理资源或对象存储/CDN成本。扩容必须按瓶颈做:供应商满时增加 Scheduler Pod没有意义;媒体积压要扩 Media Worker 或降低变体质量;数据库则要控制进度写放大和连接池。


3.15 三分钟口述稿

我们这个系统不是简单调用第三方 AI 接口,而是一个面向分钟级、高成本、强异步任务的媒体工作流平台。整体上我会把它分成控制面、供应商执行面和媒体数据面。

控制面由 API Gateway、Generation、Asset、Billing、Moderation 和 Scheduler 组成。用户创建任务时,HTTP 请求只做权限、参数、素材、审核和费用检查,然后在同一个 PostgreSQL 事务里写任务、额度预占和 Outbox,立即返回 task_id,不会同步等待模型。PostgreSQL 是事实源;Redis 只做限流、槽位、租约和热点进度;RocketMQ 负责异步解耦、重试和削峰,但我们不假设它 exactly-once。

Outbox Relay 把任务事件投递到 RocketMQ,Scheduler 再按租户公平、用户等级、供应商健康度、模型能力、成本和并发配额选择路由。所有供应商通过统一 Adapter 接入,提供 Submit、Query、Cancel、回调解析和错误分类。最关键的异常是供应商已经受理但本地 Submit 超时,这时不能盲目重试,而要把 attempt 标成 SUBMIT_UNKNOWN,使用供应商幂等键、request-id、轮询或账单对账确认,否则可能重复生成和重复成本。

状态通过 Callback Gateway 和 Polling Service共同观测,但它们都不直接覆盖数据库,而是进入统一状态机,用允许转换表和版本号处理重复、乱序以及取消与完成竞态。供应商成功后,Output Fetch Worker立即把临时结果回源到自己的对象存储,再由独立 Media Worker做探测、转码、HLS、缩略图、波形和输出审核。第三方临时 URL 不直接交给用户,播放统一通过内部 Asset、对象存储和 CDN。

前端编辑采用非破坏式时间轴,最终导出由 Render Service把固定项目版本编译成 Render DAG,再交给独立 FFmpeg 或 GPU Worker。这样 API、供应商调用、网络回源、转码和渲染分别使用独立资源池,某条重流水线积压不会拖垮任务查询。

整个系统的一次效果不是靠 MQ,而是靠 API 幂等键、数据库唯一约束、Inbox 去重、状态版本、供应商请求键和账本业务键共同实现。高可用也不只是多副本,还包括 Outbox 重放、轮询补偿、超时扫描、供应商切换、Redis 重建和账务对账。


3.16 十分钟深入讲解提纲

0:00—1:00:先定义系统本质

  • 不是模型 API 包装器,而是长耗时、高成本、弱确定性的媒体工作流平台。
  • 核心指标是任务到达率、在途量、供应商槽位、媒体吞吐和成功成本。
  • 先给出一句边界:PostgreSQL 管事实,Redis 管速度,RocketMQ 管异步,对象存储管媒体。

1:00—2:20:解释三平面

  • 控制面:任务、资产元数据、额度、审核、调度。
  • 供应商执行面:Submit、Query、Cancel、Callback、错误归一化。
  • 媒体数据面:上传、回源、转码、HLS、渲染、CDN。
  • 说明为什么大文件不能穿过 Go API。

2:20—4:20:走一遍核心组件

  • Gateway:认证、租户和限流,不做业务事务。
  • Generation:创建/取消/查询,任务+预占+Outbox 本地事务。
  • Scheduler:公平、优先级、四级槽位和供应商路由。
  • Adapter:屏蔽供应商差异,不负责计费和调度。
  • Callback/Polling:两个观测通道,统一进入状态机。
  • Output Fetch/Media:临时 URL 回源、自有资产化和媒体变体。
  • Notification:体验通道,不是事实源。
  • Render:固定项目版本、编译 DAG、独立资源池。

4:20—5:40:讲一致性与幂等

  • 为什么创建任务、额度预占和 Outbox 必须同事务。
  • MQ 可能重复,消费者要 Inbox 去重。
  • 状态使用允许转换表和 status_version
  • 账本使用追加记录和唯一业务键。
  • 对象和媒体作业使用确定性键。

5:40—6:50:重点讲第三方结果未知

  • Submit 超时不等于失败。
  • SUBMIT_UNKNOWN 中间状态。
  • 供应商幂等键、request-id、查询、回调和账单对账。
  • 只有确认未受理后才允许下一 attempt。
  • 解释重复生成与重复成本风险。

6:50—7:50:讲竞态

  • 回调与轮询并发。
  • 旧 RUNNING 晚于 SUCCEEDED。
  • 取消与完成竞争。
  • 条件更新和终态保护如何收敛。

7:50—8:40:讲性能与容量

  • Little’s Law:N = λW
  • 供应商配额通常先于 API CPU 成为瓶颈。
  • 再计算回源带宽、媒体 CPU/GPU、临时盘和每日存储。
  • 背压按供应商、租户和资源池逐层传播。

8:40—9:30:讲高可用和降级

  • Redis 故障可重建,采用保守准入。
  • MQ 故障依赖 Outbox 保留事件,积压过高后停止昂贵新任务。
  • Callback 故障由 Polling 补偿。
  • 供应商故障通过熔断和兼容路由降级。
  • Media/Render 积压不影响控制面查询。

9:30—10:00:用架构不变量收尾

  • 不在数据库事务中调用第三方。
  • 不把临时供应商 URL 交给用户。
  • 不声称 MQ 或 Redis 天然 exactly-once。
  • 不让通知决定任务事实。
  • 不盲目重试未知 Submit。
  • 任何阶段都能对账、重放和恢复。

本章结论

一个可信的 AI 视频平台架构,应同时满足以下条件:

  1. 用户请求快速返回,长任务完全异步化。
  2. 核心事实落在 PostgreSQL,本地关键事务边界清晰。
  3. Redis、RocketMQ、对象存储和 CDN 各自承担适合的职责。
  4. 供应商调用经过统一适配、调度、状态观测和未知结果处理。
  5. 媒体上传、回源、转码和渲染与控制面资源隔离。
  6. 重复、乱序、超时、取消竞态和临时 URL 过期都存在明确恢复路径。
  7. 业务一次效果由幂等键、唯一约束、状态机、账本和对账共同实现。
  8. 逻辑边界清楚,但物理拆分遵循实际扩容和事务需求,而不是为了微服务而微服务。

参考资料