bigywater.tech
网络协议 · 61

带宽时延积与套接字缓冲区:怎样把长肥管道填满

“长肥管道”(带宽大、延迟高)为什么常常跑不满?答案在带宽时延积:窗口小于 BDP,管道里就有空洞。这篇用 netlab 造一条 100 Mbit/s、RTT 100 ms 的链路,把接收缓冲区和发送缓冲区从小到大扫一遍,量吞吐,也量缓冲过大造成的排队延迟。

1,250,000 字节
100 Mbit/s × 100 ms 的带宽时延积
9.91 → 12.46 MB/s
接收缓冲区从 1024 KiB 增加到 2048 KiB:吞吐封顶在链路带宽
105 → 197 ms
接收缓冲区从 1024 KiB 增加到 4096 KiB 时的 srtt:多出来的窗口只是排队

BDP

实测 · 本机执行输出
  BDP = 12,500,000 字节/秒 × 0.1 秒 = 1,250,000 字节(约 1.19 MiB):要让管道始终是满的,发送方必须有这么多字节在路上,接收方的窗口至少要这么大
  接收窗口    64 KiB → 吞吐上限 = 窗口 ÷ RTT =    0.7 MB/s(   5.2 Mbit/s);链路上限 12.5 MB/s;取小者    0.7 MB/s(计算)
  接收窗口   256 KiB → 吞吐上限 = 窗口 ÷ RTT =    2.6 MB/s(  21.0 Mbit/s);链路上限 12.5 MB/s;取小者    2.6 MB/s(计算)
  接收窗口   512 KiB → 吞吐上限 = 窗口 ÷ RTT =    5.2 MB/s(  41.9 Mbit/s);链路上限 12.5 MB/s;取小者    5.2 MB/s(计算)
  接收窗口  1024 KiB → 吞吐上限 = 窗口 ÷ RTT =   10.5 MB/s(  83.9 Mbit/s);链路上限 12.5 MB/s;取小者   10.5 MB/s(计算)
  接收窗口  2048 KiB → 吞吐上限 = 窗口 ÷ RTT =   21.0 MB/s( 167.8 Mbit/s);链路上限 12.5 MB/s;取小者   12.5 MB/s(计算)
  接收窗口  4096 KiB → 吞吐上限 = 窗口 ÷ RTT =   41.9 MB/s( 335.5 Mbit/s);链路上限 12.5 MB/s;取小者   12.5 MB/s(计算)

BDP = 带宽 × RTT,是让管道始终满的在途字节数。窗口 ÷ RTT 给出窗口受限时的吞吐上限。

窗口受限

实测 · 本机执行输出
    SO_RCVBUF 设置值   读回值       吞吐          计算上限     srtt    cwnd        对端窗口 snd_wnd
    64 KiB               64 KiB     2.52 MB/s     0.66 MB/s    142 ms     511 KiB      303 KiB
    256 KiB             256 KiB     2.53 MB/s     2.62 MB/s    142 ms     511 KiB      310 KiB
    512 KiB             512 KiB     4.98 MB/s     5.24 MB/s    104 ms     511 KiB      511 KiB
    1024 KiB           1024 KiB     9.91 MB/s    10.49 MB/s    105 ms    1023 KiB     1023 KiB
    2048 KiB           2048 KiB    12.46 MB/s    12.50 MB/s    168 ms    2047 KiB     2047 KiB
    4096 KiB           4096 KiB    12.46 MB/s    12.50 MB/s    197 ms    2420 KiB     4095 KiB
    不设置(自动调整)           128 KiB    10.44 MB/s     1.31 MB/s    177 ms    2200 KiB     4095 KiB
  链路带宽上限 12.5 MB/s;BDP 1221 KiB。接收缓冲区小于 BDP 时吞吐被窗口卡住;够大以后吞吐封顶在带宽上

接收缓冲区小于 BDP 时,实测吞吐和窗口 ÷ RTT 吻合;大于 BDP 后封顶在带宽上。小于约 256 KiB 的 SO_RCVBUF 在 macOS 上不被照办。

缓冲过大

实测 · 本机执行输出
  无限速:SO_RCVBUF 256 KiB → 吞吐   2.60 MB/s,srtt 102 ms,cwnd 511 KiB
  无限速:SO_RCVBUF 4096 KiB → 吞吐  41.54 MB/s,srtt 101 ms,cwnd 4095 KiB

窗口超过 BDP 的部分不会提高吞吐,只会在瓶颈队列里排队,srtt 从 105 ms 涨到 197 ms。没有带宽瓶颈时不排队。

发送缓冲区

实测 · 本机执行输出
    SO_SNDBUF 设置值   读回值       吞吐
        64 KiB           64 KiB     0.63 MB/s   (srtt 104 ms)
       256 KiB          256 KiB     2.51 MB/s   (srtt 104 ms)
      1024 KiB         1024 KiB     9.97 MB/s   (srtt 105 ms)
      4096 KiB         4096 KiB    12.46 MB/s   (srtt 198 ms)

SO_SNDBUF 同样限制吞吐,小到 64 KiB 也照办(0.63 MB/s)。两端都要有至少 BDP 的缓冲。

分步演示

带宽时延积与缓冲区
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上,netlab 限速 100 Mbit/s、每方向延迟 50 ms(队列 100 个包),发送与接收在同一个进程的两个线程里;每个设置只传一次 6 秒,取 2~5.5 秒的平均,没有重复多次
    • 没有验证:真实广域网上的 BDP;小于 256 KiB 的 SO_RCVBUF 实际生效的值(只能从吞吐反推);自动调整缓冲区的上限由什么决定;队列管理算法(AQM、ECN)对 bufferbloat 的缓解;不同拥塞算法(BBR)下缓冲区的影响
    京ICP备2026067502号-1京公网安备11010502063487号