跳转至

EP09:时延稳定性,比平均延迟更重要

上集回顾 EP08《视频埋点:如何量出端到端延迟》
下集预告 EP10《如何量化视频质量》

0. 本集目标

看完这一集,观众应该能做到三件事:

  1. 把"时延"这个词定义清楚,并区分三个最容易被混用的数字:时延、平均时延、峰值时延——为什么"平均"会撒谎,为什么"峰值"又不能单独看。
  2. 理解时延与丢包率是同一条链上的两端:丢包怎么一步步把时延和时延抖动抬起来,服务端又为什么反过来用"人为加一点时延"去换丢包鲁棒性。
  3. 会读 console 后台新上线的"RTP丢包率"面板:按天/周/月聚合,每个桶给出平均与峰值,知道每个数字该信到什么程度。

1. 时延的定义:先统一"量的是哪一段"

1.1 端到端时延

本项目里"端到端时延"有一个明确的定义(EP08 有完整实现):从编码器产出这一帧码流的那一刻,到播放端这一帧真正被提交渲染显示的那一刻,中间经历的全部时间,单位毫秒。它不是"网络传输时间",而是整条链路的合计:

编码产出
  → 上行网络(推流端 → Origin)
  → Origin 的接收窗口(SRT 的 TSBPD 交付窗口 / WHIP 的 RTP 重排重传缓冲)
  → 内部转发(Origin → Edge)
  → 下行网络(Edge → 播放端)
  → 播放端 jitter buffer(抖动缓冲)
  → 解码
  → 提交渲染显示

为什么必须先把起止点说死:同样叫"延迟",只算网络往返、只算收包到解码、和算到真正上屏,会得到差一个数量级的数字,彼此不可比。EP08 已经提醒过两处口径细节——打入码流的时间戳是"编码完成、即将发送"的时刻(编码器耗时被算进去了),测量点应该取目标帧实际提交显示的回调。引用任何延迟数字时,先问一句"它量的是哪一段",否则后面所有对比都是错的。

1.2 单程时延、RTT 不要混用

WebRTC 里至少有两类常被统称为 RTT 的量,它们和本集讲的单程时延不是一回事:

指标 覆盖范围
SEI/OBU 时间戳单程时延 推流端采集/编码到播放端渲染的完整链路,含编解码、抖动缓冲、排队
ICE candidate-pair RTT 当前 ICE 候选对上的 STUN 连通性往返,不含编解码/播放缓冲
RTCP RTT 对应 RTP/SSRC 的控制报文往返,也不等于 ICE 检查 RTT,更不含完整链路

currentRoundTripTime 是免费就能拿到的直连质量信号,但只有 EP08 那套时间戳方案,才能得到覆盖完整链路的单程时延。RTT 小不等于体验时延小——这是这一集反复要强调的"口径"问题的第一个例子。


2. 平均时延:一个会撒谎的数字

2.1 定义

平均时延就是一段窗口内所有有效时延样本的算术平均。它回答的是"典型情况下大概多久",但它对异常值几乎不设防。

2.2 为什么平均值会撒谎

假设一路直播,99% 的时间延迟是 100ms,剩下 1% 的时间因为网络抖动飙到 5 秒:平均下来约 149ms,看起来是个很漂亮的数字。但观众的真实体验是什么?观众记住的是那 1%——画面卡死了一下,不会因为另外 99% 是 100ms 就觉得体验好。平均值把一次剧烈的卡顿,稀释成了一个看起来无害的小数字,而这次卡顿本身没有消失。

这在实时媒体和分布式系统里是一个有名字的问题:长尾延迟(tail latency)——真正影响体感和投诉率的,往往不是"大多数时候有多快",而是"最差的那一小部分有多差、出现得多频繁"。平均值恰恰是对长尾最不敏感的统计量,因为极端值会被大量正常样本"平均"掉。

2.3 平均值的正确用途

不是不能看平均——它是很好的基线指标:看一条曲线长期平移,说明链路整体在变好或变坏。但不要用平均值做 SLA 判定,因为它天然掩盖长尾。


3. 峰值时延:最差的那一次,以及"多久出现一次"

3.1 定义

峰值时延是一个窗口内单个样本的最大值(最大值)。它回答的是"这段窗口里最坏的那一次有多坏"。

3.2 峰值和百分位数的关系

峰值和 P99、P95 这类百分位数解决的是同一个方向的问题,但性格不同:

  • 峰值(max):单个最极端的样本,对一次异常极其敏感,也因此容易被测量噪声污染(一次时钟抖动、一次跨帧匹配失败就可能顶高它)。
  • P50(中位数):一半样本更快、一半更慢,天然抗极端值,代表"典型体验"。
  • P95/P99:约 95%/99% 的样本不超过该值,描述尾部开始出现的位置;它们比单点峰值稳,但不等同于尾部最差值。

结论是三个一起看:平均值看基线,峰值看最坏,百分位看"最坏有多常见"。只看峰值会被单次噪声骗,只看平均会被长尾骗。

3.3 生产里真实在跑的判定口径

这不是纸上谈兵。本项目当前生产环境里跑的 P2P 延迟 SLA(ppcenter/internal/ws/play_hub.go)就是按百分位分档、不看平均:

  • p2pSLAP80TargetMs = 300、p2pSLAP80MaxMs = 600
  • p2pSLAP99WarnMs = 1000、p2pSLAP99MaxMs = 2000
  • 判定顺序必须互斥:先判 fail(P80>600ms 或 P99>2s),再判 pass(P80≤300ms 且 P99≤1s),其余有效样本统一为 warn。

哪怕平均延迟很低,只要 P99 表现差,也不会判成 pass。同时要说明:这组阈值是当前的业务决策,不是协议常数,换业务目标就要重新校准;而且它目前只用于后台展示和可选告警(AppLatencyDaily.slaStatus),不触发任何赔偿。

3.4 峰值单独看会漏掉"频率"

"峰值 5 秒"这件事,如果是每天一次和每小时一次,是完全不同的严重程度。所以峰值必须配合超阈值比例/出现频率一起读——这正是下一节 console 曲线里同时给出"峰值"和"样本数"的原因之一。


4. 时延与丢包率的关系

时延和丢包率不是两个独立指标,而是同一条链的因和果,而且这条因果关系是双向的。

4.1 丢包如何把时延抬起来

  • 重传带来的额外往返:SRT 用 ARQ、WHIP 用 NACK/RTX,丢一个包就意味着"重传"至少要多花接近一个 RTT 的时间才可能在接收端被修复。丢包越多,能按时修复的比例越低,时延的抖动就越大。
  • 乱序 → 重排等待:包到达顺序参差,接收端必须等缺失的包补齐才敢往前交付,这段等待直接叠加成延迟抖动。
  • 不可恢复丢包 → 卡顿 → 感知时延飙升:ARQ 也给不回来的包(不可恢复丢包),会造成帧不完整,解码器要等下一个关键帧,表现就是画面卡住——这是观众感知上最严重的"时延"。

4.2 SRT 的固定交付窗口:先于网络成为瓶颈

只要推流走 SRT,Origin 侧就有一个几百毫秒的固定交付窗口(TSBPD 接收延迟),它先于网络路径成为瓶颈——网络再快也压不下去。这个窗口由 srtLatency 设定(起始值取自部署配置,ppmmxDocker 模板为 300ms;EP12 的 386ms 是调参后的实测),可调下限 srtLatencyMin = 300ms、上限 srtLatencyMax = 3000ms。WHIP 没有 TSBPD 概念,它的等价物是 mmx 侧的 RTP 重排/重传缓冲区(WebRTCInboundRTPBufferSize),同样在连接建立时定长、会话生命周期内不可变。

4.3 用"加时延"换"抗丢包":自适应调节

这条链上最关键的设计是:服务端会主动用"加时延"去换"少丢包"。

  • SRT 自适应接收延迟(srtLatencyAutoTune,默认开):按每分钟的不可恢复丢包事件逐级调节接收窗口——丢包率 > degradeRaisePct(默认 0.8%)就加 degradeRaiseLatencyStep(默认 +200ms),< degradeLowerPct(默认 0.3%)就减 degradeLowerLatencyStep(默认 −100ms),中间死区不动(旧的 srtLatencyRaisePct/srtLatencyStep 等字段已弃用,节点启动时会打警告),最终钳制在 [srtLatencyMin, srtLatencyMax]。而且"升高"方向会主动断开让推流端重连,好让更大的窗口立即生效(它救的是正在发生、观众可见的丢包);"降低"方向保持机会主义,只为省几百毫秒不去打断健康的流。
  • 高丢包告警与升降阈值:SRT 不可恢复丢包率 / RTP 丢包率 >1.2% 报警、<0.5% 报恢复;>0.8% 升接收延迟、<0.3% 降接收延迟,快升慢减。
  • WHIP 侧的降级与缓冲:按丢包率升降 simulcast 层级(ABR 自适应),以及播放端自适应的 jitter buffer——网络越抖,缓冲越长,播放节奏越稳。

所以"SLA 里的时延稳定性"和"SLA 里的丢包率"其实是一体两面:丢包是因,时延抖动和峰值是果。看时延曲线突然出现峰值时,第一件事往往就是去同一时间段看丢包率曲线有没有对应事件。

flowchart LR
    A["丢包率升高"] --> B["重传/重排压力增大,不可恢复丢包带来卡顿"]
    B --> C["服务端自适应:升高接收窗口 / 降 simulcast 层"]
    C --> D["时延上升,换取抗丢包(快升慢减)"]
    E["窗口/时延调得太小"] --> F["重传赶不上,包被判超期丢弃"] --> A

4.4 反过来:时延调太小也会丢包

这条因果是双向的,容易被忽略。TSBPD 窗口本质上是一个"给重传留多少余量"的预算:窗口设得过小,重传还没回来、包就被判超期丢弃,反而制造出丢包。文档里有一个具体取舍可以佐证——为弱网特意把窗口放大,是因为在 RTT ~30ms 时,200ms 窗口约等于 6.7 倍 RTT,余量一变小,弱网上行丢包/卡顿概率就上升。这就是为什么"越低延迟越好"是个危险的直觉:低延迟和抗丢包是同一个旋钮的两端。


5. console 后台:按天/周/月看平均与峰值曲线

上面讲的两条曲线(时延稳定性、丢包率)在 console 里都有落点。本集新上线的是"RTP丢包率"面板,专门把你名下所有推流的丢包率按天/周/月聚合成平均与峰值两条曲线——它同时也是排查时延峰值的第一现场。

5.1 在哪看

登录 console,进入「推流」Tab,在推流会话列表下方就是「RTP丢包率」区块。顶部三个筛选:

  • App:全部 App,或某一个 App。
  • 聚合粒度:按天 / 按周(以周一为桶标签)/ 按月。
  • 时间范围:7 / 30 / 90 / 365 天。

5.2 口径:为什么"SRT 和 WHIP"合成一条

面板标题统一叫 RTP丢包率,但底层对两种推流协议取的是各自有意义的那个量:

  • SRT 推流 → 统计 SRT 不可恢复丢包率(ARQ 修复不了、观众真正能感知到的那部分)。
  • WHIP 推流 → 统计 RTP 丢包率。

两者在服务端合并成同一条曲线,所以不管你的流走哪种协议,看到的都是同一件事:"推流上行到底丢了多少"。数据源是两条全量序列:srt_loss_samples(按分钟)与 whip_loss_samples(按 10 秒,新增),而不是只存"超阈值样本"的那张表——这样正常时段接近零的基线也能被画出来,曲线不会只有尖峰。

5.3 聚合:每个桶三个数

后端接口 GET /v1/loss/trend(granularity=day|week|month)对每个桶返回:

  • 平均(avgLossPct):桶内所有样本的均值——看基线。
  • 峰值(maxLossPct):桶内最坏的单次采样——看最坏事件。
  • 样本数(sampleCount):这个桶里积累了多少个采样——看这个数字可不可信(样本太少时,平均和峰值都别太当真)。

console 面板把平均和峰值画成两条 sparkline 曲线,配一组汇总卡(整体平均、整体峰值、总样本数)和一张逐桶明细表,可以直接对着时间点读。

5.4 怎么把两条曲线读到一起

  • 平均看趋势:平均曲线整体往上平移,说明上行链路在持续变差。
  • 峰值看事件:某个桶峰值突然拉高,是一次网络抖动/重连瞬态;配合样本数判断它是"一次尖峰"还是"持续高丢包"。
  • 和时延对着看:发现时延出现峰值时,切到同一时间段看丢包峰值——如果两者时间对得上,基本可以判定"是上行丢包抬高了时延",而不是播放端或边缘的问题。

需要说明两个口径边界:一是平均是等权样本均值(SRT 分钟样本与 WHIP 10s 样本各计一次),它反映的是采样点的平均、不是严格按时间加权的平均;二是 SRT 全量序列按分钟落库、分钟内是"最后一个 10s 采样胜出",所以极短的分钟级尖峰可能被平滑掉。要更细的排序尖峰,应回到原始采样或提高采样密度,而不是在聚合层"猜"。


6. 框图

6.1 同一组数据,平均值和峰值/百分位讲出两个故事

一组时延样本(100 次播放):98 次 100ms,2 次 5000ms。

视角 计算 结论
看平均 (98 × 100 + 2 × 5000) / 100 = 198ms "体验相当不错"
看峰值和百分位 峰值 = 5000ms;P99 = 5000ms "大多数没问题,但存在明显长尾"

平均值把长尾"平掉"了,峰值/百分位把它显性摆出来。

6.2 丢包与时延的双向链

flowchart TD
    A["丢包率升高"] --> B["重传(ARQ/NACK)多花一个 RTT<br/>乱序重排等待<br/>不可恢复丢包 → 等关键帧 → 卡顿"]
    B --> C["时延上升 / 时延抖动变大(峰值出现)"]
    C --> D["服务端自适应:加大接收窗口 / 降 simulcast 层<br/>(用加时延换抗丢包)"]
    D -.->|窗口调太小| E["重传赶不上 → 反而丢包"]
    E -.-> A

6.3 console 曲线怎么读

每个桶都给出平均 + 峰值 + 样本数三个数:

数值 读法 含义
平均平移 整体趋势 链路在持续变好或变坏
峰值拉高 一次最坏事件 网络抖动/重连瞬态
样本太少 数字不可信 平均和峰值都别太当真

排查动作:时延出现峰值 → 同一时间段看丢包峰值有没有对应。


7. 结尾与下集预告(逐字稿要点)

这一集的核心就一句话:稳定性问题不能用平均值回答——平均看基线,峰值看最坏,百分位看"最坏有多常见",三者缺一不可。而时延和丢包率是同一根链条的两端:丢包会推高时延和抖动,服务端又会主动用"加一点时延"去换抗丢包,所以低延迟和抗丢包是同一个旋钮的两端,不是两个都可以无限的指标。console 里的"RTP丢包率"面板把 SRT 不可恢复丢包率与 WHIP RTP 丢包率合并成一条曲线,按天/周/月给出平均与峰值——它就是排查时延峰值的第一现场。

下一集,我们从"延迟稳不稳"转到另一个同样容易被平均值误导的话题——怎么量化视频质量。