直播系统的故障通常不会只停留在一个组件里。推流断开可能让转码任务卡在运行中,回调丢失可能让资源无法释放,资源不足又会使新的流无法及时生成播放清单。用户最后看到的是“直播卡住了”,但系统内部可能同时存在多个不一致状态。
可靠性设计的重点,不是让每个组件永远成功,而是让每个阶段都可观察、可重试、可降级,并在重复执行或部分失败后最终收敛。下面以一条直播流为主线,讨论状态、任务、资源和观测之间如何配合。
先定义一条流的状态
直播流可以用有限状态表达生命周期:
created → connecting → receiving → processing → playable
│ │ │ │
└──────────┴────────────┴────────────┴→ failed
│
└→ recovering → playable
这些状态不只是页面上的标签,而是资源和动作的边界。connecting 可以继续等待鉴权和输入建立;receiving 表示最近仍收到有效媒体;processing 表示转码或打包在推进;playable 要求至少有一个可读取且连续的播放窗口;failed 则表示系统停止自动等待,需要重试、降级或人工处理。
状态不应只由某一个回调驱动。接入心跳、媒体时间戳、转码租约、最新清单时间和播放器探测结果,都能帮助系统判断当前状态。回调是状态变化的一种来源,而不是唯一真相。
每次状态更新都应带上流标识、输入版本、连接世代号和事件时间。主播断流后快速重连时,旧连接的延迟回调不能覆盖新连接已经产生的结果。用连接世代号或输入版本做条件更新,可以把迟到事件限制在它所属的生命周期内。
输入版本与连接世代号
“同一条流”不一定对应“同一份输入”。主播断开后重新推流,编码器可能重新开始时间戳,清晰度和音频轨道也可能发生变化。系统需要为每次有效输入分配版本,例如 stream-A/v3,并让转码、打包、清单和播放事件都引用这个版本。
连接世代号解决的是另一个问题:旧连接何时失效。可以把每次成功建连看作一个递增的 generation。只有当前 generation 的心跳、断流和完成事件才能更新主状态;旧 generation 的事件可以记录,但不能覆盖新连接的状态。
条件更新可以写成如下规则:
UPDATE stream_state
SET status = :next_status, generation = :generation
WHERE stream_id = :stream_id
AND generation <= :generation
实际实现还需要检查事件类型和状态转换是否合法。failed 不能被任意迟到的 receiving 重新打开,playable 也不能只因为连接重新建立就直接恢复。状态机的价值在于把“谁可以改变什么”写清楚。
转码任务要可重试,也要可去重
同一条输入流可能因为断线、进程重启或回调超时被多次提交。如果任务没有幂等键,系统会同时运行多个相同输出,消耗 GPU 或 CPU,还可能让不同转码进程争抢同一个输出目录。
可以用“流标识 + 输入版本 + 输出档位 + 编码配置版本”生成任务键。创建任务时先做唯一性检查;执行器拿到任务后通过租约声明执行权。租约过期后允许补偿,但新执行者需要确认旧进程已退出,或把输出写入隔离目录,避免互相覆盖。
任务记录至少要区分以下时间:创建时间、入队时间、开始执行时间、最近心跳、输出就绪时间和最终结束时间。只有一个 updated_at 字段很难判断任务是在排队、执行缓慢还是已经失去心跳。
转码回调应该包含阶段和结果,而不是只传成功或失败:
accepted → preparing → transcoding → packaging → ready
└→ failed(stage, code)
错误码要能区分输入不可解码、资源不足、进程被终止、输出写入失败和回调发送失败。只有这样,系统才能判断是重试当前阶段、降级清晰度,还是要求主播重新推流。
业务状态写成功但回调响应丢失时,发送方可以安全重放;执行器成功但状态提交失败时,对账任务可以根据输出文件、租约和版本重新判断。幂等并不等于“所有事情都只做一次”,而是允许同一结果被安全地确认多次。
断流恢复不能只靠重启进程
检测到输入断流后,系统要先判断是否为短暂抖动。立即销毁资源会造成频繁冷启动,等待过久又会让用户看到过期画面。可根据最后时间戳、心跳间隔和历史重连行为设置恢复窗口,同时停止无意义的输出生成。
恢复窗口内可以保留少量输入上下文,但要停止继续扩大队列。若主播在窗口内重新连接,系统需要比较输入版本和编码参数;如果版本兼容,可以继续使用部分转码上下文;如果时间线已经断裂,则应从新的关键帧重新生成播放窗口。
恢复时要核对输入版本、旧任务是否存活、最后一片输出是否完整、清单是否停止更新。不能只看到推流重连就把状态改成可播放,播放端还要能读取新的连续窗口。清单重新推进后,播放器还可能需要重新加载或跳过旧窗口。
如果源流长期不可恢复,系统应进入可解释的失败态,释放转码槽位、删除临时文件,并保留诊断上下文。把所有失败都留在 running 状态,最终会制造无法清理的幽灵任务。失败状态还要区分“等待主播重试”“等待资源恢复”和“输入永久不可用”,便于选择不同的通知和补偿策略。
高峰期要保护控制面
直播高峰会同时增加推流连接、转码任务、清单更新、回调和播放探测。最容易被忽略的是控制面:大量任务同时失败并重试时,状态写入和调度查询可能先于媒体处理资源耗尽。
背压应沿任务生命周期传播,而不是只限制消息消费者:
新流接入 ──→ 待转码队列 ──→ 执行槽位 ──→ 输出与回调
│ │ │ │
└─入口限流─────┴─队列上限────┴─并发上限────┴─回调限速
入口限流保护连接和鉴权服务,队列上限防止内存和持久化集合无限增长,执行并发保护编码资源,回调限速保护数据库和调度器。每层都需要记录被拒绝、延迟和降级数量,否则只看成功任务数会误判系统健康度。
资源池最好按能力和优先级拆分。基础清晰度、音频和关键业务流可以拥有保底槽位;高清转码、截图、录制后处理和历史补偿任务使用可回收资源。这样一个高峰不会让所有新流因为某类低优先级任务占满机器而无法建立基本播放能力。
恢复时不能一次性放开全部限制。先恢复状态回调和资源释放,再逐步提高新任务并发,最后处理低优先级补偿任务。重试需要指数退避、随机抖动和总次数上限,避免所有任务在同一秒再次冲击控制面。
重试还要有全局预算。单个任务可以重试若干次,但某个错误码在整个集群内持续增加时,应触发熔断或降级,而不是让每个任务独立消耗资源。对输入不可解码这类确定性错误,盲目重试只会制造更多相同失败。
可观测性要覆盖用户结果
基础设施指标仍然重要,但 CPU、内存和队列长度不能单独代表直播质量。建议至少按流、区域和输出档位记录:
| 层级 | 关键指标 |
|---|---|
| 接入 | 建连成功率、断流次数、心跳间隔、鉴权拒绝数 |
| 转码 | 排队时延、处理时长、槽位占用、阶段失败数 |
| 打包 | 清单更新时间、分片生成延迟、连续性错误 |
| 分发 | CDN 命中率、回源错误、首片成功率 |
| 播放 | 首帧时间、缓冲长度、卡顿率、清晰度切换失败率 |
这些指标需要通过同一个流标识关联。一个流从推流到首帧的时间线,通常比孤立日志更快说明问题在哪一层。日志中可以记录任务键、输入版本、连接世代号、节点、错误码和重试次数;trace 则负责把一次建连、转码和发布过程串起来。
用户结果指标要有明确的统计口径。例如首帧时间从播放器发起清单请求开始,还是从用户点击播放开始;卡顿按累计卡顿秒数计算,还是按发生过卡顿的会话比例计算。口径不清时,两个团队可能都声称指标变好了,但实际观察的是不同人群。
告警要能指导动作
告警不应该只写“直播服务异常”。不同告警应该对应不同的处理动作:
| 告警 | 可能含义 | 首个动作 |
|---|---|---|
| 接入成功率下降 | 鉴权、网络或连接槽位异常 | 检查入口和区域线路 |
| 转码排队持续上升 | 资源不足或执行器失联 | 限制低优先级任务,检查槽位 |
| 清单长时间不更新 | 打包停止或输出写入失败 | 检查最近分片和任务租约 |
| 首片成功率下降 | 清单、鉴权或 CDN 回源问题 | 抽样请求清单和首片 |
| 卡顿率上升但服务端正常 | 终端网络或播放器策略变化 | 按地区、设备和档位切分 |
告警需要包含影响范围、开始时间、样例流和建议查询字段。高优先级告警触发后,值班人员应该能够在几分钟内判断是单流故障、区域故障还是全局资源问题,而不是重新阅读所有系统文档。
降级与局部故障隔离
直播系统不一定要在所有档位都成功后才对外播放。基础清晰度和音频先就绪时,可以先发布一个可播放窗口,高档位稍后补齐。播放器看到新档位后再参与切换,已经开始观看的用户不必等待整个转码阶梯。
某个区域的转码节点异常时,可以把新任务调度到其他区域,或者只关闭高成本档位。某个 CDN 边缘节点回源失败时,可以缩短缓存、切换源站或让客户端重新选择线路。降级动作必须有边界,不能为了维持“在线”而无限消耗其他区域资源。
局部故障隔离还包括数据和权限边界。一个租户的异常推流不应占满全局输入连接,一个大流的高清转码不应阻塞基础档位,一个错误的回调重试不应拖慢所有状态更新。配额、优先级和隔离队列都应在正常流量下演练过。
故障演练与恢复顺序
恢复流程不应该只存在于故障现场。可以在隔离环境或小范围流量中演练以下场景:转码进程被杀、回调接口超时、CDN 回源失败、某区域执行器失联、输入流快速断开重连、同一任务重复投递。
一个更安全的恢复顺序是:
- 停止新的非必要任务进入,保护控制面;
- 确认当前输入版本和活跃连接,避免旧任务误更新;
- 恢复状态回调和资源释放,清理幽灵租约;
- 检查最新完整分片和清单窗口,恢复基础档位;
- 小步提高新任务并发,观察完成率和错误率;
- 最后恢复高清档位、历史补偿和低优先级任务。
每一步都要有继续和回退条件。只观察队列长度下降,可能掩盖失败率和重复执行正在上升;只观察转码成功,也可能掩盖播放器首片已经无法访问。
发布前的可靠性清单
- 状态机是否定义了合法转换、超时和人工终止;
- 输入版本和连接世代号能否阻止迟到事件覆盖新状态;
- 转码创建、回调和补偿是否都使用幂等键;
- 租约过期后,旧执行器和新执行器是否会写入同一输出;
- 失败是否区分可重试错误、确定性错误和需要人工处理的错误;
- 断流恢复、永久失败和人工终止是否有明确的状态转换;
- 队列、执行槽位、回调和重试是否分别设置上限;
- 基础档位是否拥有保底资源,高清失败时播放是否仍然连续;
- 告警是否能从“服务异常”定位到具体流、阶段和错误码;
- 指标、日志和 trace 是否能用同一流标识关联;
- 恢复流程是否在隔离环境演练过;
- 回滚和降级动作是否不会覆盖后台已有的写作数据。
直播系统的可靠性不是让组件永远成功,而是限制失败范围,让任务安全重试,让用户得到可用的降级结果。状态、租约、背压和观测数据能够收敛,偶发断流就不必升级成整场事故。
相关阅读:工程日志:异步媒体任务堆积后的背压复盘、工程日志:直播录制接入语音识别的解耦设计、WebRTC、HLS 与 LL-HLS:流媒体协议该如何选择。