bigywater.tech
网络协议 · 30

UDP 接收缓冲区溢出:丢包发生在哪一层

UDP 的包明明发出去了,对方却没收到——丢在哪一层?接收缓冲区有多大,调大有用吗?这篇让接收方故意不读、读得慢,用 netstat 的计数对账,看发送方对此一无所知,以及缓冲区大小到底能挡住多大的突发。

786,896
新建 UDP 套接字的默认接收缓冲区字节数;上限 8,388,608,超过上限会被悄悄截断
80.7%
接收方每包处理 300 µs 时,发送方全速发 20000 个数据报,默认缓冲区下的丢失率;8 MiB 缓冲区是 0%
4099 / 762 / 97
不读的接收方在默认缓冲区里最多攒下的数据报个数(载荷 100 / 1000 / 8000 字节)

缓冲区大小

实测 · 本机执行输出
  net.inet.udp.recvspace: 786896
  kern.ipc.maxsockbuf: 8388608
  新建的 UDP 套接字 SO_RCVBUF 默认 = 786896 字节
  设置      4096:读回 4096
  设置     65536:读回 65536
  设置    262144:读回 262144
  设置   1048576:读回 1048576
  设置   4194304:读回 4194304
  设置   8388608:读回 8388608
  设置  16777216:读回 8388608
  设置  67108864:读回 8388608

默认 786,896 字节,最大 8 MiB;设置过大的值不会报错,只是被截断。

能攒多少

实测 · 本机执行输出
  每个  100 字节:发了 5000 个(sendto 报错 0 次),队里攒下 4099 个(409900 字节载荷);Recv-Q 显示 541052 字节;netstat “dropped due to full socket buffers” 增加了 908;攒下的数量 + 被丢的 = 5007
  每个 1000 字节:发了 5000 个(sendto 报错 0 次),队里攒下 762 个(762000 字节载荷);Recv-Q 显示 786368 字节;netstat “dropped due to full socket buffers” 增加了 4312;攒下的数量 + 被丢的 = 5074
  每个 8000 字节:发了 5000 个(sendto 报错 0 次),队里攒下 97 个(776000 字节载荷);Recv-Q 显示 779088 字节;netstat “dropped due to full socket buffers” 增加了 4932;攒下的数量 + 被丢的 = 5029
  发送方 sendto 一次都没有报错:UDP 发送方完全不知道对方的缓冲区满了。Recv-Q 比载荷字节数大(每个数据报还要记账发送方地址等开销),所以小包能攒的个数远少于 默认缓冲区 ÷ 载荷大小
  按这三种大小,默认缓冲区(786896 字节)平均每个数据报占 192 / 1033 / 8112 字节

每个数据报占的缓冲区比载荷大(要记账发送方地址等),100 字节的包占 192 字节;小包能攒的个数远少于缓冲区 ÷ 载荷。

调大缓冲区

实测 · 本机执行输出
  SO_RCVBUF =    65536(读回 65536):攒下   63 个 / 5000,丢 4937 个(98.7%);netstat 满缓冲区丢弃 +4972
  SO_RCVBUF =   262144(读回 262144):攒下  254 个 / 5000,丢 4746 个(94.9%);netstat 满缓冲区丢弃 +4756
  SO_RCVBUF =   786896(读回 786896):攒下  762 个 / 5000,丢 4238 个(84.8%);netstat 满缓冲区丢弃 +4255
  SO_RCVBUF =  1048576(读回 1048576):攒下 1016 个 / 5000,丢 3984 个(79.7%);netstat 满缓冲区丢弃 +4024
  SO_RCVBUF =  4194304(读回 4194304):攒下 4064 个 / 5000,丢  936 个(18.7%);netstat 满缓冲区丢弃 +946
  SO_RCVBUF =  8388608(读回 8388608):攒下 5000 个 / 5000,丢    0 个(0.0%);netstat 满缓冲区丢弃 +30

缓冲区越大越能吸收突发:突发 5000 个 1000 字节的包,8 MiB 全部攒下,64 KiB 只攒下 63 个。但只是把丢包推迟,不能解决持续的过载。

读得太慢

实测 · 本机执行输出
  发送 全速,默认缓冲区:0.15 秒发完 20000 个,接收方(另一个进程)处理了 3868 个,丢 16132 个(80.7%);netstat 满缓冲区丢弃 +16326
  发送 全速,8 MiB 缓冲区:0.12 秒发完 20000 个,接收方(另一个进程)处理了 20000 个,丢 0 个(0.0%);netstat 满缓冲区丢弃 +583
  发送 每 100 µs 一个,默认缓冲区:2.16 秒发完 20000 个,接收方(另一个进程)处理了 10471 个,丢 9529 个(47.6%);netstat 满缓冲区丢弃 +9794
  发送 每 100 µs 一个,8 MiB 缓冲区:2.18 秒发完 20000 个,接收方(另一个进程)处理了 20000 个,丢 0 个(0.0%);netstat 满缓冲区丢弃 +478
  发送 每 500 µs 一个,默认缓冲区:10.22 秒发完 20000 个,接收方(另一个进程)处理了 20000 个,丢 0 个(0.0%);netstat 满缓冲区丢弃 +784
  发送 每 500 µs 一个,8 MiB 缓冲区:10.22 秒发完 20000 个,接收方(另一个进程)处理了 20000 个,丢 0 个(0.0%);netstat 满缓冲区丢弃 +766
  注意 netstat 的计数是整台机器所有进程共用的:什么都不做的 3 秒里,“dropped due to full socket buffers”也增加了 231(别的进程的 UDP 流量),所以上面的 netstat 增量里包含这部分背景噪声,以接收方自己数到的个数为准
  接收方每个包要 300 µs,最多每秒处理约 3300 个;发送速度低于这个(每 500 µs 一个 = 2000 个/秒)就不丢,高于它多出来的先排进队列,队满了就丢;缓冲区只能吸收短时间的突发,更大的缓冲区只是把丢包推迟

发送速度超过接收速度时,多出的先排队,满了就丢。发送方的 sendto 全程成功,没有任何通知。

怎么看

实测 · 本机执行输出
  发 100 个到没人监听的端口:“dropped due to no socket” +100,“dropped due to full socket buffers” +51(这两个是不同原因的计数;前者对应 w15 里的 ICMP 端口不可达,而且受 ICMP 限速)
  netstat -s -p udp 里这类计数开机以来累计:rx 20106390 ; full 13614785 ; nosock 1179540 ; delivered 4887494

netstat -s -p udp 的“dropped due to full socket buffers”是整机共用的累计计数,别的进程的流量也算在里面,要以接收方自己数到的个数为准。

分步演示

UDP 缓冲区溢出
本步的对象

    本文用到的工具与边界

    • 本机 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 上做;第四节的接收方是另一个进程(避免 Python 线程的 GIL 影响发送速度);netstat -s -p udp 的计数是整台机器所有进程共用的,实验期间有 200 多个/3 秒的背景噪声;时间与个数每次略有不同
    • 没有验证:在真实网络(en0)上因接收缓冲区满而丢包的表现;SO_RCVBUF 的记账方式(每个数据报的额外开销 92 字节来自哪些结构);多核下的差异;UDP 接收缓冲区与 net.inet.udp.maxdgram 的交互;开机以来大量“缓冲区满”丢弃具体来自哪些进程
    京ICP备2026067502号-1京公网安备11010502063487号