写在前面 文档处理是这类产品里最脏最累的活:格式千奇百怪、耗时不可预测、依赖沉重。这篇讲我们怎么把它从主服务里剥出来,以及为什么要设计成"无状态 + 回调"。
一、为什么必须剥出去
主 API 服务需要保持轻量和低延迟——它要处理登录、鉴权、对话流式返回这些高频请求。
而文档处理是完全相反的负载:
- 耗时不可预测。 一个两页的 PDF 和一个三百页的 PDF,处理时间差两个量级。
- 依赖沉重。 高保真 PDF 结构化解析要带上模型权重,Office 转换要装 LibreOffice,音视频要装 FFmpeg。全塞进主服务,镜像轻松超过 10GB。
- 资源占用高。 CPU 密集,有时还要 GPU。和 API 服务抢资源会直接导致接口变慢。
所以我们把它拆成了独立的中台服务,主 API 只通过 HTTP 调用它。
一个具体对比
拆之前:
- 主服务镜像 12GB,构建一次 12 分钟,部署时拉镜像要等
- PDF 处理高峰时,主 API 的对话接口 P99 延迟从 200ms 飙升到 3 秒——因为 PDF 解析吃掉了大量 CPU
- 改一行业务代码,重新构建 12GB 镜像、重新部署
拆之后:
- 主服务镜像 800MB,构建不到 2 分钟
- 文档中台独立扩容,处理压力不影响对话质量
- 改主服务代码不需要碰中台镜像
二、双轨:同步和异步分开走
不是所有文档任务都值得异步化。异步意味着任务表、状态轮询、回调处理——对一个一秒就能完成的转换来说,这套开销完全不值得。
我们按耗时把任务分成两轨:
同步轨——轻量格式转换(Word/Excel/PPT/EPUB 等转 Markdown)。请求进来直接处理、直接返回。用户体验上就是"点一下就好了"。
异步轨——大文件版面翻译、多模态理解、图谱抽取。进队列,由 Worker 消费,完成后回调通知。
同步轨的 SLA
同步轨的目标是 95% 的请求在 3 秒内返回。超过 3 秒的任务会被自动升级到异步轨——用户会收到一个任务 ID,可以通过轮询或通知获取结果。
这个阈值不是拍脑袋定的。我们统计了 10000 个同步请求的耗时分布,发现 3 秒的分界线正好在 95 分位。如果你的产品类型不同,这个阈值应该按自己的数据来定。
分界线不是绝对的,我们按经验设了一个耗时阈值,超过的走异步。这个阈值需要根据真实数据调整,一开始拍脑袋定的值通常偏乐观。
三、无状态:中台不连业务库
这是我们认为最值得强调的一个设计决定:文档中台不直连业务数据库。
它的输入是对象存储里的文件路径,输出也是写回对象存储,任务结果通过 HTTP 回调告诉主 API。它自己不知道用户是谁、不知道这份文件属于哪个资源、也不写任何业务表。
好处:
- 可以随意水平扩容。 任何一个实例都能处理任何一个任务,不需要考虑数据库连接数和事务竞争。
- 故障隔离。 中台挂了,业务数据不会处于中间状态;主库挂了,中台的任务也不会写坏数据。
- 边界清晰。 中台的职责只有一条:把文件 A 变成文件 B。所有业务语义都在主 API 侧。
这个设计的核心思想是:中台是一个"纯函数服务"——同样的输入总是得到同样的输出,处理过程不依赖外部状态。 这使得它的行为完全可预测、完全可测试、完全可替换。
代价是多了一次回调链路,需要处理回调失败重试和幂等。我们的做法是回调接口按任务 ID 幂等,重复回调只会更新一次状态。
四、任务认领机制
异步任务有两种触发方式,我们两种都用:
推模式——主 API 创建任务后直接把消息投进队列,中台 Worker 消费。这是主路径,延迟最低。
拉模式——中台的定时任务周期性地去主 API 认领处于 Pending 状态的任务。这是兜底路径。
为什么需要拉模式兜底
推模式看似简单直接,但在分布式系统里有不可忽视的失败模式:
- 消息队列 broker 重启期间投递的消息可能丢失
- Worker 消费到一半进程被 kill,消息回了队列但状态没改
- 网络分区导致消息确认超时
拉模式用定时任务扫描 Pending 状态超时(比如超过 5 分钟还没被处理)的任务,把它们重新拉起。这是一个简单的最终一致性保证——它不追求零延迟,只保证"没有任何任务会永远卡在 Pending"。
认领时要加锁,避免多个 Worker 同时认领同一个任务。我们的做法是认领接口原子性地把任务状态从 Pending 改成 Running 并返回,改失败说明已被别人领走。
五、版面保留翻译
这是异步轨里最重的一类任务,也是产品上差异化最明显的能力——翻译后 PDF 的排版、图表、公式位置全部保持原样,可以中英对照下载。
实现上需要几步:先用版面分析模型识别出页面上的文本块、图片、表格和公式区域;只翻译文本块,其余原样保留;再把译文按原位置重新排版渲染回去,中文渲染需要嵌入字体。
三个工程挑战
- 版面分析模型的推理成本。我们用本地推理运行时做了优化,避免每次都走重型框架。同时做了预处理缓存,同一份文档二次翻译直接复用分析结果。
- 译文长度和原文不一致。中译英通常变长,英译中通常变短,重排时需要处理溢出。溢出的处理策略是分级的:轻微溢出调字号、中等溢出扩区域、严重溢出改布局。
- 术语一致性。同一个术语在一份文档里必须译法统一,我们支持用户挂载自己的术语库。术语库的匹配做了词形变化处理(如复数、过去式等),提高命中率。
六、这套设计适合谁
如果你的产品有以下特征,这套结构值得参考:
- 用户会上传各种格式的文件,且格式覆盖面是产品卖点之一
- 处理耗时跨度大,从秒级到分钟级都有
- 处理依赖重,不想污染主服务的运行环境
- 有横跨多个服务的功能需求(如翻译调中台、图谱调中台),且希望这些消费者共享同一套处理能力而不是各自重复实现
如果你的文档处理只有一种格式、耗时稳定在几百毫秒,那么直接在主服务里做完就好,不要为了架构分层而分层。
想在你的业务里试试同样的做法?
告诉我们业务问题与数据现状,我们会给出方案建议与落地路径。
需要帮助?
联系我们的团队,获取贴合你业务场景的方案建议。