Linux 基础体系 · 第 69/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 网络内核调优:Socket Buffer、Backlog、Port、Conntrack 和验证

Linux 网络性能问题经常被归结为“把几个 buffer 调大”。但 Socket Buffer、监听 Backlog、临时 Port 和 Conntrack 位于不同层次,分别限制不同阶段的资源:

  • Socket Buffer:一个已建立 Socket 能缓存多少尚未被应用读取或尚未发送的数据。
  • Backlog:监听 Socket 能暂存多少连接请求,直到应用调用 accept()
  • Port:连接四元组中的本地或远端端口,决定连接能否建立以及临时端口是否耗尽。
  • Conntrack:Netfilter 用于跟踪流状态的连接表,决定防火墙、NAT 和状态匹配能否继续工作。
  • 验证:不能只查看 sysctl 配置,必须观察实际 Socket、队列、丢包、状态表和应用行为。

这些参数都不是“越大越好”。增大容量通常意味着更高的内存占用、更长的排队延迟,或者让故障从一个位置转移到另一个位置。


一、先建立数据流模型

以一个 TCP 服务端接收客户端连接为例,数据大致经过以下路径:

flowchart LR
    A[网卡收到数据包] --> B[驱动与 NAPI]
    B --> C[协议栈接收队列]
    C --> D[Conntrack / Netfilter]
    D --> E[TCP 状态机]
    E --> F[SYN 队列或 Accept 队列]
    F --> G[已建立 Socket]
    G --> H[Socket 接收缓冲区]
    H --> I[应用 recv/read]

不同资源对应不同位置:

  1. net.core.netdev_max_backlog 主要影响网络处理来不及及时处理时,内核设备输入队列能暂存多少包。
  2. Conntrack 表保存流的跟踪项;如果表满,即使 TCP 的 Socket 资源充足,也可能因为 Netfilter 无法创建状态项而丢包。
  3. TCP SYN 队列暂存收到 SYN、等待握手完成的请求。
  4. Accept 队列暂存已经完成 TCP 握手、等待应用 accept() 的连接。
  5. Socket 接收缓冲区保存已经交付到该 Socket、但应用尚未读取的数据。

因此,“客户端连接失败”可能来自:

  • SYN 队列不足;
  • Accept 队列已满;
  • 应用没有及时 accept()
  • 临时端口耗尽;
  • Conntrack 表已满;
  • 防火墙规则丢弃;
  • Socket 接收缓冲区或内存压力;
  • 网卡、驱动、CPU 或协议栈其他队列溢出。

调优前必须先判断数据包在哪一个阶段消失。


二、Socket Buffer:应用读写速度与网络速度之间的缓冲

2.1 发送和接收缓冲区分别解决什么问题

Socket Buffer 是内核为 Socket 保存数据的内存区域,至少需要区分两个方向:

  • 接收缓冲区:网络收到数据后,等待应用调用 read()recv()
  • 发送缓冲区:应用写入数据后,等待网卡发送、等待对端确认,或等待 TCP 重传完成。

对于 TCP,发送缓冲区不仅保存“尚未发出”的数据,还可能保存:

  • 已经发送但尚未收到 ACK 的数据;
  • 发生丢包后等待重传的数据;
  • 受拥塞窗口、接收窗口或网卡发送能力限制而暂时不能发送的数据。

因此,发送缓冲区不是简单的“网卡发送队列”。

对于 UDP,内核没有 TCP 那样的可靠重传机制。接收端应用读取不及时时,UDP 数据可能在 Socket 接收缓冲区溢出后直接丢弃;发送端发送过快时,也可能因为发送队列、设备队列或路由路径资源不足而失败或丢失。


2.2 常用的 Socket Buffer 参数

查看基础值:

sysctl \
  net.core.rmem_default \
  net.core.rmem_max \
  net.core.wmem_default \
  net.core.wmem_max \
  net.ipv4.tcp_rmem \
  net.ipv4.tcp_wmem

典型输出形式如下,具体数值依发行版、内核版本和系统配置而不同:

net.core.rmem_default = 212992
net.core.rmem_max = 212992
net.core.wmem_default = 212992
net.core.wmem_max = 212992
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304

这些参数含义不同:

net.core.rmem_default

普通 Socket 的默认接收缓冲区上限或初始相关值。它不是所有 TCP 连接最终都会固定使用的大小。

net.core.rmem_max

通过 SO_RCVBUF 设置接收缓冲区时的通用上限之一。应用可显式设置的值不能无限增大;具备 CAP_NET_ADMIN 的程序可以使用 SO_RCVBUFFORCE 绕过普通限制,但这不应作为常规调优手段。

net.core.wmem_defaultnet.core.wmem_max

分别对应发送方向的默认值和通用上限。

net.ipv4.tcp_rmem

三个数字分别表示:

最小值 默认值 最大值

它用于 TCP 接收缓冲区的自动调节。启用 TCP 接收缓冲区自动调节时,内核会根据吞吐、RTT、窗口和内存压力,在这个范围内调整连接的接收缓冲区。

net.ipv4.tcp_wmem

三个数字同样表示 TCP 发送缓冲区自动调节所使用的最小值、默认值和最大值。

需要注意,tcp_rmemtcp_wmemnet.core.rmem_maxnet.core.wmem_max 不是简单的两组别名。前者主要描述 TCP 自动调节范围,后者是 Socket 层面的通用约束。实际可用值还受到协议开销、内存压力、应用设置方式以及内核版本实现影响。


2.3 SO_RCVBUF 的“翻倍”现象

应用执行:

int size = 4 * 1024 * 1024;
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

然后再读取:

int actual;
socklen_t len = sizeof(actual);
getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &actual, &len);

在 Linux 中,返回值经常大于应用传入值,常见现象是接近两倍。这是因为内核为 Socket 管理缓冲区时还要考虑内部控制结构和协议开销,Linux 的 Socket Buffer 设置与报告存在内部折算。

因此,不能用下面这种方式断言“我设置了 4 MiB,所以内核只分配了 4 MiB”:

# 错误理解:把 getsockopt 返回值直接当成纯 payload 容量

正确做法是:

  1. 查看应用实际调用的 setsockopt()
  2. 查看应用随后通过 getsockopt() 读回的值;
  3. ss -m 观察连接的内存统计;
  4. 通过吞吐、RTT、重传和丢包验证是否真的解决了问题。

2.4 TCP 窗口、BDP 与 Buffer 的关系

TCP 接收窗口反映接收端还能接受多少数据。为了让长距离高速链路不因等待 ACK 而停顿,缓冲区至少要能覆盖带宽时延积:

BDP=Bandwidth×RTTBDP = Bandwidth \times RTT

其中:

  • Bandwidth 是链路有效带宽,单位为 bit/s;
  • RTT 是往返时延,单位为 s;
  • BDP 的结果是 bit,需要除以 8 转成 byte。

完整算例

假设:

  • 链路带宽为 1 Gbit/s
  • RTT 为 50 ms = 0.05 s
  • 忽略协议头和拥塞控制的额外影响。

则:

BDP=1,000,000,000×0.05=50,000,000 bitBDP = 1,000,000,000 \times 0.05 = 50,000,000 \text{ bit}

换算为字节:

50,000,000/8=6,250,000 byte50,000,000 / 8 = 6,250,000 \text{ byte}

也就是约 5.96 MiB

如果 TCP 发送端或接收端的有效窗口只有 1 MiB,即使物理链路能达到 1 Gbit/s,单条连接也可能被窗口限制。粗略上限为:

ThroughputReceiveWindowRTTThroughput \leq \frac{ReceiveWindow}{RTT}

例如:

1,048,576/0.0520,971,520 byte/s1,048,576 / 0.05 \approx 20,971,520 \text{ byte/s}

约为 167.8 Mbit/s,明显低于 1 Gbit/s。

但 BDP 只是必要条件,不是性能保证。实际吞吐还受到以下因素限制:

  • TCP 拥塞窗口 cwnd
  • 对端接收窗口 rwnd
  • 丢包和重传;
  • 拥塞控制算法;
  • CPU 和加密开销;
  • 网卡队列和 qdisc;
  • 应用是否持续读写;
  • 内核内存压力。

当 RTT 为 1 ms、带宽为 100 Mbit/s 时:

BDP=100,000,000×0.001/8=12,500 byteBDP = 100,000,000 \times 0.001 / 8 = 12,500 \text{ byte}

这时把每条连接的缓冲区扩大到几十 MiB,通常不会带来收益,反而会放大并发连接的内存占用。


2.5 接收缓冲区过小和过大的两种失败模式

过小

表现可能包括:

  • TCP 接收窗口较小;
  • 高 RTT 链路吞吐上不去;
  • 应用读取稍慢时,发送端受到窗口限制;
  • UDP 接收缓冲区溢出,出现丢包;
  • ss -u -a/proc/net/snmp 或应用统计中出现接收错误。

TCP 中,接收缓冲区不足不一定立即表现为丢包。TCP 会通过缩小通告窗口施加反压,最终表现为吞吐下降或发送端阻塞。

过大

表现可能包括:

  • 高并发下内存占用显著增加;
  • 应用读取速度慢时,数据在内核中排队更久;
  • 队列变深,形成 bufferbloat,延迟上升;
  • 连接数暴增时触发 TCP 内存压力或全局内存回收;
  • 误以为“缓冲区大”可以弥补应用处理能力不足。

例如,假设服务有 50,000 个并发 TCP 连接,每个连接的接收缓冲区目标值为 8 MiB。仅从数量级看:

50,000×8 MiB400 GiB50,000 \times 8 \text{ MiB} \approx 400 \text{ GiB}

内核不会简单地立即为每个连接实际分配完整最大值,TCP 也会按需自动调节,但这个算例说明:把单连接上限调大,可能给高并发系统带来严重的内存风险。


2.6 观察实际 Socket,而不是只看 sysctl

查看 TCP 连接和监听队列:

ss -lnt
ss -ant

常见输出:

State  Recv-Q Send-Q Local Address:Port  Peer Address:Port
LISTEN 128    0      0.0.0.0:8080       0.0.0.0:*
ESTAB  0      0      192.0.2.10:8080   192.0.2.20:53124

LISTEN Socket:

  • Recv-Q 通常表示已经完成握手、等待应用 accept() 的连接数量;
  • Send-Q 通常与监听队列上限相关,但具体显示和内核版本、Socket 类型有关。

ESTAB Socket:

  • Recv-Q 表示已经收到、但应用尚未读取的数据;
  • Send-Q 表示已经写入内核、但尚未完成发送或确认的数据。

查看 Socket 内存细节:

ss -m -t -n

输出中可能包含类似:

skmem:(r0,rb131072,t0,tb262656,w0,o0,bl0,d0)

这些字段是内核 Socket 内存统计,不应机械地把每个字段都解释成“应用可读 payload”。例如:

  • r:当前接收队列相关用量;
  • rb:接收缓冲区上限;
  • t:发送相关当前用量;
  • tb:发送缓冲区上限;
  • 其他字段与转发、分片、回收和协议内部开销有关。

不同内核版本的字段和显示细节可能存在差异,应结合本机 ss 版本和 ss(8) 文档解释。

全局 Socket 统计可以查看:

cat /proc/net/sockstat
cat /proc/net/sockstat6

其中 TCP_meminuseorphan 等字段有助于判断是否出现大量 Socket 或 TCP 内存压力,但它们不能替代每连接分析。


三、Backlog:SYN 队列与 Accept 队列不是一回事

3.1 TCP 建连过程中的两个队列

TCP 服务端执行 listen() 后,连接请求至少要经过两个逻辑阶段:

sequenceDiagram
    participant C as 客户端
    participant K as Linux TCP 栈
    participant A as 应用

    C->>K: SYN
    K-->>C: SYN+ACK
    Note over K: 半连接进入 SYN 队列
    C->>K: ACK
    Note over K: 连接完成握手,进入 Accept 队列
    A->>K: accept()
    K-->>A: 返回已建立 Socket

SYN 队列

SYN 队列保存已经收到 SYN、但三次握手尚未完成的请求,常被称为半连接队列。

相关参数:

sysctl net.ipv4.tcp_max_syn_backlog

它主要影响每个监听 Socket 的未完成连接请求容量。实际行为还受到 SYN Cookie、内存压力和内核实现影响。

Accept 队列

Accept 队列保存已经完成握手、但应用尚未调用 accept() 取走的连接。

相关参数:

sysctl net.core.somaxconn

应用通过:

listen(fd, backlog);

传入的 backlog 并不是无条件生效。现代 Linux 中,实际监听队列上限通常受到 somaxconn 限制,近似可以理解为:

effective_backlog=min(application_backlog, somaxconn)effective\_backlog = \min(application\_backlog,\ somaxconn)

这是一个便于理解的模型,具体边界值和 Socket 类型应以对应内核实现为准。

因此:

listen(fd, 65535);

并不意味着一定能排队 65535 个连接。如果 net.core.somaxconn 较小,内核仍会限制实际值。


3.2 为什么应用不调用 accept() 会导致连接问题

假设:

  • 服务端应用每秒只能调用 accept() 并处理 1000 个新连接;
  • 客户端每秒发起 5000 个连接;
  • Accept 队列容量为 1024

在短时间内,已完成握手的连接进入 Accept 队列的速度高于应用取出的速度:

queue_growth=arrival_rateaccept_ratequeue\_growth = arrival\_rate - accept\_rate

queue_growth=50001000=4000 connections/squeue\_growth = 5000 - 1000 = 4000 \text{ connections/s}

队列很快达到上限。此时继续到达的连接可能出现:

  • 连接建立延迟;
  • 客户端 connect() 超时或重试;
  • 服务端统计中出现监听溢出;
  • 某些配置下内核直接丢弃 ACK 或复位连接。

查看 TCP 统计:

nstat -az | egrep 'Listen|Syncookies|TCPReqQFull|TCPBacklog'

不同内核版本的计数器名称不完全一致,可以先完整查看:

nstat -az | less

也可以查看:

netstat -s | egrep -i 'listen|SYN|overflow|drop'

netstat 在很多现代发行版中属于兼容工具,优先使用 ssnstat/proc 统计。


3.3 tcp_abort_on_overflow 的风险

查看:

sysctl net.ipv4.tcp_abort_on_overflow

当 Accept 队列溢出时,该参数会影响内核是丢弃相关请求还是更积极地发送 RST。启用它可能让客户端更快得到“连接失败”,但这并没有提升服务能力,只是改变失败表现。

它适合在明确理解应用和客户端重试语义后进行实验,不应把它当作解决 Backlog 溢出的办法。根因通常是:

  • 应用线程或事件循环没有及时 accept()
  • TLS 握手、进程调度或初始化逻辑阻塞了接收新连接;
  • worker 数量不足;
  • CPU、锁或日志系统导致入口线程变慢;
  • 连接建立速率本身超过服务设计能力。

3.4 SYN Cookie 不是“无限 SYN 队列”

查看:

sysctl net.ipv4.tcp_syncookies

SYN Cookie 可以在 SYN 队列受压或遭受 SYN Flood 时,不为每个半连接立即保存完整状态,而是把部分状态编码进返回的 SYN-ACK 序列号中。

它的作用是缓解半连接资源消耗,不等于:

  • Accept 队列变大;
  • 应用 accept() 速度变快;
  • 所有 TCP 选项都能无条件保留;
  • Conntrack 表不再消耗资源;
  • 正常连接可以无限增加。

一个常见反例是:SYN Cookie 已经工作,但应用线程仍然每秒只能接受少量连接。此时半连接攻击压力可能缓解,完成握手后的 Accept 队列仍会溢出。


3.5 监听 Backlog 的验证

先确认应用传入的 backlog。对于无法修改源码的程序,可以使用:

strace -f -e trace=network -p <PID>

观察是否出现类似:

listen(3, 4096) = 0

需要 root 权限或适当的 ptrace 权限,生产环境使用时要评估开销和安全策略。

再查看运行时监听状态:

ss -lntp 'sport = :8080'

对监听 Socket 做压测时,要区分两类测试:

  1. 连接建立速率测试:验证 SYN 和 Accept 队列;
  2. 持续数据吞吐测试:验证 Socket Buffer、拥塞控制和应用读写。

只进行 curl 或少量手工连接,通常无法触发 Backlog 问题。


四、Port:端口资源、四元组和 TIME_WAIT

4.1 一个 TCP 连接由四元组标识

TCP 连接通常由以下四元组唯一标识:

(local IP, local port, remote IP, remote port)(local\ IP,\ local\ port,\ remote\ IP,\ remote\ port)

例如:

192.0.2.10:443 -> 198.51.100.20:53124

其中:

  • 192.0.2.10 是本地 IP;
  • 443 是本地服务端口;
  • 198.51.100.20 是远端 IP;
  • 53124 是远端端口。

这解释了一个重要事实:

服务端可以在同一个本地端口上同时承载许多连接,只要完整四元组不同。

因此,443 并不是“只能建立一个连接”的资源。服务端监听端口通常是固定的,真正容易耗尽的往往是客户端或代理的临时端口。


4.2 临时端口范围

查看:

sysctl net.ipv4.ip_local_port_range

典型输出:

net.ipv4.ip_local_port_range = 32768 60999

这表示内核为主动连接、绑定端口 0 等场景选择临时端口时,通常会在该范围内分配端口。范围包含多少端口可按:

count=highlow+1count = high - low + 1

计算。上例有:

6099932768+1=2823260999 - 32768 + 1 = 28232

但这不是“最多只能建立 28232 个出站连接”。完整四元组允许同一个本地端口连接到不同的远端 IP 和端口;实际限制还取决于:

  • 远端地址和端口是否相同;
  • 是否存在旧连接或 TIME_WAIT;
  • bind() 方式;
  • SO_REUSEADDRSO_REUSEPORT
  • NAT 设备的映射能力;
  • Conntrack 状态;
  • 应用是否固定绑定本地端口。

4.3 ip_local_reserved_ports

查看:

sysctl net.ipv4.ip_local_reserved_ports

该参数用于告诉内核:自动选择临时端口时避开哪些端口。例如:

sudo sysctl -w net.ipv4.ip_local_reserved_ports=30000-30010,40000

这并不是防火墙规则,也不一定禁止应用显式 bind() 到这些端口。它的主要语义是影响内核自动分配。

修改前应检查系统上的固定端口、监控代理、容器映射和服务发现配置,避免让临时端口范围与固定服务规划互相冲突。


4.4 TIME_WAIT 与端口耗尽

主动关闭 TCP 连接的一方通常会进入 TIME_WAIT,以便:

  1. 确保旧连接中延迟到达的报文不会污染新连接;
  2. 在必要时重传最后的 ACK。

大量短连接客户端可能出现:

ss -tan state time-wait | wc -l

或者按本地端口统计:

ss -tan state time-wait \
  | awk 'NR > 1 {print $4}' \
  | sed 's/.*://' \
  | sort | uniq -c | sort -nr | head

TIME_WAIT 本身不是错误。它是 TCP 正常状态机的一部分。真正的问题是:

  • 短连接创建速度高;
  • 本地端口范围小;
  • 远端目标固定;
  • TIME_WAIT 生命周期内不能安全复用足够多的四元组;
  • 应用或代理没有使用连接复用。

Linux 提供:

sysctl net.ipv4.tcp_tw_reuse

现代内核中该参数主要与主动出站连接复用 TIME_WAIT 有关,通常需要满足时间戳等协议安全条件。它不能解决服务端监听端口不足,也不应与已经移除的 tcp_tw_recycle 混淆。tcp_tw_recycle 曾导致 NAT 客户端时间戳差异问题,已从现代 Linux 中移除;在新系统上编写配置时不应依赖它。

优先级更高的解决方式通常是:

  • 使用 HTTP keep-alive、HTTP/2 或连接池;
  • 降低不必要的短连接创建速率;
  • 让更合适的一方主动关闭连接;
  • 扩大合理的临时端口范围;
  • 只有在确认内核版本、协议条件和业务模型后,再评估 tcp_tw_reuse

4.5 常见端口故障与诊断

bind: Address already in use

可能原因:

  • 已有进程监听该端口;
  • 旧连接处于 TIME_WAIT;
  • SO_REUSEADDR 使用方式不符合预期;
  • 多个进程竞争同一个监听地址;
  • IPv4 和 IPv6 通配地址发生冲突;
  • 容器网络命名空间或端口映射造成冲突。

诊断:

ss -lntp 'sport = :8080'
lsof -nP -iTCP:8080 -sTCP:LISTEN

Cannot assign requested address

主动连接中常见原因是本地临时端口或本地地址分配失败。应查看:

ss -s
ss -tan state time-wait | wc -l
sysctl net.ipv4.ip_local_port_range

如果程序显式绑定了固定本地端口,还要检查是否把本可复用的端口人为限制成了很小集合。


五、Conntrack:Netfilter 的连接状态表

5.1 Conntrack 不等于 TCP 状态机

TCP 内核状态机维护的是 TCP 连接本身的状态,例如:

SYN-SENT -> SYN-RECEIVED -> ESTABLISHED
ESTABLISHED -> FIN-WAIT-1 -> FIN-WAIT-2 -> TIME-WAIT

Conntrack 则是 Netfilter 子系统对网络流进行跟踪的机制,用于支持:

  • 有状态防火墙规则;
  • NAT;
  • ESTABLISHED,RELATED 匹配;
  • 连接相关的报文分类。

Conntrack 会观察 TCP、UDP、ICMP 等流,但它不提供 TCP 的可靠传输,也不替代 TCP Socket 状态机。

一个 TCP 连接可能已经是 ESTABLISHED,但其 Conntrack 项仍可能因超时、表满或网络命名空间差异而出现问题。反过来,Conntrack 显示某个流存在,也不表示应用一定接受了对应连接。


5.2 Conntrack 的基本数据结构

Conntrack 通常依据流的地址、端口、协议等信息建立跟踪项。对于 TCP,可以近似理解为:

原方向:src_ip:src_port -> dst_ip:dst_port
反方向:dst_ip:dst_port -> src_ip:src_port
协议:TCP
状态:SYN_SENT、ESTABLISHED、TIME_WAIT 等相关跟踪状态
超时:该项多久没有报文后被回收

对于 UDP,协议本身没有连接状态,因此 Conntrack 通过报文活动和超时推断一个“伪连接”生命周期。

查看 Conntrack 总体状态:

sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

查看当前跟踪项:

sudo conntrack -L

查看统计:

sudo conntrack -S

conntrack 命令通常来自 conntrack-tools 包,未安装时需要按发行版安装。容器、网络命名空间和权限会影响可见内容;在宿主机执行不一定等同于在容器网络命名空间中执行。


5.3 Conntrack 表满会发生什么

当:

nf_conntrack_countnf_conntrack_maxnf\_conntrack\_count \approx nf\_conntrack\_max

新流需要创建跟踪项时可能失败。典型日志包括:

nf_conntrack: table full, dropping packet

此时可能出现:

  • 新 TCP 连接失败;
  • NAT 新映射建立失败;
  • DNS、UDP、短连接业务间歇性超时;
  • 已建立连接通常仍能继续,直到其状态项超时或业务需要新的相关流;
  • 防火墙的 ESTABLISHED,RELATED 规则行为异常。

注意,“表满”不一定表示 CPU 或带宽已满。它可能是大量短连接、UDP 超时过长、扫描流量或 NAT 映射长期占用造成的。


5.4 估算 Conntrack 容量

一个基础估算是:

required entriesnew_flows_per_second×average_timeoutrequired\ entries \approx new\_flows\_per\_second \times average\_timeout

假设:

  • 每秒创建 20,000 个 UDP 流;
  • 每个流平均保留 30 s
  • 暂不考虑突发、协议分类和安全余量。

则:

20,000×30=600,00020,000 \times 30 = 600,000

至少需要约 600,000 个跟踪项,实际还要为流量突发、TCP 状态、相关连接和管理余量留出空间。

这只是容量估算,不是性能保证。若把 UDP 超时从 30 s 调到 300 s,同样的创建速率理论上会把常驻项需求扩大十倍:

20,000×300=6,000,00020,000 \times 300 = 6,000,000

所以,简单地把超时调大可能直接加速表耗尽。

查看常用超时:

sysctl -a 2>/dev/null | grep '^net.netfilter.nf_conntrack_.*timeout'

常见参数包括:

net.netfilter.nf_conntrack_tcp_timeout_established
net.netfilter.nf_conntrack_tcp_timeout_time_wait
net.netfilter.nf_conntrack_udp_timeout
net.netfilter.nf_conntrack_udp_timeout_stream

这些名称和可用参数受内核配置及模块版本影响,应以本机输出为准。


5.5 nf_conntrack_max 与哈希桶

查看:

sysctl net.netfilter.nf_conntrack_max

Conntrack 通常使用哈希表快速定位跟踪项。某些内核中可以通过模块参数查看或设置哈希桶数量:

cat /sys/module/nf_conntrack/parameters/hashsize

也可以查看模块信息:

modinfo nf_conntrack

nf_conntrack_max、哈希桶数量、内存消耗和内核版本之间存在实现差异。不能直接套用某篇旧文章中的“每个连接固定占用多少字节”,因为实际占用会受到:

  • 内核版本;
  • 32 位或 64 位;
  • 是否启用 NAT;
  • 扩展匹配和 helper;
  • slab 分配;
  • 网络命名空间数量;
  • TCP、UDP、ICMP 协议类型。

扩大 nf_conntrack_max 前应检查内存预算:

slabtop
grep -i conntrack /proc/slabinfo

如果表项来自大量扫描流量,单纯扩大表只能延后故障,可能让攻击者消耗更多内存。


5.6 NOTRACK 的边界

对明确不需要状态跟踪的流量,可以在 raw 表或等价的 nftables 规则中使用 NOTRACK,减少 Conntrack 压力。但这样会失去:

  • NAT 所需的跟踪能力;
  • 基于连接状态的防火墙匹配;
  • 相关连接识别;
  • 某些审计和策略能力。

因此,NOTRACK 不是“加速所有流量”的开关。只有在流量角色、路径和安全策略都明确时才应使用。


六、与四个主题紧密相关的其他队列

只调标题中的参数,可能漏掉真正的瓶颈。

6.1 net.core.netdev_max_backlog

查看:

sysctl net.core.netdev_max_backlog

它影响网络设备输入方向、协议栈处理不及时情况下的排队容量。它与 Socket 接收缓冲区不同:

  • netdev_max_backlog 过小:包可能还没到 Socket 就被丢弃;
  • Socket 接收缓冲区过小:包已进入连接处理路径,但应用读取不及时;
  • Backlog 调大:只能增加暂存能力,不能增加 CPU 处理能力。

查看接口统计:

ip -s link

典型关注:

  • RX errors
  • RX dropped
  • overruns

使用 ip 查看接口、地址和链路状态属于 iproute2 的标准用法;具体字段由驱动和内核支持决定。

6.2 网卡、qdisc 和 CPU

发送方向还可能受 qdisc 队列、网卡发送队列和多队列中断影响:

ip -s link show dev eth0
tc -s qdisc show dev eth0
ethtool -S eth0

这些工具的输出高度依赖驱动。看到 Socket Send-Q 很大,不一定说明 Socket Buffer 参数小;它也可能说明:

  • 对端接收窗口小;
  • TCP 拥塞窗口小;
  • 路径丢包;
  • qdisc 或网卡发送队列拥塞;
  • 应用一次性写入速度远高于网络发送速度。

七、sysctl 修改:临时验证、持久化与回滚

7.1 先保存当前值

修改前保存相关参数:

sysctl \
  net.core.rmem_max \
  net.core.wmem_max \
  net.core.somaxconn \
  net.ipv4.tcp_max_syn_backlog \
  net.ipv4.ip_local_port_range \
  net.ipv4.ip_local_reserved_ports \
  net.netfilter.nf_conntrack_max \
  net.netfilter.nf_conntrack_count \
  > /tmp/network-sysctl.before

查看所有当前配置:

sysctl -a > /tmp/sysctl.all.before

sysctl -a 的输出可能包含大量参数和权限受限项,不应把整个文件原样作为永久配置导入。


7.2 临时修改

例如:

sudo sysctl -w net.core.somaxconn=4096
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192

确认:

sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog

临时修改通常在重启后失效,适合:

  • 压测前验证假设;
  • 观察某个参数是否改变瓶颈;
  • 避免未经验证的配置进入生产持久状态。

但“立即生效”不等于“已有 Socket 全部按新值重建”。有些参数影响新建 Socket,有些影响后续自动调节,有些只在特定路径读取。修改后应重新建立测试连接,并进行对照测试。


7.3 持久化

常见做法是在 /etc/sysctl.d/ 下建立独立文件:

sudo tee /etc/sysctl.d/90-network-tuning.conf >/dev/null <<'EOF'
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
EOF

加载:

sudo sysctl --system

需要注意:

  • /etc/sysctl.d/ 文件的加载顺序和发行版实现有关;
  • 可能存在 /usr/lib/sysctl.d//run/sysctl.d/ 等供应商或运行时配置;
  • 同一参数重复出现时,后加载的配置可能覆盖先前值;
  • 容器通常没有权限修改宿主机网络命名空间中的 sysctl;
  • 某些参数必须在模块加载后才存在。

检查最终值:

sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog

7.4 回滚

最可靠的回滚是恢复修改前记录的单项值,而不是删除系统中所有 sysctl 配置:

sudo sysctl -w net.core.somaxconn=<旧值>
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=<旧值>

如果已经写入配置文件:

sudo rm /etc/sysctl.d/90-network-tuning.conf
sudo sysctl --system

然后再次核验。生产环境中还要考虑:

  • systemd、配置管理工具或启动脚本会不会重新写入参数;
  • 容器编排系统是否有独立 sysctl 配置;
  • 回滚是否需要重启服务,以便新建 Socket 读取旧配置;
  • 增大的 Conntrack 表是否已经消耗内存;
  • 降低参数不会自动清理已经存在的连接和状态项。

八、验证:从配置值走到故障证据

8.1 建立基线

在修改前记录至少以下信息:

uname -a
sysctl -a 2>/dev/null | egrep \
'net.core.(rmem|wmem|somaxconn|netdev_max_backlog)|net.ipv4.(tcp_rmem|tcp_wmem|tcp_max_syn_backlog|ip_local_port_range|tcp_tw_reuse)|net.netfilter.nf_conntrack'
ss -s
cat /proc/net/sockstat
nstat -az
ip -s link
tc -s qdisc show

同时记录:

  • CPU 使用率和软中断;
  • 应用请求延迟;
  • 新建连接速率;
  • 活跃连接数;
  • 重传率;
  • UDP 丢包率;
  • Conntrack 使用率;
  • 进程文件描述符和线程数。

没有基线,就无法判断“参数改变”是否真的带来收益。


8.2 验证 Socket Buffer

对某个服务端口查看:

ss -tnm 'sport = :8080'

重点观察:

  • Recv-Q 是否长期增长;
  • Send-Q 是否长期增长;
  • rbtb 是否达到上限;
  • 是否只在高 RTT 测试中出现问题;
  • TCP 重传是否同时增加。

统计 TCP 重传:

nstat -az | egrep 'RetransSegs|OutRsts|InErrs|Listen'

如果 Recv-Q 长期很大而 CPU 不高,优先检查应用读取速度,而不是立刻增大接收缓冲区。如果 Send-Q 长期很大,还要查看对端窗口、拥塞窗口和网络丢包。

抓包辅助判断:

sudo tcpdump -i eth0 -nn -s 128 'tcp port 8080'

可以观察:

  • 是否反复重传同一序列号;
  • 接收窗口是否变成零;
  • 三次握手是否完成;
  • 服务端是否发送 RST;
  • ACK 是否延迟或丢失。

8.3 验证 Backlog

查看监听队列:

watch -n 1 "ss -lnt 'sport = :8080'"

观察监听 Socket 的 Recv-Q 是否接近上限。然后并行查看:

nstat -az
dmesg -T | egrep -i 'listen|SYN|overflow|conntrack|drop'

如果使用 systemd socket activation、反向代理或多进程模型,还必须确定到底是哪一个进程持有监听 Socket。前端代理的 Backlog 与后端应用的 Backlog 是两个不同位置,不能只调后端。

压测时应明确压测类型。例如使用 wrkh2loadiperf3 或自定义程序时,它们产生的连接复用方式不同:

  • wrk 默认会复用连接,未必能有效测试新连接 Backlog;
  • 反复短连接更容易暴露端口、TIME_WAIT 和 Conntrack 压力;
  • iperf3 更适合测试持续 TCP 吞吐,不代表 HTTP 应用的 accept() 能力。

8.4 验证端口

查看连接状态分布:

ss -tan | awk 'NR > 1 {print $1}' | sort | uniq -c

按本地端口范围观察:

ss -tan | awk 'NR > 1 {print $4}' | sort | uniq -c | sort -nr | head

检查是否存在大量 TIME_WAIT:

ss -tan state time-wait | wc -l

如果怀疑临时端口耗尽,不能只看端口范围,还要确认:

  1. 远端目标是否固定为同一个 IP 和端口;
  2. 程序是否显式绑定本地端口;
  3. 是否存在 NAT;
  4. 是否大量短连接;
  5. 错误是否确实为 EADDRNOTAVAIL 或相关端口分配失败。

应用层日志中的“连接超时”不足以证明端口耗尽,必须结合系统调用、Socket 状态和抓包验证。


8.5 验证 Conntrack

watch -n 1 '
printf "count=";
sysctl -n net.netfilter.nf_conntrack_count;
printf "max=";
sysctl -n net.netfilter.nf_conntrack_max;
'

查看统计:

sudo conntrack -S

按协议或状态粗略统计:

sudo conntrack -L 2>/dev/null | awk '{print $1}' | sort | uniq -c

在高流量系统中,conntrack -L 可能本身产生明显开销或输出巨大,不应在生产高峰无条件执行。优先查看计数器、采样和日志。

同时观察:

dmesg -T | grep -i 'nf_conntrack'

如果出现 table full,应进一步回答:

  • 是活跃业务流量多,还是扫描流量多;
  • TCP 还是 UDP 占用为主;
  • 超时是否过长;
  • 是否真的需要对这类流量做跟踪;
  • 增加容量后,内存是否足够。

九、一个可重复的调优验证流程

下面是一种比“直接改参数”更可靠的实验顺序。

第一步:确定故障阶段

使用 tcpdumpss 和应用日志确认:

  • SYN 是否到达;
  • SYN-ACK 是否返回;
  • ACK 是否完成;
  • 连接是否进入 Accept 队列;
  • 应用是否及时 accept()
  • 建连后是否出现 Recv-QSend-Q 增长;
  • Conntrack 是否有对应状态项。

第二步:只改变一个变量

例如先只修改:

sudo sysctl -w net.core.somaxconn=4096

不要同时把:

  • somaxconn
  • tcp_max_syn_backlog
  • rmem_max
  • wmem_max
  • nf_conntrack_max
  • ip_local_port_range

全部改大。否则即使压测结果变化,也无法知道是哪一个参数产生了影响。

第三步:重新建立连接并重复相同负载

部分参数在新建 Socket 或新建连接时读取。压测应固定:

  • 客户端数量;
  • 连接建立速率;
  • 请求大小;
  • 持续时间;
  • RTT 和带宽;
  • 服务端进程数;
  • 内核版本和 CPU 绑核方式。

第四步:同时采集多个层次的证据

至少采集:

ss -s
ss -lnt
nstat -az
cat /proc/net/sockstat
ip -s link
sudo conntrack -S

必要时加上:

mpstat -P ALL 1
sar -n DEV 1
sar -n TCP,ETCP 1

如果安装了 perf,还可以分析软中断、协议栈和锁竞争,但应根据具体瓶颈选择事件,不能把 perf 输出当作网络调优结论本身。

第五步:验证副作用

提高参数后,检查:

  • 内存是否持续增长;
  • TCP_mem 是否进入压力状态;
  • Conntrack slab 是否增加;
  • P99/P999 延迟是否反而上升;
  • 失败请求是否从客户端超时变成服务端排队;
  • 大量连接关闭后 TIME_WAIT 是否积累;
  • 其他租户或容器是否受到资源争抢。

十、典型故障的因果分析

场景一:高延迟链路吞吐低

现象:

  • 单条 TCP 连接吞吐明显低于链路能力;
  • CPU 不高;
  • 重传不多;
  • Send-Q 或通告接收窗口显示受限。

推导:

  1. RTT 较高,BDP 较大;
  2. TCP 可用窗口小于 BDP;
  3. 发送端发送完窗口内数据后必须等待 ACK;
  4. 链路存在空闲时间;
  5. 增大合适的 TCP Buffer,并确认自动调节和窗口缩放生效。

反例:如果抓包发现大量重传,根因可能是丢包或拥塞控制,而不是 Buffer 太小。


场景二:服务端新连接失败,但已有连接正常

现象:

  • 已建立请求仍能处理;
  • 新连接偶发超时;
  • 监听 Recv-Q 接近上限;
  • 应用 CPU 可能并不高,但入口线程被阻塞。

推导:

  1. TCP 握手完成;
  2. 连接进入 Accept 队列;
  3. 应用调用 accept() 的速度低于完成握手的速度;
  4. Accept 队列达到上限;
  5. 后续连接出现延迟、丢弃或 RST。

处理重点是:

  • 检查应用是否及时 accept()
  • 检查事件循环和 TLS 初始化;
  • 确认应用的 listen(backlog)
  • 再评估 somaxconn

单独提高 tcp_max_syn_backlog 对这个问题通常无效,因为瓶颈已经不在半连接队列。


场景三:大量短连接后出现 EADDRNOTAVAIL

现象:

  • 客户端频繁建立到同一目标的短 TCP 连接;
  • TIME_WAIT 数量高;
  • 创建连接失败;
  • 增大服务端监听 Backlog 无效。

推导:

  1. 客户端主动关闭,产生大量 TIME_WAIT;
  2. 相同目标四元组组合在 TIME_WAIT 期间受到复用限制;
  3. 可用临时端口和安全可复用组合不足;
  4. 出现本地地址或端口分配失败。

优先检查连接复用、临时端口规划和 NAT 资源,而不是调整 Socket 接收缓冲区。


场景四:UDP 流量突然大量丢失

现象:

  • 应用未及时读取 UDP;
  • Socket 接收队列增长;
  • 内核 UDP 接收错误或丢包计数增加;
  • Conntrack 表可能同时接近上限。

推导:

  1. 报文到达内核;
  2. Conntrack 和协议处理可能成功;
  3. 报文进入 UDP Socket 接收缓冲区;
  4. 应用读取速度低于到达速度;
  5. 缓冲区耗尽后新报文被丢弃。

此时需要分别验证:

  • SO_RCVBUF 和实际 Recv-Q
  • 应用读取线程;
  • CPU 和软中断;
  • Conntrack 是否需要跟踪该流;
  • 网卡输入队列是否更早发生丢包。

只提高 UDP Socket Buffer,无法解决应用长期读取不足;只提高 Conntrack,也无法恢复已丢失的报文。


十一、生产环境中的取舍

11.1 大 Buffer 与低延迟的冲突

Buffer 解决的是速率不匹配和链路带宽利用率问题,但会增加排队。若应用线程被阻塞,较大的缓冲区会让更多数据进入内核,客户端可能更晚才感知服务端处理能力不足。

因此需要同时观察:

  • 吞吐;
  • 排队长度;
  • P99 延迟;
  • 内存;
  • 重传;
  • 应用处理时间。

“吞吐提高但延迟恶化”不一定是成功。

11.2 大 Conntrack 表与内存安全的冲突

提高 nf_conntrack_max 可以减少正常业务因表满失败的概率,但也扩大了攻击或异常流量可消耗的内存范围。应结合:

  • 流量入口限制;
  • SYN Flood 防护;
  • UDP 速率限制;
  • 防火墙规则;
  • Conntrack 超时;
  • 主机内存预算;
  • 连接建立速率。

11.3 增大 Backlog 与故障延迟的冲突

更大的 Accept 队列可以吸收瞬时连接突发,但不会提高应用处理速率。如果到达速率长期高于 accept() 速率,队列只会更晚溢出,并增加客户端等待时间。

Backlog 适合吸收短时突发,不适合掩盖长期服务能力不足。


十二、不要混淆的参数和结论

12.1 somaxconn 不是 Socket Buffer

somaxconn 影响监听队列上限;rmem_maxwmem_max 影响 Socket 数据缓冲区限制。提高其中一个不会自动提高另一个。

12.2 tcp_max_syn_backlog 不是 Accept 队列

前者主要处理尚未完成握手的请求,后者处理已经完成握手、等待应用 accept() 的连接。

12.3 ip_local_port_range 不是服务端监听端口范围

它主要影响自动选择的临时端口。服务端监听 804438080 等固定端口并不由它决定。

12.4 Conntrack 数量不是 TCP 连接数量

一个 TCP 连接通常对应一个主要 Conntrack 项,但 NAT、相关连接、不同网络命名空间、协议类型和状态处理会造成差异。不能用:

ss 中 ESTABLISHED 数量 == nf_conntrack_count

作为严格等式。

12.5 sysctl 值不是实际运行时资源

sysctl 只描述默认值、上限或自动调节策略。实际 Socket 使用多少内存、队列中有多少数据、Conntrack 是否真的接近满,都必须通过运行时统计验证。


十三、一个谨慎的起点配置

下面只展示验证格式,不代表适用于所有机器:

# /etc/sysctl.d/90-network-tuning.conf

# 监听队列:必须结合应用 listen(backlog) 验证
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192

# 仅在确认 BDP、并发和内存预算后设置
# net.core.rmem_max = ...
# net.core.wmem_max = ...
# net.ipv4.tcp_rmem = ...
# net.ipv4.tcp_wmem = ...

# 临时端口:需避开固定服务端口和现有网络规划
# net.ipv4.ip_local_port_range = 20000 60999

# Conntrack:必须根据流速、超时和内存预算计算
# net.netfilter.nf_conntrack_max = ...

加载前先做语法和差异确认:

sudo sysctl --system
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog

然后重新执行相同压测,比较:

  • 新连接成功率;
  • 监听 Recv-Q
  • TCP 重传;
  • Socket Recv-Q/Send-Q
  • TIME_WAIT 数量;
  • Conntrack count/max
  • 主机内存和延迟。

只有当故障证据、参数变化和结果变化形成因果链时,配置才值得进入生产。

Linux 网络调优的核心不是把所有队列都扩大,而是识别数据包所在的队列、确认该队列为何增长、计算它需要的容量,并用运行时证据验证修改是否改善了目标指标,同时没有把压力转移到内存、延迟、端口或 Conntrack。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。