第 06 篇
怎样从收发记录看出延迟在增长
用发送间隔和到达间隔计算耗时变化,解释固定时钟偏移为何能抵消,再用连续记录区分增长、尖峰和发送前排队,检查乱序、时钟走速与记录点带来的误判。
Hex4C59 11 分钟阅读
软件工程·原创·概念与设计
第五篇给定了传输能力,因此可以算出队列怎样增长。真实通话里,这个能力不是直接已知的。接收端看到包越来越晚,怎样从记录里提出一个有依据的判断?
先把问题缩小:手机发出两个包,相隔 20 毫秒;电脑收到它们,相隔 23 毫秒。在时钟和记录条件满足时,后一包在两个记录点之间比前一包多花了 3 毫秒。这个差值还没有告诉我们任一包的总耗时,也没有定位哪台设备在排队。
本篇从这一次相减开始,推到一段连续记录。读完应当能计算相邻变化和相对首包的变化,分清持续增长、一次波动与发送前排队,并说明什么时候还不能把结果当作网络拥塞证据。需要第三篇的包序与时间戳概念,以及第五篇的发送队列概念;公式只用加减法。
所有包号、时刻和时钟误差均为教学构造。文末 Python 实验不读取真实日志、不发包,也不实现带宽估计或拥塞控制算法。
先确定两个时刻分别记录在哪里 #
“发送时刻”可能指编码结束、进入 Pacer、交给底层传输,甚至硬件实际发送。接收侧也可能在系统收到包时记录,或等应用线程取出数据后才记录。它们测到的区间不同。
本篇把起点设在包离开 Pacer、提交给底层传输的记录点,终点设在接收端取得该包并立即记录的接口。这不是精确到物理链路的测量;两个接口之间还可能包含系统和路径中的等待。后文的“传输耗时”,都指这两个明确记录点之间的耗时。
发送与接收记录按同一包配对。假设都是同一来源、120 字节 IP 包,没有丢失、重传、重复、编号回绕或重启。先按发送顺序比较,主例中也按序到达。
还要暂时给出时钟条件:两端都用稳定时钟,单位均为毫秒,走速相同,没有跳变,记录误差可忽略;两台机器显示的数字可以相差一个固定值。后面会专门检查时钟走速不同时会怎样。
这些条件决定了算术结果能够解释什么。RFC 7679 在单向延迟测量中区分时钟关系、记录误差以及主机时间与线路时间的差异;RFC 8888 也要求拥塞反馈使用的收发记录尽可能接近实际发送和接收位置。12
两个间隔相减,先得到什么 #
下面是两个不同本地时钟给出的记录。发送端的 0 与接收端的 1000,没有事先对齐:
| 包号 | 发送端记录 | 接收端记录 |
|---|---|---|
| 300 | 0 毫秒 | 1000 毫秒 |
| 301 | 20 毫秒 | 1023 毫秒 |
先在每台机器内部算间隔:
发送间隔 = 20 − 0 = 20 毫秒
到达间隔 = 1023 − 1000 = 23 毫秒
相邻耗时变化 = 23 − 20 = +3 毫秒如果两包花费的时间相同,到达间隔也应保持 20 毫秒。现在到达间隔多了 3 毫秒,说明 301 比 300 多花了 3 毫秒。IP 包时延变化指标也使用两个包的单向延迟之差来描述这类变化。3
这里不能直接把 1000 − 0 当作 300 的单向延迟,因为两端时间基准未知。也不能说“301 总共花了 3 毫秒”:3 是两包耗时的差。
为什么固定偏移能抵消 #
给这两个包的发送记录取名 sA、sB,接收记录取名 rA、rB。真正发生在两个记录点之间的耗时记为 dA、dB;接收时钟相对发送时钟的固定偏移记为 C。
rA = sA + dA + C
rB = sB + dB + C
rB − rA = (sB − sA) + (dB − dA)
dB − dA = (rB − rA) − (sB − sA)第二行减第一行时,同一个 C 消失了。先算每台机器内部的间隔,再比较两个间隔,就不必先知道固定偏移是多少。
但 dA 仍然未知。下面两组另外假设的条件,都能生成前面的同一份记录:
| 可能条件 | 300 的耗时 | 301 的耗时 | 固定时钟偏移 C |
|---|---|---|---|
| 甲 | 40 毫秒 | 43 毫秒 | 960 毫秒 |
| 乙 | 100 毫秒 | 103 毫秒 | 900 毫秒 |
甲有 0 + 40 + 960 = 1000,乙有 0 + 100 + 900 = 1000;后一包也分别得到 1023。仅凭这份记录,无法从甲、乙中选出实际情况。
若已有足够准确的两端时间关系,才可以进一步计算绝对单向耗时;“固定偏移在做差时消掉”没有提供这种额外信息。1
到达间隔没变大,耗时也可能一直增长 #
把记录扩成六包。发送间隔始终是 20 毫秒,到达间隔始终是 23 毫秒:
| 包号 | 发送记录 | 接收记录 | 相比上一包多耗时 | 相对包 300 的耗时变化 |
|---|---|---|---|---|
| 300 | 0 | 1000 | — | 0 |
| 301 | 20 | 1023 | +3 | +3 |
| 302 | 40 | 1046 | +3 | +6 |
| 303 | 60 | 1069 | +3 | +9 |
| 304 | 80 | 1092 | +3 | +12 |
| 305 | 100 | 1115 | +3 | +15 |
表内时间单位都是毫秒。每一步多花 3 毫秒,五个间隔累计就是 15 毫秒。相对首包,也可以直接计算:
305 相对 300 的变化
= 接收端经过的时间 − 发送端经过的时间
= (1115 − 1000) − (100 − 0)
= 115 − 100
= +15 毫秒到达间隔一直是 23,没有从 23 涨到 26、29,耗时却持续增加。观察趋势时要比较到达间隔与发送间隔,不能只检查到达间隔本身是否越来越大。
这组结果与持续积压的表现相容,值得继续检查队列与实际发送量。它没有证明增加一定发生在某台路由器,也没有给出可用容量是多少。RFC 8836 将延迟及其变化作为拥塞控制需要考虑的信息之一,控制还要应对路径变化、应用受限和竞争流量等条件。4
一次尖峰,后面可能恢复 #
换一份独立记录:发送仍每隔 20 毫秒,但只有 302 的到达比原来晚了 15 毫秒。
| 包号 | 发送记录 | 接收记录 | 到达间隔 | 相邻耗时变化 | 相对 300 的变化 |
|---|---|---|---|---|---|
| 300 | 0 | 1000 | — | — | 0 |
| 301 | 20 | 1020 | 20 | 0 | 0 |
| 302 | 40 | 1055 | 35 | +15 | +15 |
| 303 | 60 | 1060 | 5 | −15 | 0 |
| 304 | 80 | 1080 | 20 | 0 | 0 |
| 305 | 100 | 1100 | 20 | 0 | 0 |
303 比 302 少花 15 毫秒,因此到达间隔被压缩成 5 毫秒;这不意味着 303 的绝对耗时为负。它只是回到首包的耗时水平,首包本身仍可能很慢。
相邻变化要带符号累加:0 + 15 − 15 + 0 + 0 = 0。如果先取绝对值再相加,会得到 30 毫秒,那描述的是另一种波动总量,不能当作首尾耗时增长。
下面在同一纵轴上比较持续增长、一次尖峰和后面要解释的发送前排队。先看前两种:它们都出现正差,后续记录却不同。
这些曲线不使用实际测量数据,也没有把 6 包或 3 毫秒设成检测拥塞的阈值。这里只比较所给窗口里的变化形状。
不能用媒体时间替代实际发送时间 #
第三篇介绍的 RTP 时间戳描述媒体的采样位置,不是包交给网络的时刻。即使先把 RTP 刻度换成毫秒,也没有消除编码、排队和发送调度造成的差异。5
另设一组记录:媒体仍每 20 毫秒产生一段,但发送前的队列让包每隔 30 毫秒才提交给底层。提交之后到接收记录点的耗时保持不变。
| 包号 | 媒体位置,毫秒 | 实际提交记录 | 接收记录 |
|---|---|---|---|
| 300 | 0 | 0 | 1000 |
| 301 | 20 | 30 | 1030 |
| 302 | 40 | 60 | 1060 |
| 303 | 60 | 90 | 1090 |
| 304 | 80 | 120 | 1120 |
| 305 | 100 | 150 | 1150 |
用实际提交间隔算,30 − 30 = 0:提交之后这段区间,没有观察到耗时增长。
如果错用媒体间隔,就会算成 30 − 20 = +10,到 305 累计成 50 毫秒。这个差额来自本例提交之前的等待,不能直接贴成提交之后的网络增长。
零变化也没有说明通话没有问题。305 的媒体位置是 100 毫秒,却到 150 毫秒才提交;发送端已经多等了 50 毫秒。第五篇的本机队列记录仍然需要保留,只是不能与另一段区间混为一谈。
运行相同记录,再改变一个条件 #
下载 delay_trace.py。需要 Python 3.9 或更新版本,没有第三方依赖。在保存脚本的目录运行:
python3 delay_trace.py2026 年 10 月 15 日,使用 Python 3.9.6 核对默认六包案例,关键结果为:
Case: growing; matched packets: 6
Send span: 100 ms; receive span: 115 ms
Last relative delay change: 15 ms
Sum of signed changes: 15 ms; sum of absolute changes: 15 ms
Absolute one-way delay: unknown; this program is not a bandwidth estimator.change_ms 是相邻变化,relative_ms 是相对包 300 的变化。首包的相邻间隔没有前一个样本可比,所以显示 -;它的相对变化定义为零,没有声称它的绝对耗时为零。
先预测:给接收端每个时间戳都加 5000 毫秒,最后的 15 毫秒会变吗?
python3 delay_trace.py --receiver-offset-ms 5000接收记录变成 6000、6023 等,但间隔保持,最后仍为 15 毫秒。接着分别运行一次尖峰与发送前排队:
python3 delay_trace.py --case spike
python3 delay_trace.py --case sender-queue两次最后的相对变化都是零。尖峰案例的带符号变化总和为零、绝对值总和为 30 毫秒;发送前排队案例的实际发送跨度和接收跨度都是 150 毫秒,媒体跨度则仍为 100 毫秒。它们变成零的原因不同,要对照逐行记录解释。
也可查看稳定案例,并导出完整 CSV:
python3 delay_trace.py --case steady
python3 delay_trace.py --csv delay-trace.csv默认只打印结果;指定 --csv 才新建文件,已有同名文件时拒绝覆盖。CSV 保留包号、120 字节的假设包长、媒体位置、收发记录与两种变化。长窗口只在终端展示部分行,CSV 则保留全部行。
脚本用有理数进行时间计算,生成的记录本来就已正确配对。它没有解析 RTP、捕获系统时钟、读取真实抓包,也没有计算可用带宽。
进一步检查:时钟走速和乱序 #
固定偏移消掉了,走速误差仍可能留下 #
前面的推导假设两端时钟走速相同。“都显示毫秒”只说明单位名称一致,还不能保证它们实际走得一样快。单调时钟不向后跳,也不等于两台机器的时钟走速完全相同。时钟频差会影响延迟变化测量。6
用一个独立的构造条件检查:包的真实耗时稳定,接收端时钟却比发送端快 100 ppm。ppm 表示百万分之一,100 ppm 相当于每经过 1,000,000 份理想时间,多记 100 份。100 秒内会多记:
100 秒 × 100 ÷ 1,000,000 = 0.010 秒 = 10 毫秒100 ppm 是本例人为选择的误差,不是某类设备的典型值。运行稳定案例,生成 5001 包,让发送跨度覆盖 100 秒:
python3 delay_trace.py --case steady --packets 5001 --receiver-ppm 100关键结果为:
Send span: 100000 ms; receive span: 100010 ms
Last relative delay change: 10 ms这 10 毫秒来自本次注入的时钟走速误差。仅加固定偏移无法消除它;要把真实测量解释成路径耗时变化,还需处理或约束时钟走速与记录误差。程序在此只演示误差影响,没有实现时钟校准。
先配对,再明确包的比较顺序 #
第三篇说明,包可以乱序。下面的案例仍按发送顺序保留每个包的身份,但把 301 的接收记录推迟到 1045:
python3 delay_trace.py --case reorder实际到达顺序变为 300, 302, 301, 303, 304, 305。按发送顺序检查 301→302,会得到发送间隔 20 毫秒、接收记录差 1040 − 1045 = −5 毫秒,变化为 −25 毫秒。
这个负接收差表示:在选定的发送顺序里,后发的 302 反而先到。它不是负的单包传输时间,也不是一段按接收时间向前推进的正常间隔。程序保留带符号结果,并在 CSV 中标为 receive_reversal。
完整估计器需要决定怎样分组、选样和处理乱序;本脚本不把这些带标记的相邻差送入某个拥塞判定。不能重新排序一列时间戳后,还沿用原来的另一列,导致收发包身份错配。
换一组记录,自己推算 #
前三题沿用相同时钟走速、固定偏移和可忽略记录误差的条件。三包均已正确配对,按序到达:
| 包号 | 发送端记录 | 接收端记录 |
|---|---|---|
| A | 10 毫秒 | 5010 毫秒 |
| B | 35 毫秒 | 5040 毫秒 |
| C | 60 毫秒 | 5060 毫秒 |
- A→B、B→C 的发送间隔、到达间隔和耗时变化分别是多少?
- A、B、C 相对 A 的变化分别是多少?C 回到零,能否说其绝对传输耗时为零?
- 接收端所有记录再统一加 2000 毫秒,哪些结果会改变?若只把 B 的记录额外推迟 2 毫秒,两个相邻变化又分别是多少?
- 另一个独立案例中,媒体间隔 20 毫秒、实际提交间隔 28 毫秒、到达间隔 28 毫秒。提交之后的耗时变化是多少?若用媒体间隔相减,会把哪段等待混进来?
展开逐步计算与回看位置
- 两个发送间隔都是 25 毫秒;到达间隔分别为 30、20 毫秒,所以变化为 +5、−5 毫秒。若把接收记录直接减发送记录,回看固定偏移那一节。
- 相对变化为 0、+5、0。C 回到 A 的耗时水平,A 的绝对耗时仍未知。不能从相对零推出绝对零;绝对值相加得到的 10 毫秒也不是首尾增长。
- 统一加 2000 只改变接收时钟显示的数字,间隔与变化保持。若只让 B 再晚 2 毫秒,到达间隔变成 32、18,变化是 +7、−7。它们仍在 C 处抵消,但中间尖峰更高,不能只看最后一行。
- 提交之后是
28 − 28 = 0。若错用媒体间隔,会得到28 − 20 = +8,把提交前多出来的等待混进后面的测量区间。回看媒体位置与实际发送时刻的表。
把变化变成可检查的证据 #
一份能继续用于诊断的记录,至少应保留包身份、包大小、媒体位置、所选发送与接收记录点、时间单位、时钟来源,以及记录缺失或重传等标记。实际存在多次发送时,还要分清接收结果对应原发送还是重传;本篇的单次发送假设不能直接覆盖它们。
可以按下面的顺序检查结论:
| 先确认什么 | 再解释什么 |
|---|---|
| 收发确实对应同一个包 | 才能比较它的两个记录点 |
| 起止位置稳定,时钟与精度适用 | 才能将间隔差解释为该范围内的耗时变化 |
| 多个相邻变化是否持续、是否回落 | 才能区分窗口里的增长和尖峰 |
| 本机队列、实际发送量及其他反馈 | 才能进一步检查原因与发送调整 |
| 数据最终是否赶上播放 | 才能把传输记录连回用户的通话体验 |
持续增长与排队相容,但单靠这条曲线不能确定拥塞位置或网络容量;没有增长也可能有很高的固定延迟。包是否丢失、接收吞吐是多少,仍需与相同窗口里的其他记录一起看。RFC 8888 提供的是反馈信息,并不替代使用这些信息的控制算法。2
这篇能够算出的,是明确条件下两个记录点之间的耗时变化:先在各自时钟里求间隔,再相减;继续带符号累计,才能看见首尾关系。把未知的绝对耗时保留下来,也把发送前排队、时钟误差与乱序分别标出,后续判断才有可追溯的依据。
接下来会把这些媒体路径知识放回建立一通通话的过程:两端怎样交换会话信息、找到能互相到达的路径,再开始双端实验。
继续第七篇:两端怎样找到能通话的网络路径 · 回看第五篇:带宽没跟上,声音为什么越排越晚 · 系列目录
参考资料 #
资料核对于 2026 年 10 月 15 日。本文只借助公开资料解释测量边界与基本数量关系;案例、图和脚本均为自行构造的教学记录,不是正式 RFC 测量实现、GCC 实现或真实网络测试。
RFC 7679 §3.4 与 §3.7:单向延迟定义和测量误差,2016 年 1 月发布,说明时钟同步、主机与线路时间差、记录误差等边界。 ↩︎ ↩︎
RFC 8888 §3:拥塞控制反馈所需的记录,2021 年 1 月发布,说明包身份、发送记录、接收结果及时间信息的测量位置。 ↩︎ ↩︎
RFC 3393 §1—§2:IP 包时延变化,2002 年 11 月发布,将两个选定包的单向延迟之差定义为相应时延变化,并讨论选样与测量条件。 ↩︎
RFC 8836 §2:实时交互媒体的拥塞控制要求,2021 年 1 月发布,讨论延迟、网络条件变化与应用受限等约束。 ↩︎
RFC 3550 §5.1:RTP 时间戳,2003 年 7 月发布,说明时间戳反映媒体的采样时刻。 ↩︎
RFC 3393 §2.7.1:两个时钟的测量误差,讨论固定偏移的抵消以及时钟走速误差对变化测量的影响。 ↩︎
一起讨论
选中正文中的文字,可以针对这段内容发起讨论。
正在读取讨论…
在 GitHub 查看与管理讨论 ↗