27、SFU 调试:分析 SFU 的转发日志,验证订阅/取消订阅逻辑
说实话,SFU 的调试,最让人头疼的不是媒体流卡顿,而是「订阅/取消订阅」的逻辑对不对。
你想想看,一个房间几十个人,每个人看到的画面都不一样。有人只看大屏,有人要听所有人的声音。如果订阅逻辑出了错,轻则多浪费带宽,重则直接黑屏。
我个人习惯,遇到这类问题,第一件事就是翻转发日志。日志不会骗人,它清清楚楚地告诉你:谁订阅了谁,什么时候订阅的,什么时候取消的。
转发日志里到底有什么?
SFU 的转发日志,说白了就是一张「谁在看谁」的流水账。我常用的 mediasoup 和 Janus,日志格式虽然不同,但核心字段都一样。
我拿 mediasoup 的日志举个例子:
// mediasoup 的订阅日志示例
[INFO] Transport::Produce() | transportId:"1234" kind:"video" ssrc:123456
[INFO] Router::AddConsumer() | consumerId:"abcd" producerId:"efgh" kind:"video"
[INFO] Consumer::SetPreferredLayers() | consumerId:"abcd" spatialLayer:2 temporalLayer:0
[INFO] Consumer::Pause() | consumerId:"abcd" reason:"user_mute"
[INFO] Consumer::Resume() | consumerId:"abcd"
[INFO] Consumer::Close() | consumerId:"abcd" reason:"unsubscribe"
你看,每一行都带着时间戳和操作类型。我最关注的是 AddConsumer、Close 和 SetPreferredLayers 这三类。
验证订阅逻辑的四个关键步骤
我在项目中遇到过好几次「订阅了但没收到流」的诡异问题。后来总结了一套排查流程,分享给你:
- 检查 Consumer 创建时机 —— 是不是在用户加入房间后才创建的?
- 检查 Producer 是否存在 —— 如果 Producer 还没产生,Consumer 创建了也是空转。
- 检查 PreferredLayers —— 订阅的是全分辨率还是缩略图?日志里有没有设置 spatialLayer?
- 检查 Consumer 状态 —— 是不是被 Pause 了?Resume 回来了吗?
嗯,这里要注意:很多 SFU 在用户静音时会自动 Pause Consumer,但取消静音后如果 Resume 没触发,那用户就永远听不到声音了。我曾经因为这个被用户投诉过……
取消订阅的常见陷阱
取消订阅看起来简单,不就是调个 consumer.close() 吗?但实际坑不少。
- 重复取消 —— 前端点了两次取消按钮,后端收到两个 Close 请求,第二个会报错。
- 忘记清理 —— 用户离开房间时,只关了 PeerConnection,没关 Consumer。SFU 那边还留着资源。
- 异步竞争 —— 先取消订阅,紧接着又订阅同一个 Producer。如果顺序没处理好,新的 Consumer 可能创建失败。
用日志验证订阅/取消订阅的完整流程
我建议你写一个小脚本,实时监控 SFU 的日志,然后模拟用户操作。比如:
# 伪代码:监控订阅日志
def monitor_subscriptions(log_file):
active_consumers = {}
for line in tail(log_file):
if "AddConsumer" in line:
consumer_id = extract_id(line, "consumerId")
producer_id = extract_id(line, "producerId")
active_consumers[consumer_id] = producer_id
print(f"[订阅] {consumer_id} -> {producer_id}")
elif "Close" in line and "unsubscribe" in line:
consumer_id = extract_id(line, "consumerId")
if consumer_id in active_consumers:
del active_consumers[consumer_id]
print(f"[取消订阅] {consumer_id}")
# 检查泄漏
if len(active_consumers) > expected_max:
print(f"[警告] 活跃 Consumer 数量异常: {len(active_consumers)}")
这个脚本虽然简单,但很实用。我每次做压力测试都会跑着它,一旦发现 Consumer 数量只增不减,就知道有泄漏了。
订阅/取消订阅的状态机
为了让你更直观地理解,我画了一张状态图。它展示了 Consumer 从创建到销毁的完整生命周期:
从图里你能看到,Consumer 创建后,可以设置分辨率层级,也可以被暂停、恢复,最终被销毁。如果日志里出现「创建 -> 暂停 -> 销毁」而没有「恢复」,那说明用户可能只是暂时静音,但 SFU 却把 Consumer 关了——这就不对了。
实战:用日志定位一个典型的订阅 Bug
我记得有一次,用户反馈「我看不到某位主播的画面」。我立刻去翻 SFU 的日志,发现:
10:23:01 [AddConsumer] consumerId:"c1" producerId:"p1" kind:"video"
10:23:02 [SetPreferredLayers] consumerId:"c1" spatialLayer:0 temporalLayer:0
10:23:05 [Pause] consumerId:"c1" reason:"producer_paused"
10:23:10 [Resume] consumerId:"c1"
10:23:11 [Close] consumerId:"c1" reason:"unsubscribe"
看起来一切正常?但仔细看时间:10:23:10 恢复了,10:23:11 就取消了。这中间只隔了 1 秒。
我怀疑是前端逻辑问题:用户点了「取消订阅」按钮,但后端同时收到了「恢复」和「取消」两个请求。因为网络延迟,恢复请求先到了,取消请求后到了。结果就是:Consumer 刚恢复就被关了。
总结一下
分析 SFU 的转发日志,说白了就是盯着 Consumer 的「生老病死」。创建、设置层级、暂停、恢复、销毁——每一步都有对应的日志。只要你能把日志和用户操作对应起来,99% 的订阅问题都能定位。
我个人习惯,每次上线新功能,都会先跑一遍日志监控脚本,确保订阅/取消订阅的数量是匹配的。嗯,这招帮我避免了好几次线上事故。