TCP 缓冲区与窗口大小调优:发送缓冲、接收缓冲、滑动窗口

说实话,TCP 优化里最容易被忽视的就是缓冲区。很多人调了超时、改了拥塞算法,但数据还是跑不上去。我猜问题出在哪?缓冲区太小。

我自己在负责一个视频推流项目时遇到过:服务器明明带宽 1Gbps,CPU 也没瓶颈,但吞吐死活只有 300Mbps。抓包一看,窗口一直在 64KB 附近徘徊。原来是接收缓冲区默认值太小,把对端的发送窗口给掐住了。

所以这一章,咱们就彻底把发送缓冲、接收缓冲、滑动窗口的事聊透。你搞清楚这仨,TCP 性能至少可以翻倍。

发送缓冲区:数据上路的“蓄水池”

发送缓冲区就是内核里给每个 TCP 连接分配的一块内存。应用程序调用 send() 把数据丢进去,然后内核负责按 TCP 协议拆成段发出去。

  • 作用:暂存待发送数据,让应用不必等网络。
  • 满了会怎样? send() 阻塞或返回 EAGAIN(非阻塞模式)。
  • 太小:应用频繁进入等待,上下文切换暴增。
  • 太大:内存浪费,且丢包时重传的数据量更多。

我习惯看一个指标:发送缓冲区里积压的数据量。如果积压长期接近缓冲区上限,就该考虑扩容了。

经验值:对大多数后台服务,64KB ~ 256KB 是合理的起调范围。视频流服务器可以给到 512KB ~ 1MB,但要配合 BDP(带宽延迟积)来算。

接收缓冲区:对端在等你收

接收缓冲区是对方数据抵达后的第一站。应用没及时读取数据,它们就躺在里面。接收缓冲区的大小直接影响本端通告的接收窗口 wscale。

为什么?因为 TCP 滑动窗口的右边界就是“接收缓冲区剩余空间”。

小心陷阱:接收缓冲区设得太小,对端会频繁进入 PSH 模式,甚至零窗口探测,白白浪费 CPU。我曾经调一个数据库同步延迟问题,把接收缓冲从 16KB 提到 128KB 后,延迟直接降了 40%。

滑动窗口:流量控制的握手

滑动窗口不是物理缓冲区,它是一个“承诺”——“我还能收多少字节”。发送方根据这个承诺决定发多少,不能超过窗口大小。

这玩意有三层含义:

  1. 接收窗口 (rwnd):接收缓冲区剩余,告诉对端别发太快。
  2. 拥塞窗口 (cwnd):发送方自己估算的链路承载能力。
  3. 实际发送窗口 = min(rwnd, cwnd)。

你调发送缓冲/接收缓冲,最终就反映到 rwnd 上。而 rwnd 又通过 TCP 头里的 Window 字段传递。如果接收缓冲太小,那就算网络再好,也跑不起来。

一张图看透数据流

下面这张 SVG 图把发送缓冲、接收缓冲和滑动窗口的关系画清楚了。你看一眼就能记住。

TCP 缓冲区与滑动窗口数据流 TCP 发送缓冲 → 滑动窗口 → 接收缓冲 应用层 写数据 发送缓冲区 TCP 分段 滑动窗口 = min(rwnd, cwnd) 网络 接收缓冲区 应用层 读数据 接收窗口 (rwnd) 反馈 接收缓冲剩余空间 = rwnd 对端按 rwnd 限制发送量 滑动窗口动态控制流量

从图里你能看到:数据从左到右经过发送缓冲区、TCP 分段(受滑动窗口约束)、网络、接收缓冲区,最终到应用。接收缓冲区剩余空间通过 ACK 包里的 Window 字段反馈回来,形成闭环。

实战参数调优:改多大才合适?

直接上干货。我总结了几个常见场景的缓冲区建议值,你直接拿去用。

场景发送缓冲接收缓冲说明
通用 Web 服务64KB64KB够用,兼顾内存
文件传输(局域网)256KB256KB配合大窗口减少 ACK 数量
视频直播(公网)512KB ~ 1MB512KB ~ 1MB按 BDP 算,延迟可忍受
数据库同步(低延迟)128KB128KB平衡内存与延迟
小包高频(如游戏)16KB ~ 32KB16KB ~ 32KB避免数据积压,降低延迟

动手改设置:代码来了

Linux 下有两种改法:系统级和 socket 级。系统级用 sysctl,socket 级用 setsockopt。

系统级调优(影响所有连接)

# 查看当前值
$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096  87380  6291456

$ sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096  16384  4194304

三个值分别是:最小值、默认值、最大值。单位是字节。我建议直接调整默认值和最大值:

# 设置接收缓冲(最小值 4KB,默认 128KB,最大 16MB)
$ sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"

# 设置发送缓冲(最小值 4KB,默认 64KB,最大 16MB)
$ sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"

Socket 级调优(针对特定连接)

int rcvbuf = 262144;  // 256KB
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

int sndbuf = 262144;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
注意:内核会把 SO_RCVBUF 设置值翻倍(因为内部也需要开销)。你设 256KB,实际会分配 512KB 的内核内存。别设太猛,否则内存撑爆。

避坑指南

  • 别信默认值:Linux 默认接收缓冲只有 87KB(新版可能 128KB),对千兆网卡根本不够。
  • 调了后看实际效果:用 ss -ti 看 wscale 和 rto,窗口要是没变大说明你设的没生效(可能没重启服务)。
  • 动态调整 vs 固定值:Linux 支持自动调整窗口(tcp_moderate_rcvbuf),但我个人偏向根据业务固定,避免波动。
  • 调发送缓冲要考虑重传:缓冲区太大,丢包后重传队列也大,可能加剧拥塞。通常设一次 RTT 内能发完的量就够了。

嗯,到这里你应该能明白:TCP 调优不是靠蒙。你理解了发送缓冲、接收缓冲和滑动窗口的关系,再配合合理的参数,性能提升是立竿见影的。我当年那个视频项目,调完缓冲区后从 300Mbps 直接干到 850Mbps,CPU 占用反而降了。这就是调对地方的效果。