任务执行架构
有了“任务对象”和“消息队列”之后,下一步就是回答一个更工程化的问题:
这套系统到底应该拆成哪些角色,它们分别负责什么?
如果这一步没有想清楚,最后常见的结果就是:
- API 什么都做
- Worker 什么都知道
- 状态分散在各处
- 任务失败以后没人知道该去哪看
所以这一篇讲的是“职责分层”。
一个够用的基础架构
对于大多数中后台和 AI 场景,一个足够清晰的任务执行架构通常长这样:
flowchart LR
C[Client] --> API[API / Task Controller]
API --> DB[(Task DB)]
API --> MQ[Message Queue]
MQ --> W1[Worker Pool A]
MQ --> W2[Worker Pool B]
W1 --> DB
W2 --> DB
W1 --> RS[(Result Store)]
W2 --> RS
DB --> Query[Query API]
Query --> C
W1 --> Notify[Callback / SSE / WebSocket]
W2 --> Notify
Notify --> C
这套图里每个角色都应该职责单一。
每一层分别做什么
1. API / Task Controller
这一层只负责:
- 校验参数
- 创建任务记录
- 返回
taskId - 把任务投递到队列
它不应该承担真正的长耗时执行。
2. Task DB / State Store
这一层负责记录:
- 任务状态
- 当前步骤
- 进度
- 执行次数
- 失败原因
- 最终结果索引
你可以把它理解为“系统对外的真相来源”。
3. Message Queue
负责:
- 缓冲任务
- 解耦 API 与 Worker
- 控制消费节奏
- 配合重试和死信
4. Worker Pool
Worker 是真正干活的地方,它负责:
- 拉任务
- 执行业务步骤
- 更新状态
- 处理失败重试
如果任务类型不同,还可以拆成多个池子,比如:
ocr-workerembedding-workernotify-worker
5. Result Store
如果任务结果是大文件、大文本、图片、视频,不建议直接塞进任务表,而是放到对象存储或专门结果表里。
6. Query / Notify
任务做完以后,前端要么主动查,要么被动接收通知:
- 轮询查询
- SSE 推送
- WebSocket 推送
- 回调 webhook
为什么要这样拆
原因 1:快路径和慢路径分离
API 层是“快路径”,目标是尽快返回。
Worker 层是“慢路径”,目标是稳定完成任务。
把快路径和慢路径混在一起,系统的吞吐和稳定性都会变差。
原因 2:执行能力可以独立扩容
如果 OCR 很慢,那你就扩 OCR Worker;如果通知很多,就扩通知 Worker。
如果都堆在 API 里,就只能整体扩容,成本高,也不精准。
原因 3:状态与执行分离后,系统更可恢复
任务执行到一半挂掉时,只要状态存储还在,你就知道:
- 它卡在哪一步
- 是否可以重试
- 是否需要人工介入
一个标准执行流程
sequenceDiagram
autonumber
participant C as Client
participant API as API
participant DB as Task DB
participant MQ as MQ
participant W as Worker
participant R as Result Store
C->>API: POST /tasks
API->>DB: insert CREATED
API->>MQ: publish task_id
API-->>C: 202 Accepted
W->>MQ: consume task_id
W->>DB: update RUNNING
W->>R: write result artifact
W->>DB: update SUCCESS + result_ref
C->>API: GET /tasks/{id}
API->>DB: query task
API-->>C: status / progress / result
这个流程里有一个非常重要的细节:
API 返回成功,并不代表任务成功;它只代表“任务已被系统受理”。
这也是为什么长任务接口通常返回 202 Accepted,而不是直接返回最终结果。
一个简单案例:OCR 解析平台
假设你在做合同 OCR 系统,可以按下面思路拆:
API 层
- 接收文件地址
- 创建
contract_parse任务 - 返回任务 ID
Worker 层
download-worker:下载文件并做预处理ocr-worker:调用 OCR 服务extract-worker:提取字段persist-worker:写结果与索引
状态层
- 保存当前步骤
- 保存任务进度
- 保存失败原因
查询层
GET /tasks/{id}查看状态GET /tasks/{id}/result查看最终结果
这样一拆,你会发现系统职责非常清楚,任何问题都更容易定位。
最小可落地版本怎么做
如果你不是在搭超大平台,而是先做一个够用版本,最小架构可以是:
- 一个 API 服务
- 一个 MySQL 任务表
- 一个 Redis 队列
- 一个 Worker 进程
- 一个轮询查询接口
先把这条链路跑通,再考虑:
- 多 worker
- 优先级队列
- DAG 编排
- 回调通知
- 监控告警
常见反模式
1. API 同时负责创建和执行
短期看省事,长期看会非常难扩展。
2. Worker 直接决定前端展示逻辑
Worker 应该更新状态,不应该知道页面怎么画。
3. 结果只存在内存里
Worker 重启以后结果就丢了,任务查询也没法做。
这一篇要记住的核心点
- 长任务架构的基本角色是:API、状态存储、队列、Worker、结果存储、查询/通知
- API 是快路径,Worker 是慢路径,这两层一定要分开
- 状态存储是系统真相来源,前端展示和排障都要围绕它展开
下一篇进入更复杂但也更有价值的部分:当一个任务不是线性步骤,而是一个依赖图时,该怎么做任务编排。