为什么要调优 Linux 网络栈

Linux 内核默认的网络参数是为通用场景设计的,保守而安全。但在高并发 Web 服务、大规模负载均衡器、CDN 节点等场景下,默认值会成为瓶颈。典型症状包括:

  • dmesg 出现 nf_conntrack: table full, dropping packet
  • ss -s 显示大量 TIME-WAIT 连接
  • 压测时 CPU 没跑满但吞吐量上不去
  • 网卡 PPS(包每秒)远低于硬件能力

这些问题大多源于内核网络参数未调优。从协议栈参数、conntrack、网卡多队列、拥塞控制四个层面详细梳理。

内核网络参数的完整说明可参考 kernel.org 文档,本文涉及的参数在 Documentation/networking/ip-sysctl.txt 中有详细定义。

TCP/IP 协议栈关键内核参数

所有参数通过 sysctl/proc/sys/net/ 查看和修改。以下参数均在 net.ipv4 命名空间下。

连接队列参数

# 查看当前值
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn
sysctl net.core.netdev_max_backlog

somaxconn — 监听套接字的全连接队列上限:

# 默认值通常为 128 或 4096(取决于内核版本)
# 高并发场景建议设置为 65535
net.core.somaxconn = 65535

全连接队列存放已完成三次握手但尚未被 accept() 取走的连接。队列满后新连接被丢弃,客户端看到连接超时。注意:应用层面的 listen(fd, backlog) 的 backlog 值不能超过 somaxconn,实际取两者最小值。

tcp_max_syn_backlog — 半连接队列上限:

net.ipv4.tcp_max_syn_backlog = 65535

半连接队列存放已收到 SYN 但尚未完成三次握手的连接。队列满后 SYN 包被丢弃,客户端重试 SYN。SYN Flood 攻击正是通过耗尽半连接队列来拒绝服务。

netdev_max_backlog — 网卡到协议栈的接收队列上限:

net.core.netdev_max_backlog = 250000

当网卡通过中断将数据包传递给内核协议栈时,如果协议栈处理不过来,数据包会暂存在这个队列中。队列满后网卡会丢弃新包。万兆网卡或高 PPS 场景下默认值(1000)远远不够。

TIME-WAIT 相关参数

# 查看 TIME-WAIT 连接数量
ss -s | grep TIME-WAIT

# 典型输出
# TCP:   34567 (estab 8900, closed 24000, orphaned 0, synrecv 0, timewait 24000)

tcp_tw_reuse — 允许复用 TIME-WAIT 状态的连接:

net.ipv4.tcp_tw_reuse = 1

开启后,新的连接可以复用处于 TIME-WAIT 状态的本地端口。这对主动发起大量短连接的场景(如客户端、代理服务器)非常有效。注意区分tcp_tw_reuse 用于出站连接(作为客户端),对入站连接(作为服务端)无效。

不要开启 tcp_tw_reuse 的"兄弟参数" tcp_tw_recycle 这个参数在 NAT 环境下会导致大量连接被丢弃(因为 NAT 后多台机器的时间戳不一致),已在内核 4.12 中被移除。如果你在网上看到建议同时开启两者的文章,那是过时的。

tcp_max_tw_buckets — TIME-WAIT 状态连接数上限:

net.ipv4.tcp_max_tw_buckets = 1048576

超过上限后多余的 TIME-WAIT 连接被直接销毁,并打印警告日志。这个值不是越大越好——每个 TIME-WAIT 连接占用约 1.5KB 内存,100 万个约消耗 1.5GB。

TCP keepalive 参数

# keepalive 探测前的空闲时间(秒)
net.ipv4.tcp_keepalive_time = 600

# 探测间隔(秒)
net.ipv4.tcp_keepalive_intvl = 30

# 探测失败次数,超过后断开连接
net.ipv4.tcp_keepalive_probes = 3

默认值(7200 秒 / 75 秒 / 9 次)意味着一个死连接要 2 小时才被检测到。生产环境建议缩短到 10 分钟内。

TCP 缓冲区参数

# TCP 读缓冲区(最小/默认/最大,单位字节)
net.ipv4.tcp_rmem = 4096 87380 67108864

# TCP 写缓冲区
net.ipv4.tcp_wmem = 4096 65536 67108864

# 网络层接收缓冲区
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

对于高带宽延迟积(BDP)的链路(如跨海传输),适当增大 tcp_rmem 的最大值可以提升单连接吞吐量。但注意内存消耗——每个连接的缓冲区最多可达设定最大值。

conntrack 表优化

conntrack(连接跟踪)是 Netfilter 模块的功能,记录每条网络连接的状态。负载均衡器、NAT 网关和防火墙主机上,conntrack 表满了会导致丢包。

查看状态

# 当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_count
# 示例输出: 234567

# 最大连接数
cat /proc/sys/net/netfilter/nf_conntrack_max
# 示例输出: 262144

# 查看 conntrack 哈希表大小
cat /proc/sys/net/netfilter/nf_conntrack_buckets
# 示例输出: 65536

计算合理的 conntrack_max

conntrack 表大小应根据内存容量设定。每条 conntrack 记录约消耗 320 字节内存:

# 假设 64GB 内存,预留 2GB 给 conntrack
# 2GB / 320B ≈ 6,991,760

# 公式:nf_conntrack_max = 可用内存(字节) / 320
# 但实际上不会全部用于 conntrack,建议按内存的 1%-3% 估算

# 对于 64GB 内存的服务器
echo 1048576 > /proc/sys/net/netfilter/nf_conntrack_max

哈希表大小

conntrack 使用哈希表存储连接记录,哈希表大小必须是 2 的幂次方,且建议为 nf_conntrack_max 的 1/4 到 1/8:

# 哈希表大小只能在模块加载时设置,不能运行时修改
# 需要在加载 nf_conntrack 模块时传入参数

# 临时设置(需先卸载模块)
sudo modprobe -r nf_conntrack
sudo modprobe nf_conntrack hashsize=262144

# 永久设置:创建 modprobe 配置
echo "options nf_conntrack hashsize=262144" | sudo tee /etc/modprobe.d/nf_conntrack.conf

哈希表过小的危害:哈希冲突增加,查找效率从 O(1) 退化为 O(n),导致网络延迟升高。经验值:哈希表大小 >= nf_conntrack_max / 4

conntrack 超时参数

# 查看所有超时参数
sysctl -a | grep nf_conntrack_.*_timeout

# 关键超时参数调整
# TCP 已建立连接的超时(默认 432000 秒 = 5 天,太长)
net.netfilter.nf_conntrack_tcp_timeout_established = 3600

# TIME-WAIT 状态超时(默认 120 秒)
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30

# CLOSE_WAIT 状态超时(默认 60 秒)
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30

# SYN_SENT 状态超时(默认 120 秒)
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 30

tcp_timeout_established 默认 5 天是大坑——大量空闲连接会长期占用表项。负载均衡器上建议设置为 1 小时。

监控 conntrack 使用率

#!/bin/bash
# conntrack 监控脚本
CURRENT=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
MAX=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
RATIO=$(echo "scale=2; $CURRENT * 100 / $MAX" | bc)

echo "conntrack: $CURRENT / $MAX ($RATIO%)"

if (( $(echo "$RATIO > 80" | bc -l) )); then
    echo "WARNING: conntrack table usage above 80%!"
fi

网卡中断与 RPS/RFS/XPS 多队列绑定

现代多核服务器的网卡通常支持多队列(multi-queue),每个队列对应一个硬件中断,可以绑定到不同的 CPU 核上处理,实现网络包的并行处理。

查看网卡队列与中断

# 查看网卡队列数量
ethtool -l eth0

# 输出示例
# Channel parameters for eth0:
# Pre-set maximums:
# RX:		0
# TX:		0
# Other:		0
# Combined:	8       # 最大支持 8 个组合队列
# Current hardware settings:
# RX:		0
# TX:		0
# Other:		0
# Combined:	8       # 当前使用 8 个队列

# 设置队列数(通常等于 CPU 核数)
sudo ethtool -L eth0 combined 8

# 查看中断号
cat /proc/interrupts | grep eth0

# 输出示例
#            CPU0       CPU1       CPU2       CPU3
#  31:   ...   0          0          0          0   PCI-MSI  -eth0-rx-0
#  32:   ...   0          0          0          0   PCI-MSI  -eth0-rx-1
#  33:   ...   0          0          0          0   PCI-MSI  -eth0-tx-0
#  34:   ...   0          0          0          0   PCI-MSI  -eth0-tx-1

绑定中断到 CPU 核

#!/bin/bash
# 将 eth0 的 RX 队列中断均匀绑定到各 CPU 核
# 适用于 8 核 CPU + 8 队列网卡

IRQS=$(grep eth0-rx /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
CPU=0
MASKS=(1 2 4 8 10 20 40 80)  # CPU0-CPU7 的掩码(十六进制)

for irq in $IRQS; do
    # 设置中断亲和性
    printf "%x" $((1 << $CPU)) > /proc/irq/$irq/smp_affinity
    echo "IRQ $irq -> CPU$CPU"
    CPU=$((CPU + 1))
done

注意:smp_affinity 写入的是位掩码。例如 00000001 表示 CPU0,00000002 表示 CPU1,00000003 表示 CPU0+CPU1。

RPS(Receive Packet Steering)

当网卡不支持多队列或队列数少于 CPU 核数时,RPS 在软件层面实现类似效果——将收包的软中断分散到多个 CPU 核:

# 设置 eth0 的 rx-0 队列由 CPU0-CPU7 处理(8 核掩码:ff)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 对所有 RX 队列设置
for i in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
    echo ff > $i
done

# 设置 RPS 流表大小(提升流分发准确性)
echo 32768 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

RFS(Receive Flow Steering)

RFS 在 RPS 基础上进一步优化:不只是把包分散到不同 CPU,而是把同一个连接的包送到处理该连接的应用程序所在的 CPU,提升 CPU 缓存命中率:

# 全局 RFS 流表大小(建议 = RPS 队列数 × 每队列 rps_flow_cnt)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 每队列流表大小
for i in /sys/class/net/eth0/queues/rx-*/rps_flow_cnt; do
    echo 4096 > $i
done

XPS(Transmit Packet Steering)

XPS 是发送方向的优化,指定哪些 CPU 可以通过哪些发送队列发包,减少发送路径上的锁竞争:

# 设置 tx-0 队列只能由 CPU0 使用
echo 01 > /sys/class/net/eth0/queues/tx-0/xps_cpus

# 8 队列对应 8 核
QUEUES=(0 1 2 3 4 5 6 7)
CPUS=(01 02 04 08 10 20 40 80)
for i in "${!QUEUES[@]}"; do
    echo ${CPUS[$i]} > /sys/class/net/eth0/queues/tx-${QUEUES[$i]}/xps_cpus
done

RPS/RFS/XPS 效果对比

技术方向作用硬件要求
中断绑定RX硬件中断分散到不同核网卡支持多队列
RPSRX软中断分散到不同核无(纯软件)
RFSRX按连接亲和性分发到应用所在核无(纯软件)
XPSTX发送队列绑定到特定核,减少锁竞争无(纯软件)

TCP BBR 拥塞控制

BBR(Bottleneck Bandwidth and RTT)是 Google 开发的拥塞控制算法,2016 年进入 Linux 内核(4.9+)。与传统的 CUBIC 算法基于丢包判断拥塞不同,BBR 通过探测瓶颈带宽往返时延来控制发送速率。

启用 BBR

# 查看当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 默认输出: net.ipv4.tcp_congestion_control = cubic

# 查看内核支持的算法
sysctl net.ipv4.tcp_available_congestion_control
# 输出: net.ipv4.tcp_available_congestion_control = reno cubic bbr

# 如果没有 bbr,需要加载模块
sudo modprobe tcp_bbr

# 启用 BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

# 同时设置默认 qdisc 为 fq(BBR 推荐搭配 fq 队列调度)
sudo sysctl -w net.core.default_qdisc=fq

永久生效

# 写入 /etc/sysctl.d/99-bbr.conf
sudo tee /etc/sysctl.d/99-bbr.conf << 'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF

# 加载并验证
sudo sysctl -p /etc/sysctl.d/99-bbr.conf
sysctl net.ipv4.tcp_congestion_control
# 输出: net.ipv4.tcp_congestion_control = bbr

BBR vs CUBIC 效果对比

BBR 的核心优势在长肥管道(高带宽 × 高延迟)场景下最为明显:

# 使用 iperf3 对比测试
# 服务端
iperf3 -s

# 客户端 - CUBIC
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic
iperf3 -c <server_ip> -t 30 -P 4

# 客户端 - BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
iperf3 -c <server_ip> -t 30 -P 4

典型结果对比(跨地域场景):

场景CUBIC 吞吐BBR 吞吐提升
同机房(RTT < 1ms)9.8 Gbps9.9 Gbps无明显差异
跨城(RTT ~30ms)2.1 Gbps7.8 Gbps3.7x
跨洲(RTT ~200ms)180 Mbps1.2 Gbps6.7x
弱网(1% 丢包)45 Mbps890 Mbps19.8x

BBR 在有丢包的网络环境下提升尤为显著,因为 CUBIC 一遇到丢包就大幅降速,而 BBR 不依赖丢包信号。

BBR 的注意事项

  1. BBR v1 vs v2:内核 5.4+ 开始支持 BBR v2(在 tcp_bbr2 模块中),v2 修复了 v1 在浅队列链路上的不公平性和 bufferbloat 问题。如果内核版本支持,建议使用 v2。
  2. 公共云限制:部分云厂商(如 AWS)的内核不支持加载自定义 BBR 模块,需使用云厂商提供的优化内核。
  3. 不是万能药:BBR 优化的是单连接吞吐。对于大量短连接的场景(如 HTTP API),连接建立开销和 TLS 握手才是瓶颈,拥塞控制算法影响不大。

生产环境调优清单

以下是一份适用于高并发 Web 服务器的完整 sysctl.conf 配置,已在生产环境验证:

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

##############################################
# 连接队列
##############################################
# 全连接队列
net.core.somaxconn = 65535
# 半连接队列
net.ipv4.tcp_max_syn_backlog = 65535
# 网卡接收队列
net.core.netdev_max_backlog = 250000

##############################################
# TCP 参数
##############################################
# 复用 TIME-WAIT 端口(仅对出站连接有效)
net.ipv4.tcp_tw_reuse = 1
# TIME-WAIT 数量上限
net.ipv4.tcp_max_tw_buckets = 1048576
# SYN+ACK 重试次数(防 SYN Flood)
net.ipv4.tcp_synack_retries = 2
# SYN 重试次数
net.ipv4.tcp_syn_retries = 2
# FIN-WAIT-2 超时时间(秒)
net.ipv4.tcp_fin_timeout = 15

# TCP keepalive
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

# TCP 缓冲区(最小/默认/最大)
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576

# 开启 TCP Fast Open(减少一个 RTT)
net.ipv4.tcp_fastopen = 3

# 开启 MTU 探测
net.ipv4.tcp_mtu_probing = 1

# 禁用慢启动重启(长连接复用时保持窗口大小)
net.ipv4.tcp_slow_start_after_idle = 0

# 本地端口范围(出站连接可用端口)
net.ipv4.ip_local_port_range = 10000 65535

##############################################
# conntrack(负载均衡器/NAT 网关需要)
##############################################
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 30

##############################################
# 拥塞控制
##############################################
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

##############################################
# 其他
##############################################
# 开启反向路径过滤(防 IP 欺骗)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# 禁止 ICMP 重定向
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
# 禁止源路由
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# 应用配置
sudo sysctl -p /etc/sysctl.d/99-network-tuning.conf

# 验证关键参数
sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse \
       net.ipv4.tcp_congestion_control net.netfilter.nf_conntrack_max

调优验证清单

检查项命令预期
全连接队列溢出netstat -s | grep overflowed增量为 0
半连接队列溢出netstat -s | grep "SYNs to LISTEN"增量为 0
conntrack 满丢包dmesg | grep "table full"无输出
TIME-WAIT 过多ss -s | grep timewait< conntrack_max
BBR 已启用sysctl tcp_congestion_controlbbr
端口耗尽ss -s | grep TCPestab < port_range

小结

Linux 网络栈调优是系统工程,涉及从内核协议栈到网卡硬件的多个层次。核心原则是:

  1. 先监控再调优:不要盲目改参数,先用 netstat -sss -snstat 和 conntrack 监控定位瓶颈。
  2. 分层次优化:连接队列 → conntrack → 网卡多队列 → 拥塞控制,按影响从大到小逐层调。
  3. 理解原理而非背参数:知道每个参数控制什么,才能在不同场景下做正确的取舍。
  4. 测试验证:每次只改一组参数,用 iperf3wrk 或真实流量压测验证效果。

延伸阅读:Linux 网络栈性能优化指南(kernel.org) 涵盖了更底层的参数定义和原理说明,适合需要深入理解内核网络子系统的读者。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. kernel.org 文档 — Linux 内核组织,参考了kernel.org 文档相关内容