# RTC 入门 06：怎样从收发记录看出延迟在增长

作者：Hex4C59 · 发布于 2026-10-15

原文：https://hex4c59.cc/articles/rtc-06-delay-trend/


[第五篇](https://hex4c59.cc/articles/rtc-05-send-queue/)给定了传输能力，因此可以算出队列怎样增长。真实通话里，这个能力不是直接已知的。接收端看到包越来越晚，怎样从记录里提出一个有依据的判断？

先把问题缩小：手机发出两个包，相隔 20 毫秒；电脑收到它们，相隔 23 毫秒。在时钟和记录条件满足时，后一包在两个记录点之间比前一包多花了 3 毫秒。这个差值还没有告诉我们任一包的总耗时，也没有定位哪台设备在排队。

本篇从这一次相减开始，推到一段连续记录。读完应当能计算相邻变化和相对首包的变化，分清持续增长、一次波动与发送前排队，并说明什么时候还不能把结果当作网络拥塞证据。需要第三篇的包序与时间戳概念，以及第五篇的发送队列概念；公式只用加减法。

所有包号、时刻和时钟误差均为教学构造。文末 Python 实验不读取真实日志、不发包，也不实现带宽估计或拥塞控制算法。

## 先确定两个时刻分别记录在哪里

“发送时刻”可能指编码结束、进入 Pacer、交给底层传输，甚至硬件实际发送。接收侧也可能在系统收到包时记录，或等应用线程取出数据后才记录。它们测到的区间不同。

本篇把起点设在**包离开 Pacer、提交给底层传输的记录点**，终点设在**接收端取得该包并立即记录的接口**。这不是精确到物理链路的测量；两个接口之间还可能包含系统和路径中的等待。后文的“传输耗时”，都指这两个明确记录点之间的耗时。

发送与接收记录按同一包配对。假设都是同一来源、120 字节 IP 包，没有丢失、重传、重复、编号回绕或重启。先按发送顺序比较，主例中也按序到达。

还要暂时给出时钟条件：两端都用稳定时钟，单位均为毫秒，走速相同，没有跳变，记录误差可忽略；两台机器显示的数字可以相差一个固定值。后面会专门检查时钟走速不同时会怎样。

这些条件决定了算术结果能够解释什么。RFC 7679 在单向延迟测量中区分时钟关系、记录误差以及主机时间与线路时间的差异；RFC 8888 也要求拥塞反馈使用的收发记录尽可能接近实际发送和接收位置。[^oneway][^feedback]

## 两个间隔相减，先得到什么

下面是两个不同本地时钟给出的记录。发送端的 0 与接收端的 1000，没有事先对齐：

| 包号 | 发送端记录 | 接收端记录 |
| --- | --- | --- |
| 300 | 0 毫秒 | 1000 毫秒 |
| 301 | 20 毫秒 | 1023 毫秒 |

先在每台机器内部算间隔：

```text
发送间隔 = 20 − 0 = 20 毫秒
到达间隔 = 1023 − 1000 = 23 毫秒
相邻耗时变化 = 23 − 20 = +3 毫秒
```

如果两包花费的时间相同，到达间隔也应保持 20 毫秒。现在到达间隔多了 3 毫秒，说明 301 比 300 多花了 3 毫秒。IP 包时延变化指标也使用两个包的单向延迟之差来描述这类变化。[^variation]

这里不能直接把 `1000 − 0` 当作 300 的单向延迟，因为两端时间基准未知。也不能说“301 总共花了 3 毫秒”：3 是两包耗时的差。

### 为什么固定偏移能抵消

给这两个包的发送记录取名 `sA`、`sB`，接收记录取名 `rA`、`rB`。真正发生在两个记录点之间的耗时记为 `dA`、`dB`；接收时钟相对发送时钟的固定偏移记为 `C`。

```text
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。仅凭这份记录，无法从甲、乙中选出实际情况。

若已有足够准确的两端时间关系，才可以进一步计算绝对单向耗时；“固定偏移在做差时消掉”没有提供这种额外信息。[^oneway]

## 到达间隔没变大，耗时也可能一直增长

把记录扩成六包。发送间隔始终是 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 毫秒。相对首包，也可以直接计算：

```text
305 相对 300 的变化
= 接收端经过的时间 − 发送端经过的时间
= (1115 − 1000) − (100 − 0)
= 115 − 100
= +15 毫秒
```

到达间隔一直是 23，没有从 23 涨到 26、29，耗时却持续增加。观察趋势时要比较到达间隔与发送间隔，不能只检查到达间隔本身是否越来越大。

这组结果与持续积压的表现相容，值得继续检查队列与实际发送量。它没有证明增加一定发生在某台路由器，也没有给出可用容量是多少。RFC 8836 将延迟及其变化作为拥塞控制需要考虑的信息之一，控制还要应对路径变化、应用受限和竞争流量等条件。[^control]

## 一次尖峰，后面可能恢复

换一份独立记录：发送仍每隔 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 毫秒，那描述的是另一种波动总量，不能当作首尾耗时增长。

下面在同一纵轴上比较持续增长、一次尖峰和后面要解释的发送前排队。先看前两种：它们都出现正差，后续记录却不同。

<figure class="interactive-diagram" style="margin:1.8rem 0;padding:1.3rem 0;border-top:1px solid var(--code-border);border-bottom:1px solid var(--code-border);container-type:inline-size">
    <iframe id="interactive-diagram-37a85e4c11f3e535e396d03217a8b704-0" src="https://hex4c59.cc/articles/rtc-06-delay-trend/delay-patterns.html" title="比较三组收发记录：纵轴是相对首包的耗时变化，不是绝对单向延迟" width="100%" height="1020" loading="lazy" sandbox="allow-scripts" style="display:block;width:100%;border:0;background:transparent;--figure-height:1020px;--figure-mobile-height:1140px;--figure-compact-height:1060px"></iframe>
    <figcaption style="font-size:.78rem;line-height:1.8;margin-top:.7rem;color:var(--content-secondary)">比较三组收发记录：纵轴是相对首包的耗时变化，不是绝对单向延迟 · <a href="https://hex4c59.cc/articles/rtc-06-delay-trend/delay-patterns.html" target="_blank" rel="noopener">在独立窗口中查看</a></figcaption>
  </figure>


这些曲线不使用实际测量数据，也没有把 6 包或 3 毫秒设成检测拥塞的阈值。这里只比较所给窗口里的变化形状。

## 不能用媒体时间替代实际发送时间

第三篇介绍的 RTP 时间戳描述媒体的采样位置，不是包交给网络的时刻。即使先把 RTP 刻度换成毫秒，也没有消除编码、排队和发送调度造成的差异。[^rtp]

另设一组记录：媒体仍每 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 毫秒。第五篇的本机队列记录仍然需要保留，只是不能与另一段区间混为一谈。

## 运行相同记录，再改变一个条件

[下载 https://hex4c59.cc/articles/rtc-06-delay-trend/delay_trace.py](delay_trace.py)。需要 Python 3.9 或更新版本，没有第三方依赖。在保存脚本的目录运行：

```bash
python3 delay_trace.py
```

2026 年 10 月 15 日，使用 Python 3.9.6 核对默认六包案例，关键结果为：

```text
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 毫秒会变吗？

```bash
python3 delay_trace.py --receiver-offset-ms 5000
```

接收记录变成 6000、6023 等，但间隔保持，最后仍为 15 毫秒。接着分别运行一次尖峰与发送前排队：

```bash
python3 delay_trace.py --case spike
python3 delay_trace.py --case sender-queue
```

两次最后的相对变化都是零。尖峰案例的带符号变化总和为零、绝对值总和为 30 毫秒；发送前排队案例的实际发送跨度和接收跨度都是 150 毫秒，媒体跨度则仍为 100 毫秒。它们变成零的原因不同，要对照逐行记录解释。

也可查看稳定案例，并导出完整 CSV：

```bash
python3 delay_trace.py --case steady
python3 delay_trace.py --csv delay-trace.csv
```

默认只打印结果；指定 `--csv` 才新建文件，已有同名文件时拒绝覆盖。CSV 保留包号、120 字节的假设包长、媒体位置、收发记录与两种变化。长窗口只在终端展示部分行，CSV 则保留全部行。

脚本用有理数进行时间计算，生成的记录本来就已正确配对。它没有解析 RTP、捕获系统时钟、读取真实抓包，也没有计算可用带宽。

## 进一步检查：时钟走速和乱序

### 固定偏移消掉了，走速误差仍可能留下

前面的推导假设两端时钟走速相同。“都显示毫秒”只说明单位名称一致，还不能保证它们实际走得一样快。单调时钟不向后跳，也不等于两台机器的时钟走速完全相同。时钟频差会影响延迟变化测量。[^clocks]

用一个独立的构造条件检查：包的真实耗时稳定，接收端时钟却比发送端快 100 ppm。ppm 表示百万分之一，100 ppm 相当于每经过 1,000,000 份理想时间，多记 100 份。100 秒内会多记：

```text
100 秒 × 100 ÷ 1,000,000 = 0.010 秒 = 10 毫秒
```

100 ppm 是本例人为选择的误差，不是某类设备的典型值。运行稳定案例，生成 5001 包，让发送跨度覆盖 100 秒：

```bash
python3 delay_trace.py --case steady --packets 5001 --receiver-ppm 100
```

关键结果为：

```text
Send span: 100000 ms; receive span: 100010 ms
Last relative delay change: 10 ms
```

这 10 毫秒来自本次注入的时钟走速误差。仅加固定偏移无法消除它；要把真实测量解释成路径耗时变化，还需处理或约束时钟走速与记录误差。程序在此只演示误差影响，没有实现时钟校准。

### 先配对，再明确包的比较顺序

第三篇说明，包可以乱序。下面的案例仍按发送顺序保留每个包的身份，但把 301 的接收记录推迟到 1045：

```bash
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 毫秒 |

1. A→B、B→C 的发送间隔、到达间隔和耗时变化分别是多少？
2. A、B、C 相对 A 的变化分别是多少？C 回到零，能否说其绝对传输耗时为零？
3. 接收端所有记录再统一加 2000 毫秒，哪些结果会改变？若只把 B 的记录额外推迟 2 毫秒，两个相邻变化又分别是多少？
4. 另一个独立案例中，媒体间隔 20 毫秒、实际提交间隔 28 毫秒、到达间隔 28 毫秒。提交之后的耗时变化是多少？若用媒体间隔相减，会把哪段等待混进来？

<details>
<summary>展开逐步计算与回看位置</summary>

1. 两个发送间隔都是 25 毫秒；到达间隔分别为 30、20 毫秒，所以变化为 +5、−5 毫秒。若把接收记录直接减发送记录，回看固定偏移那一节。
2. 相对变化为 0、+5、0。C 回到 A 的耗时水平，A 的绝对耗时仍未知。不能从相对零推出绝对零；绝对值相加得到的 10 毫秒也不是首尾增长。
3. 统一加 2000 只改变接收时钟显示的数字，间隔与变化保持。若只让 B 再晚 2 毫秒，到达间隔变成 32、18，变化是 +7、−7。它们仍在 C 处抵消，但中间尖峰更高，不能只看最后一行。
4. 提交之后是 `28 − 28 = 0`。若错用媒体间隔，会得到 `28 − 20 = +8`，把提交前多出来的等待混进后面的测量区间。回看媒体位置与实际发送时刻的表。

</details>

## 把变化变成可检查的证据

一份能继续用于诊断的记录，至少应保留包身份、包大小、媒体位置、所选发送与接收记录点、时间单位、时钟来源，以及记录缺失或重传等标记。实际存在多次发送时，还要分清接收结果对应原发送还是重传；本篇的单次发送假设不能直接覆盖它们。

可以按下面的顺序检查结论：

| 先确认什么 | 再解释什么 |
| --- | --- |
| 收发确实对应同一个包 | 才能比较它的两个记录点 |
| 起止位置稳定，时钟与精度适用 | 才能将间隔差解释为该范围内的耗时变化 |
| 多个相邻变化是否持续、是否回落 | 才能区分窗口里的增长和尖峰 |
| 本机队列、实际发送量及其他反馈 | 才能进一步检查原因与发送调整 |
| 数据最终是否赶上播放 | 才能把传输记录连回用户的通话体验 |

持续增长与排队相容，但单靠这条曲线不能确定拥塞位置或网络容量；没有增长也可能有很高的固定延迟。包是否丢失、接收吞吐是多少，仍需与相同窗口里的其他记录一起看。RFC 8888 提供的是反馈信息，并不替代使用这些信息的控制算法。[^feedback]

这篇能够算出的，是明确条件下两个记录点之间的耗时变化：先在各自时钟里求间隔，再相减；继续带符号累计，才能看见首尾关系。把未知的绝对耗时保留下来，也把发送前排队、时钟误差与乱序分别标出，后续判断才有可追溯的依据。

接下来会把这些媒体路径知识放回建立一通通话的过程：两端怎样交换会话信息、找到能互相到达的路径，再开始双端实验。

[继续第七篇：两端怎样找到能通话的网络路径](https://hex4c59.cc/articles/rtc-07-connection-setup/) · [回看第五篇：带宽没跟上，声音为什么越排越晚](https://hex4c59.cc/articles/rtc-05-send-queue/) · [系列目录](https://hex4c59.cc/articles/rtc-from-zero/)

## 参考资料

资料核对于 2026 年 10 月 15 日。本文只借助公开资料解释测量边界与基本数量关系；案例、图和脚本均为自行构造的教学记录，不是正式 RFC 测量实现、GCC 实现或真实网络测试。

[^variation]: [RFC 3393 §1—§2：IP 包时延变化](https://datatracker.ietf.org/doc/html/rfc3393#section-2)，2002 年 11 月发布，将两个选定包的单向延迟之差定义为相应时延变化，并讨论选样与测量条件。
[^oneway]: [RFC 7679 §3.4 与 §3.7：单向延迟定义和测量误差](https://www.rfc-editor.org/rfc/rfc7679.html#section-3.7)，2016 年 1 月发布，说明时钟同步、主机与线路时间差、记录误差等边界。
[^clocks]: [RFC 3393 §2.7.1：两个时钟的测量误差](https://datatracker.ietf.org/doc/html/rfc3393#section-2.7.1)，讨论固定偏移的抵消以及时钟走速误差对变化测量的影响。
[^feedback]: [RFC 8888 §3：拥塞控制反馈所需的记录](https://www.rfc-editor.org/rfc/rfc8888.html#section-3)，2021 年 1 月发布，说明包身份、发送记录、接收结果及时间信息的测量位置。
[^rtp]: [RFC 3550 §5.1：RTP 时间戳](https://www.rfc-editor.org/rfc/rfc3550.html#section-5.1)，2003 年 7 月发布，说明时间戳反映媒体的采样时刻。
[^control]: [RFC 8836 §2：实时交互媒体的拥塞控制要求](https://www.rfc-editor.org/rfc/rfc8836.html#section-2)，2021 年 1 月发布，讨论延迟、网络条件变化与应用受限等约束。

