作者:Jo Turk
原文:https://pion.ly/blog/sctp-interleaving/
什么是队头阻塞,以及它为什么会成为 WebRTC 数据通道的问题
SCTP(WebRTC 数据通道所使用的协议)看起来本该非常适合承载多路复用的应用数据。它是基于消息的,支持在一条关联(association)内使用多条流,而 WebRTC 数据通道正是构建在它之上,用于传输非媒体数据。
要理解发送端的队头阻塞,首先需要理解 SCTP 如何发送消息。消息按发送顺序被调度。SCTP 会按照路径 MTU(最大传输单元)对消息分片,在 WebRTC 中通常约为 1200 字节。因此一条 256 KB 的消息会被拆成大约 218 个更小的分片(chunk)。
那么真正的问题是什么?
设想你在用 SCTP 传输媒体,用于某些特殊场景:也许是 DRM 保护内容,或是 WebRTC 媒体轨道(RTP)通常不支持的编解码器,例如 AAC 音频或 VVC 视频。与此同时,控制流、遥测流或聊天流也在共享同一条 SCTP 关联。
如果媒体流开始发送大体积的可靠消息,SCTP 会把它们切分成更小的分片,而这些分片可能独占发送队列。换句话说,小小的聊天消息或控制消息可能不得不排在一大块传输之后等待。
如果该流是可靠的、且数据包丢失,那些大分片的重传会让更小的流等得更久。聊天消息看起来被延迟了,控制流量也会感觉卡顿。
large video fragment 0
large video fragment 1
large video fragment 2
large video fragment 3
...
large video fragment 200
tiny control message finally sent
chat messages finally sent
audio-like frame finally sent
那为什么我不能自己分片、自己排队?
许多应用确实是这么做的,以避免产生大体积的 SCTP 消息:不是发送一条巨大的消息,而是由应用自己把它切成更小的块(就像 SCTP 自己做的那样),并自行调度这些块。
但代价是,你实际上在 SCTP 之上又构建了一套协议。
而你需要处理诸如块编号、重组、流量控制、背压处理、重传语义、公平调度以及消息边界之类的事情。
这会因流的模式不同而变得更棘手(各有利弊,自己挑)。
如果你使用部分可靠流或非可靠流(SCTP 不会重传丢失的数据包),你就必须决定分片丢失时该怎么办:是丢弃、部分解码、重传,还是采用某种 FEC?
但如果你决定不处理这套复杂的非可靠逻辑,而改用可靠流,那么你就又回到了老问题:在网络不佳、发生丢包和重传时,其他流会被延迟交付。
RFC 8260 正是为解决这一类问题而写的。问题在于,TSN(传输序列号)同时承担了太多职责:可靠性、分片重组和排序。而且分片消息还必须使用连续的 TSN。
I-DATA 改变了什么
SCTP 消息交织(interleaving)使用 I-DATA chunk。重要的变化是:TSN 不再用于对用户消息内部的分片进行排序。I-DATA 新增了:
MID = Message Identifier(消息标识符)
FSN = Fragment Sequence Number(分片序列号)
不过 TSN 依然存在。它仍然用于可靠性、SACK(选择性确认)、丢包检测和重传。但分片重组现在改用 MID + FSN,而不再要求“所有分片必须在 TSN 空间中相邻”。RFC 8260 指出,I-DATA 新增了 MID 和 FSN,移除了 SSN,用 MID 标识消息,用 FSN 枚举该消息的各分片。
于是各标识的职责变为:
TSN = 可靠性 / SACK / 重传
SID = SCTP 流
MID = 该流内的用户消息
FSN = 该消息内的分片编号
示例:
SID=4 MID=90 FSN=0 TSN=100 video fragment
SID=2 MID=301 FSN=0 TSN=101 chat message
SID=0 MID=12 FSN=0 TSN=102 control message
SID=4 MID=90 FSN=1 TSN=103 video fragment
SID=1 MID=44 FSN=0 TSN=104 chat message
SID=4 MID=90 FSN=2 TSN=105 video fragment
视频分片仍然能被正确重组,因为它们通过如下组合被识别:
SID=4 + MID=90 + FSN=0,1,2,...
它们不再需要占用一段连续的 TSN 范围 :)
丢包情况下的交织
交织并不会让丢包消失。它不会给每条流各自独立的拥塞窗口。它不会凭空创造带宽。它只是让其他流能在大消息的分片之间争取到发送机会。
启用交织后,糟糕的网络环境仍可能长这样:
bulk fragment sent
audio-like frame sent
control message sent
bulk fragment sent
telemetry sent
bulk fragment lost
audio-like frame sent
retransmit lost bulk fragment
control message sent
这比下面这种情形好得多:
bulk fragment sent
bulk fragment sent
bulk fragment lost
bulk fragment sent
bulk fragment sent
retransmit lost bulk fragment
control finally sent
audio-like frame finally sent
telemetry finally sent
重传仍然要消耗容量,但它不再必须把所有其他流都堵在大消息原本的串行化之后。
“流调度器”在其中的位置
交织赋予 SCTP 从不同流中挑选 chunk 的能力,但它仍然需要一套策略来决定下一条该轮到哪条流。
常见策略有:
round-robin(轮询):
每条非空流轮流获得机会
priority(优先级):
高优先级流先于低优先级流被服务
WFQ(加权公平队列):
各条流按权重获得容量
RFC 8260 定义了 SCTP 流调度器,包括轮询、优先级和加权公平队列。它指出 WFQ 使用可配置的每条流权重,并且如果一条流的权重是另一条的 n 倍,它应获得 n 倍的容量。它还指出,配合用户消息交织的 WFQ 被用于 WebRTC 数据通道。
对于对队头阻塞敏感的应用,这是一个强大的组合:
I-DATA 交织 + WFQ 或优先级调度
交织打破了大消息这堵墙,WFQ 或优先级决定谁受到保护。
Pion/SCTP 的 RFC 8260 实现自带 WFQ 调度器(默认)、可自定义权重的 WFQ,以便你优先照顾某条特定流(func WithInterleavingWeightedFairqueuingWeight(streamID uint16, weight uint16)),以及轮询调度器(通过 WithInterleavingRoundRobinScheduler() 启用)。
如果你有兴趣,甚至可以定义自己的调度器 :)
type InterleavingStreamScheduler interface {
Reset()
Push(StreamSchedulerChunk)
Peek() StreamSchedulerChunk
Pop(StreamSchedulerChunk) error
}
func WithInterleavingStreamSchedulerFactory(newScheduler InterleavingStreamSchedulerFactory)
交织检查示例
我在 pion 里加了一个示例,可以让你观察并试验交织效果。它包含一个基础压力测试:如果启用了交织,你会看到类似这样的结果(注意所有控制消息都在大块传输完成之前到达)。

如果没有启用交织,你会看到类似这样(注意控制消息不得不等待大块分片完成)。

如何启用并使用交织?
在 Pion 4.2.13 及以上版本中,交织应默认启用,你无需做任何事情。
在我撰写本文时,交织在 Firefox 中默认启用,在 Chrome 中位于开关之后,且已合并到 Pion/sctp。
实际上,除非对端也支持并在握手期间完成协商,否则交织不会被真正使用。要了解当前流是否启用了交织,我们通过 Pion 的 GetStats() 暴露了该信息:
report := pc.GetStats()
for _, stat := range report {
sctpStats, ok := stat.(webrtc.SCTPTransportStats)
if !ok || sctpStats.Metadata == nil {
continue
}
fmt.Println("SCTP interleaving:", sctpStats.Metadata.MessageInterleavingEnabled)
fmt.Println("Partial reliability:", sctpStats.Metadata.PartialReliabilityMode)
}
统计元数据包括:
{
"metadata": {
"messageInterleavingEnabled": true,
"partialReliabilityMode": "i-forward-tsn",
"zeroChecksumSendingEnabled": true,
"zeroChecksumReceivingEnabled": true
}
}
如果 W3C 接受我提交的这个 issue,浏览器 API 中可能也会有类似的统计信息。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/73189.html