3. 发送缓冲区调优:SO_SNDBUF 选项与最佳大小计算

发送缓冲区,说白了就是内核帮你暂存待发数据的一块内存。你调用 send() 时,数据先拷进这里,然后内核协议栈慢慢往外发。这块缓冲区设多大,直接影响你的应用能跑多快、会不会卡顿。

我早年调一个金融交易网关,就因为发送缓冲区设太小,导致每秒丢了几百笔订单。嗯,从那以后我再也不敢小看这个参数了。

3.1 SO_SNDBUF 选项:怎么设、什么时候生效

SO_SNDBUF 是套接字级别的选项,用 setsockopt() 来设置。但有个坑——内核实际分配的大小,往往是你要的值的两倍。

int sndbuf = 65536;  // 我想要 64KB
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));

// 读回来看看实际是多少
int actual = 0;
socklen_t len = sizeof(actual);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &actual, &len);
printf("实际发送缓冲区: %d\n", actual);  // 很可能输出 131072
注意:内核会把你的值翻倍,多出来的部分用于内部管理开销(sk_buff 结构体、分片信息等)。你设 64KB,实际可用数据空间可能只有 64KB,但内核总共预留了 128KB。

为什么会这样?因为 Linux 内核在分配时,会把用户请求值乘以 2,再减去一些协议头预留空间。我记得第一次查这个 bug 时,盯着 tcp_sndbuf 的源码看了半天才明白。

3.2 发送缓冲区大小对性能的影响

缓冲区太小和太大,都会出问题。我列个表,你一看就明白。

场景 表现 根因
缓冲区过小 send() 频繁阻塞、吞吐量上不去 应用层写入速度 > 内核发送速度,缓冲区很快填满
缓冲区过大 内存浪费、延迟变高、丢包时重传代价大 大量数据积压在缓冲区,TCP 拥塞控制反应迟钝
缓冲区刚好 吞吐量高、延迟稳定、CPU 占用合理 应用层与内核发送速率匹配,流水线饱满

我调过一个视频推流服务,默认 16KB 的发送缓冲区,推 1080p 视频时 send() 经常返回 EAGAIN。改成 256KB 后,吞吐量直接翻了三倍。你想想看,缓冲区就是水管的口径——口径太小,水龙头开再大也没用。

3.3 如何计算最佳发送缓冲区大小

最佳大小没有银弹,但有个经验公式可以用。我个人习惯按 带宽-延迟积(BDP) 来算。

核心公式:

最佳发送缓冲区 ≈ 带宽 × RTT × 2

单位:带宽用 bps,RTT 用秒,结果用字节。

举个例子:

  • 带宽:100 Mbps(即 12.5 MB/s)
  • RTT:20 ms(0.02 秒)
  • BDP = 12.5 MB/s × 0.02 s = 0.25 MB = 256 KB
  • 最佳缓冲区 ≈ 256 KB × 2 = 512 KB

为什么要乘 2?因为发送缓冲区不仅要装正在发的数据,还要装已经发出但还没收到 ACK 的数据。乘 2 之后,能保证发送窗口不会因为缓冲区不够而被迫缩小。

实战技巧:如果你不确定 RTT,可以用 ping 测一下。如果带宽不确定,可以用 iperf 跑一轮。我一般在压测环境里先设一个较大的值(比如 1MB),然后逐步往下压,找到吞吐量开始下降的那个临界点。

3.4 内核自动调优:tcp_sndbuf 与 tcp_wmem

Linux 内核其实有自动调优机制。你设了 SO_SNDBUF 后,内核会在 tcp_wmem 的范围内动态调整。

# 查看当前 tcp_wmem 值
cat /proc/sys/net/ipv4/tcp_wmem
# 输出示例:4096  16384  4194304
#          min    default  max

三个值的含义:

  • min:每个 TCP 连接的最小发送缓冲区,默认 4KB
  • default:初始值,默认 16KB
  • max:上限,默认 4MB

如果你不设 SO_SNDBUF,内核会从 default 开始,根据拥塞窗口自动往上扩,最多到 max。但如果你手动设了 SO_SNDBUF,内核的自动调优就失效了——它会把你的值当作固定上限。

我曾经踩过的坑:在一个长连接池服务里,我手动设了 64KB 的 SO_SNDBUF,结果高并发下大量连接堆积,内存暴涨。后来发现,内核自动调优本来可以按需扩到 256KB,被我硬生生锁死在 64KB。从那以后,除非明确知道带宽-延迟积,否则我尽量不动 SO_SNDBUF,只调 tcp_wmem 的 max 值。

3.5 知识体系:发送缓冲区调优全景

下面这张图,把发送缓冲区调优的核心逻辑串起来了。你可以把它当作调优时的检查清单。

发送缓冲区调优知识体系 发送缓冲区调优全景图 发送缓冲区 SO_SNDBUF 设置 BDP 公式计算最佳值 内核 tcp_wmem 调优 setsockopt 内核翻倍机制 带宽 × RTT 乘 2 安全系数 min / default / max 自动扩缩机制 最佳发送缓冲区:高吞吐 + 低延迟 + 低内存 调优原则:先测 BDP,再设 SO_SNDBUF,最后用 tcp_wmem 兜底 手动设置会关闭内核自动调优,非必要不动手

3.6 调优步骤总结

如果你现在就要调一个线上服务,按这个顺序来:

  1. 测网络环境:用 ping 拿 RTT,用 iperf 拿带宽,算出 BDP。
  2. 设初始值:BDP × 2 作为 SO_SNDBUF 的候选值。
  3. 压测验证:用 sar -n TCP 或 ss -ti 观察发送缓冲区是否经常满。
  4. 微调:如果 send() 阻塞频繁,加大;如果内存压力大,减小。
  5. 考虑自动调优:如果不想手动管理,调大 tcp_wmem 的 max,不设 SO_SNDBUF。
我的习惯:对于延迟敏感的服务(比如实时音视频),我手动设一个偏小的缓冲区(BDP 的 1.5 倍),避免数据积压。对于吞吐敏感的服务(比如文件传输),我直接不设 SO_SNDBUF,让内核自动扩到 tcp_wmem max。

发送缓冲区调优,说白了就是找到那个「不卡顿、不浪费」的平衡点。你多测几次,慢慢就有感觉了。