不假设一次调用会成功:一套录音转写系统的可靠性设计
这是我个人做的一个项目——一个录音、转写、AI 摘要一体的应用。项目分两个子系统,这篇讲第一个:录音接收落库与转写服务对接。
这篇不讲产品,只讲一件事:转写服务是别人的系统,单次调用随时可能超时、报错、或者结果根本没送达。不能假设这次调用会成功,只能为每一种可能的失败,提前准备好接盘的路径。下面是这套设计具体长什么样。
01 · 转写请求为什么要过一层任务队列,而不是直接调用等结果
最初的需求其实是一条三段式流程:先从对接的通话平台系统那边接收录音文件、落到自己的存储里,再把录音转成文字,最后把"转写完成"这件事通知给对应的用户。三段各自异步、各自可能变慢或失败——如果把它们串成一条同步调用链,收到文件就原地转写、转写完就原地通知,任何一段卡住,都会把前面已经做完的事情一起拖住。
最初设想:三段串行
录音文件
(异步)
但真正落地时,通知用户这一步被单独拎出来了:录音一到、落库完成,就立刻通知用户"收到了",不等转写结果——转写完全走自己的异步节奏,两件事之间不再有依赖关系。
实际做法:落库后分叉两条线
录音文件
"收到录音了"
转写这一段尤其如此:通常要几十秒到几分钟才能出结果,如果收到录音就同步调用转写服务、原地等返回,整条链路就被这一次外部调用的耗时和成功率绑死了——转写服务一抖动,拖累的是整个流程。所以录音一落库,先生成一条状态为"待处理"的转写任务记录,提交转写请求之后立刻返回,不阻塞,把"接收录音"和"转写完成"两个时间点彻底解耦。
中间靠一个状态机串起来:待处理 → 处理中 → 完成/失败。提交任务后,通过一个工作流引擎等待"转写完成"的回调信号,同时给这次等待设置了 25 分钟的超时保护——避免转写服务那边没了动静,这个任务就无限期悬在"处理中"。
02 · 同步优先,异步兜底:给部分场景开一条快速通道
异步队列解决了整体稳定性的问题,但产品侧有一个更激进的诉求:桌面客户端用户录完音、点上传,上传完成的那一刻就应该收到通知和转写结果——不是"提交了,稍后回调",而是"传完即出"。正好转写服务供应商提供了一种极速转写能力——处理速度本身更快,而且结果直接在请求响应中同步返回,不需要走回调,但只支持 2 小时以内的录音——这是供应商的硬限制。于是为桌面客户端产生的、时长在 2 小时以内的录音开了一条"同步优先"通道:直接同步调用极速转写、尽快拿到结果,不用干等回调信号。
但同步调用意味着又把命运绑回了这一次请求的成功率——产品要快,可靠性不能丢。所以这条快速通道一旦失败,不是直接把任务标记失败,而是自动降级回退到"异步提交 + 等回调"的常规路径,保持任务状态继续往前走。两条路径并发存在时(比如降级发生的瞬间),状态更新做了幂等保护——先把任务状态置为"处理中"再持久化,避免快速通道和常规路径互相抢着改状态,把彼此的进度覆盖掉。
03 · 转写没有一次成功:分级重试 + 回调补偿 + 主动轮询兜底
不管走的是第 02 节的同步快速通道,还是常规的异步路径,最终都会碰到同一个问题:这一次调用可能失败。转写服务作为外部依赖,是运营商级别的服务,不稳定是常态而不是意外——限流、内部错误、超时,各种问题都会发生。所以这层设计了几道保险,有的层层递进,有的各管一段,每一层对应一种不同的失败方式:
这几层保险在系统里各自的位置
- 请求级重试 —— 转写服务本身有时频控制,单次调用偶尔会因为触发限流或瞬时抖动而失败,按退避策略本地重试最多 3 次,先把这类"再试一下就好了"的问题吸收掉
- 任务级重试 —— 转写服务内部也会出错,失败时会带一个错误码回来;不是所有错误都值得重试,所以根据错误码判断这次失败是不是"可重试"的——如果是,不在当场死等,而是把任务重新放回"待处理"队列,交给下一轮定时任务处理,最多重试 3 次,超过上限才判定为最终失败
- 回调补偿 —— 转写服务算完结果后,会主动调用回调接口把结果推过来,原始数据一进来就先落库。但回调延迟或重复的根源在供应商那边:转写服务自己对处理超时的任务有内部重试机制,所以回调不是"一次准时送达",而是可能延迟、也可能重复推送。为此还有一个独立的定时任务(每 5 分钟)定期扫描重放那些延迟到达、还没处理成功的回调,防止因为供应商这种不确定的送达节奏而漏掉结果
- 主动轮询兜底 —— 这一层是为切换转写服务商留的接口:不是所有供应商都会主动推送转写结果,像早期接入的一家供应商就没有回调机制,只能靠主动轮询去问"这个任务现在完成了吗"。现在主链路已经切换到会主动回调的供应商,这层轮询对当前流量基本不再触发——但接口留着,因为换供应商这件事以后还会发生,到时候这层就是现成的兜底,不用临时再写
- 限流保护 —— 所有请求过一层令牌桶限速(30 请求/秒,允许 10 个突发),避免自己把下游转写服务的接口打爆,反而制造更多失败
04 · 这套设计的代价,以及哪些问题它接不住
这套组合不是没有代价的。多一条同步快速通道,就多一份幂等保护的复杂度要维护;多几层重试和轮询,系统里同时活跃的状态机分支就更多,排查一个任务"为什么卡在处理中"要多看几个地方。用复杂度换可靠性,是笔明确的交易,不是免费的午餐。
它也不是万能的:如果转写服务本身长时间大范围不可用,再多重试和轮询也只是在延迟失败的发生,不能凭空制造出一个可用的转写结果——这套设计兜的是"偶发性、局部性"的失败,不是"下游整体宕机"这种系统性风险,后者需要更上层的降级预案(比如换一家转写服务、或者先把原始录音保留、事后补转写)。
但对"外部依赖单次调用大概率会在某个环节抖一下"这个更常见的问题,让每一种失败——超时、错误码、回调丢失、进程重启——都有一条对应的路径接住它,结果就是:最终转写成功与否,不再取决于某一次外部调用刚好没出问题。
如果你也在做涉及外部服务依赖、需要稳定性保障设计的系统,或者原生客户端的音频/设备处理,可以聊聊。
聊聊你的想法 →