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]
不同资源对应不同位置:
net.core.netdev_max_backlog主要影响网络处理来不及及时处理时,内核设备输入队列能暂存多少包。- Conntrack 表保存流的跟踪项;如果表满,即使 TCP 的 Socket 资源充足,也可能因为 Netfilter 无法创建状态项而丢包。
- TCP SYN 队列暂存收到 SYN、等待握手完成的请求。
- Accept 队列暂存已经完成 TCP 握手、等待应用
accept()的连接。 - 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_default 和 net.core.wmem_max
分别对应发送方向的默认值和通用上限。
net.ipv4.tcp_rmem
三个数字分别表示:
最小值 默认值 最大值
它用于 TCP 接收缓冲区的自动调节。启用 TCP 接收缓冲区自动调节时,内核会根据吞吐、RTT、窗口和内存压力,在这个范围内调整连接的接收缓冲区。
net.ipv4.tcp_wmem
三个数字同样表示 TCP 发送缓冲区自动调节所使用的最小值、默认值和最大值。
需要注意,tcp_rmem、tcp_wmem 与 net.core.rmem_max、net.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 容量
正确做法是:
- 查看应用实际调用的
setsockopt(); - 查看应用随后通过
getsockopt()读回的值; - 用
ss -m观察连接的内存统计; - 通过吞吐、RTT、重传和丢包验证是否真的解决了问题。
2.4 TCP 窗口、BDP 与 Buffer 的关系
TCP 接收窗口反映接收端还能接受多少数据。为了让长距离高速链路不因等待 ACK 而停顿,缓冲区至少要能覆盖带宽时延积:
其中:
Bandwidth是链路有效带宽,单位为 bit/s;RTT是往返时延,单位为 s;BDP的结果是 bit,需要除以 8 转成 byte。
完整算例
假设:
- 链路带宽为
1 Gbit/s; - RTT 为
50 ms = 0.05 s; - 忽略协议头和拥塞控制的额外影响。
则:
换算为字节:
也就是约 5.96 MiB。
如果 TCP 发送端或接收端的有效窗口只有 1 MiB,即使物理链路能达到 1 Gbit/s,单条连接也可能被窗口限制。粗略上限为:
例如:
约为 167.8 Mbit/s,明显低于 1 Gbit/s。
但 BDP 只是必要条件,不是性能保证。实际吞吐还受到以下因素限制:
- TCP 拥塞窗口
cwnd; - 对端接收窗口
rwnd; - 丢包和重传;
- 拥塞控制算法;
- CPU 和加密开销;
- 网卡队列和 qdisc;
- 应用是否持续读写;
- 内核内存压力。
当 RTT 为 1 ms、带宽为 100 Mbit/s 时:
这时把每条连接的缓冲区扩大到几十 MiB,通常不会带来收益,反而会放大并发连接的内存占用。
2.5 接收缓冲区过小和过大的两种失败模式
过小
表现可能包括:
- TCP 接收窗口较小;
- 高 RTT 链路吞吐上不去;
- 应用读取稍慢时,发送端受到窗口限制;
- UDP 接收缓冲区溢出,出现丢包;
ss -u -a、/proc/net/snmp或应用统计中出现接收错误。
TCP 中,接收缓冲区不足不一定立即表现为丢包。TCP 会通过缩小通告窗口施加反压,最终表现为吞吐下降或发送端阻塞。
过大
表现可能包括:
- 高并发下内存占用显著增加;
- 应用读取速度慢时,数据在内核中排队更久;
- 队列变深,形成 bufferbloat,延迟上升;
- 连接数暴增时触发 TCP 内存压力或全局内存回收;
- 误以为“缓冲区大”可以弥补应用处理能力不足。
例如,假设服务有 50,000 个并发 TCP 连接,每个连接的接收缓冲区目标值为 8 MiB。仅从数量级看:
内核不会简单地立即为每个连接实际分配完整最大值,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_mem、inuse、orphan 等字段有助于判断是否出现大量 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 限制,近似可以理解为:
这是一个便于理解的模型,具体边界值和 Socket 类型应以对应内核实现为准。
因此:
listen(fd, 65535);
并不意味着一定能排队 65535 个连接。如果 net.core.somaxconn 较小,内核仍会限制实际值。
3.2 为什么应用不调用 accept() 会导致连接问题
假设:
- 服务端应用每秒只能调用
accept()并处理1000个新连接; - 客户端每秒发起
5000个连接; - Accept 队列容量为
1024。
在短时间内,已完成握手的连接进入 Accept 队列的速度高于应用取出的速度:
队列很快达到上限。此时继续到达的连接可能出现:
- 连接建立延迟;
- 客户端
connect()超时或重试; - 服务端统计中出现监听溢出;
- 某些配置下内核直接丢弃 ACK 或复位连接。
查看 TCP 统计:
nstat -az | egrep 'Listen|Syncookies|TCPReqQFull|TCPBacklog'
不同内核版本的计数器名称不完全一致,可以先完整查看:
nstat -az | less
也可以查看:
netstat -s | egrep -i 'listen|SYN|overflow|drop'
netstat 在很多现代发行版中属于兼容工具,优先使用 ss、nstat 和 /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 做压测时,要区分两类测试:
- 连接建立速率测试:验证 SYN 和 Accept 队列;
- 持续数据吞吐测试:验证 Socket Buffer、拥塞控制和应用读写。
只进行 curl 或少量手工连接,通常无法触发 Backlog 问题。
四、Port:端口资源、四元组和 TIME_WAIT
4.1 一个 TCP 连接由四元组标识
TCP 连接通常由以下四元组唯一标识:
例如:
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 等场景选择临时端口时,通常会在该范围内分配端口。范围包含多少端口可按:
计算。上例有:
但这不是“最多只能建立 28232 个出站连接”。完整四元组允许同一个本地端口连接到不同的远端 IP 和端口;实际限制还取决于:
- 远端地址和端口是否相同;
- 是否存在旧连接或 TIME_WAIT;
bind()方式;SO_REUSEADDR、SO_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,以便:
- 确保旧连接中延迟到达的报文不会污染新连接;
- 在必要时重传最后的 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: table full, dropping packet
此时可能出现:
- 新 TCP 连接失败;
- NAT 新映射建立失败;
- DNS、UDP、短连接业务间歇性超时;
- 已建立连接通常仍能继续,直到其状态项超时或业务需要新的相关流;
- 防火墙的
ESTABLISHED,RELATED规则行为异常。
注意,“表满”不一定表示 CPU 或带宽已满。它可能是大量短连接、UDP 超时过长、扫描流量或 NAT 映射长期占用造成的。
5.4 估算 Conntrack 容量
一个基础估算是:
假设:
- 每秒创建
20,000个 UDP 流; - 每个流平均保留
30 s; - 暂不考虑突发、协议分类和安全余量。
则:
至少需要约 600,000 个跟踪项,实际还要为流量突发、TCP 状态、相关连接和管理余量留出空间。
这只是容量估算,不是性能保证。若把 UDP 超时从 30 s 调到 300 s,同样的创建速率理论上会把常驻项需求扩大十倍:
所以,简单地把超时调大可能直接加速表耗尽。
查看常用超时:
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 errorsRX droppedoverruns
使用 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是否长期增长;rb、tb是否达到上限;- 是否只在高 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 是两个不同位置,不能只调后端。
压测时应明确压测类型。例如使用 wrk、h2load、iperf3 或自定义程序时,它们产生的连接复用方式不同:
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
如果怀疑临时端口耗尽,不能只看端口范围,还要确认:
- 远端目标是否固定为同一个 IP 和端口;
- 程序是否显式绑定本地端口;
- 是否存在 NAT;
- 是否大量短连接;
- 错误是否确实为
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 占用为主;
- 超时是否过长;
- 是否真的需要对这类流量做跟踪;
- 增加容量后,内存是否足够。
九、一个可重复的调优验证流程
下面是一种比“直接改参数”更可靠的实验顺序。
第一步:确定故障阶段
使用 tcpdump、ss 和应用日志确认:
- SYN 是否到达;
- SYN-ACK 是否返回;
- ACK 是否完成;
- 连接是否进入 Accept 队列;
- 应用是否及时
accept(); - 建连后是否出现
Recv-Q或Send-Q增长; - Conntrack 是否有对应状态项。
第二步:只改变一个变量
例如先只修改:
sudo sysctl -w net.core.somaxconn=4096
不要同时把:
somaxconntcp_max_syn_backlogrmem_maxwmem_maxnf_conntrack_maxip_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或通告接收窗口显示受限。
推导:
- RTT 较高,BDP 较大;
- TCP 可用窗口小于 BDP;
- 发送端发送完窗口内数据后必须等待 ACK;
- 链路存在空闲时间;
- 增大合适的 TCP Buffer,并确认自动调节和窗口缩放生效。
反例:如果抓包发现大量重传,根因可能是丢包或拥塞控制,而不是 Buffer 太小。
场景二:服务端新连接失败,但已有连接正常
现象:
- 已建立请求仍能处理;
- 新连接偶发超时;
- 监听
Recv-Q接近上限; - 应用 CPU 可能并不高,但入口线程被阻塞。
推导:
- TCP 握手完成;
- 连接进入 Accept 队列;
- 应用调用
accept()的速度低于完成握手的速度; - Accept 队列达到上限;
- 后续连接出现延迟、丢弃或 RST。
处理重点是:
- 检查应用是否及时
accept(); - 检查事件循环和 TLS 初始化;
- 确认应用的
listen(backlog); - 再评估
somaxconn。
单独提高 tcp_max_syn_backlog 对这个问题通常无效,因为瓶颈已经不在半连接队列。
场景三:大量短连接后出现 EADDRNOTAVAIL
现象:
- 客户端频繁建立到同一目标的短 TCP 连接;
- TIME_WAIT 数量高;
- 创建连接失败;
- 增大服务端监听 Backlog 无效。
推导:
- 客户端主动关闭,产生大量 TIME_WAIT;
- 相同目标四元组组合在 TIME_WAIT 期间受到复用限制;
- 可用临时端口和安全可复用组合不足;
- 出现本地地址或端口分配失败。
优先检查连接复用、临时端口规划和 NAT 资源,而不是调整 Socket 接收缓冲区。
场景四:UDP 流量突然大量丢失
现象:
- 应用未及时读取 UDP;
- Socket 接收队列增长;
- 内核 UDP 接收错误或丢包计数增加;
- Conntrack 表可能同时接近上限。
推导:
- 报文到达内核;
- Conntrack 和协议处理可能成功;
- 报文进入 UDP Socket 接收缓冲区;
- 应用读取速度低于到达速度;
- 缓冲区耗尽后新报文被丢弃。
此时需要分别验证:
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_max、wmem_max 影响 Socket 数据缓冲区限制。提高其中一个不会自动提高另一个。
12.2 tcp_max_syn_backlog 不是 Accept 队列
前者主要处理尚未完成握手的请求,后者处理已经完成握手、等待应用 accept() 的连接。
12.3 ip_local_port_range 不是服务端监听端口范围
它主要影响自动选择的临时端口。服务端监听 80、443、8080 等固定端口并不由它决定。
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 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux Network Namespace 与 veth:容器网络、路由和 NAT 实验
- 下一篇:Linux TLS 与证书运维:PKI、链、SNI、OCSP、续期和排障
- 延伸:Linux sysctl 与内核参数:来源、持久化、风险、压测和回滚
- 延伸:Linux TCP 深入:握手、状态机、窗口、重传、拥塞控制和队列
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论