# 网络高并发的内核调优 —— 往内核里拧哪些旋钮

> [overview.md](/concepts/network/overview.md) 说高并发有**应用层**和**内核层**两条优化线;应用层(IO 模型、并发模型)在 [io-models.md](/concepts/network/io-models.md)/[epoll.md](/concepts/network/epoll.md)/[thread-models.md](/concepts/network/thread-models.md),本篇讲**内核层**——一台机器要扛住成千上万连接,内核网络栈的默认参数往往不够,得按领域拧一组旋钮。


> 本篇按"连接队列 / fd 上限 / 缓冲区 / TIME_WAIT / 端口 / 网卡中断 / conntrack"分块,每块讲**什么问题、调哪个参数、怎么权衡、怎么观测**。参数"往哪写、临时 vs 永久"见 [../../tools/proc/kernel-tuning.md](/tools/proc/kernel-tuning.md),本篇不重复入口机制,只讲该调什么。

## 一、连接队列(backlog):连接还没被 accept 就排在这

一个 TCP 连接从 SYN 到能被 `accept()` 取走,要经过**两个内核队列**:

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<q>> #FFF9C4
  BorderColor<<q>>     #F9A825
  BackgroundColor<<app>> #C8E6C9
  BorderColor<<app>>   #388E3C
}
rectangle "客户端 SYN 到达" as SYN
rectangle "① 半连接队列 (SYN queue)\n收到 SYN、回了 SYN-ACK、\n等客户端 ACK 的连接\n上限: tcp_max_syn_backlog" <<q>> as HALF
rectangle "② 全连接队列 (accept queue)\n三次握手完成、等应用 accept() 取走\n上限: min(somaxconn, listen()的backlog)" <<q>> as FULL
rectangle "应用 accept()\n取走连接开始处理" <<app>> as APP
SYN -down-> HALF
HALF -down-> FULL : 三次握手完成
FULL -down-> APP
note right of HALF : 满了 → 丢 SYN\n(SYN flood 也打这里)
note right of FULL : 满了 → 丢连接 / 丢 ACK\n应用 accept 太慢就会满
@enduml
```

- **半连接队列满**:新来的 SYN 被丢(SYN flood 攻击就是灌满它)。调 `net.ipv4.tcp_max_syn_backlog`;开 `tcp_syncookies` 抗 SYN flood(队列满时用 cookie 不占队列)。
- **全连接队列满**:握手都完成了却因应用 `accept()` 太慢挤爆,连接被丢/重置。上限是 **`min(net.core.somaxconn, listen() 第二个参数)`**——两个都要够大,只调 sysctl 不改代码的 `listen(fd, backlog)` 也白搭。
- **观测**:`ss -lnt` 看 listen socket 的 `Recv-Q`(当前全连接队列积压)/`Send-Q`(队列上限);`netstat -s | grep -i "listen\|overflow"` 看溢出计数(`times the listen queue of a socket overflowed`)。

> **一句话**:连接进不了应用,先怀疑两个队列——SYN queue 满是握手阶段丢 SYN、accept queue 满是应用 accept 跟不上;`somaxconn` 和代码里的 `listen()` backlog **要一起调**。

## 二、文件描述符上限:每连接吃一个 fd

高并发第一道硬墙:**一个连接 = 一个 fd**,万级连接就要万级 fd,而默认 `ulimit -n` 常常只有 1024。三层上限都要够:

| 层级 | 参数 | 含义 |
|------|------|------|
| 进程级 | `ulimit -n`(RLIMIT_NOFILE)| 单进程能开多少 fd,默认常 1024,高并发要调到几十万 |
| 系统级(可分配)| `fs.nr_open` | 单进程 fd 上限的硬顶(ulimit 不能超过它)|
| 系统级(总量)| `fs.file-max` | 全系统能打开的 fd 总数 |

不调 fd 上限,连接数一到 1024 就 `accept()` 返回 `EMFILE`(too many open files)——高并发第一个撞的坑。

## 三、TCP 收发缓冲区:大带宽/高时延链路要调大

每个 TCP 连接有收(rmem)/发(wmem)缓冲区。缓冲区太小,高带宽×高时延(BDP 大)的链路灌不满、吞吐上不去;太大则**每连接占内存多**,万级连接下内存爆炸——高并发要在两者间权衡。

- `net.core.rmem_max` / `wmem_max`:单个 socket 缓冲区的**上限**。
- `net.ipv4.tcp_rmem` / `tcp_wmem`:`最小 默认 最大` 三元组,内核在这范围内**自动调整**(autotuning)。
- **BDP 视角**:理想缓冲区 ≈ 带宽 × RTT。10Gbps × 10ms ≈ 12.5MB——默认几百 KB 根本喂不满,长肥管道(LFN)必须调大 `tcp_rmem/wmem` 上限。
- 权衡:高并发短连接(内存敏感)别把默认值调太大;少量大吞吐长连接才需要大缓冲区。

## 四、TIME_WAIT:大量短连接的头号麻烦

主动关闭方进入 **TIME_WAIT** 状态并停留 2×MSL(默认 60s)。高并发**短连接**(如没连接池的 HTTP)会瞬间堆出几万个 TIME_WAIT,占满**临时端口**,新连接 `connect()` 失败(端口耗尽)。

| 手段 | 作用 | 注意 |
|------|------|------|
| `net.ipv4.tcp_tw_reuse=1` | 允许把 TIME_WAIT 的连接**复用于新的出站连接**(有时间戳保护,安全)| 只影响**主动连接**侧(客户端/反代到上游)|
| `net.ipv4.ip_local_port_range` 调宽 | 扩大临时端口池(如 `1024 65535`)| 缓解端口耗尽 |
| **长连接 / 连接池** | 从根上不产生海量短连接 | **真正的根治**,优先做这个 |
| ~~`tcp_tw_recycle`~~ | **已废弃、别用** | NAT 环境下会错误丢包,4.12 内核已移除 |

> **一句话**:TIME_WAIT 堆积是"短连接 + 主动关闭"的必然产物;`tcp_tw_reuse` + 调宽端口范围能缓解,但**根治是改用长连接/连接池**;`tcp_tw_recycle` 是坑,永远别开。

> 深入阅读：[tcp-timewait.md](/concepts/network/tcp-timewait.md) —— TIME_WAIT 为什么设计 2×MSL、`tcp_tw_reuse` 的时间戳安全原理、`tcp_tw_recycle` 废弃根因、SO_LINGER 的陷阱、短连接端口耗尽的数学上限与根治方案。

## 五、网卡与中断层:别让单核扛所有收包

连接数上去后,**网卡收包中断**可能压垮单个 CPU 核(所有软中断都在一个核上,呼应 [../process/interrupts.md](/concepts/process/interrupts.md) 的软中断/ksoftirqd)。硬件侧高并发靠"把收包分散到多核":

| 技术 | 做什么 | 层次 |
|------|--------|------|
| **RSS**(Receive Side Scaling)| 多队列网卡按流哈希(五元组)把包分到**多个硬件队列**,每队列绑一个 CPU | 硬件 |
| **RPS**(Receive Packet Steering)| 单队列网卡的软件版:内核把收到的包按哈希分发到多核处理软中断 | 软件 |
| **RFS**(Receive Flow Steering)| 在 RPS 基础上让包尽量送到"处理该连接的应用所在的核",提升 cache 命中 | 软件 |
| **中断亲和性** | `/proc/irq/<n>/smp_affinity` 把网卡各队列的中断绑到不同核,别全挤一个核(呼应 [../process/thread-affinity.md](/concepts/process/thread-affinity.md))| 配置 |
| **offload**(GRO/GSO/TSO)| 把分包/合包卸载给网卡硬件,减少 CPU 处理的包数(`ethtool -k` 看)| 硬件卸载 |

> **一句话**:高并发下"收包"本身就是负载——用多队列网卡 + RSS/RPS/RFS 把中断和软中断摊到多核、把队列中断绑到不同 CPU,别让一个核成为全网卡的瓶颈。

## 六、conntrack:防火墙连接跟踪表会满

如果机器开了 netfilter/iptables 的**连接跟踪**(NAT、状态防火墙都依赖它),每条连接在 `nf_conntrack` 表里占一项。高并发下表满 → `nf_conntrack: table full, dropping packet` → **丢包**。

- 调大 `net.netfilter.nf_conntrack_max`、`nf_conntrack_buckets`;缩短 `nf_conntrack_tcp_timeout_*`。
- 高并发**纯转发网关**若不需要状态跟踪,直接在 `raw` 表 `NOTRACK` 关掉 conntrack,省表省 CPU。

## 七、其他常用旋钮

- **`TCP_NODELAY`**(socket 选项,关 Nagle):小包低延迟场景关掉 Nagle 算法,别攒包。延迟敏感服务必设。
- **`net.ipv4.tcp_fastopen`**:TFO 让部分数据在三次握手期间就带上,省一个 RTT。
- **`SO_REUSEPORT`**:多 worker 各自 listen 同端口、内核负载均衡、无惊群——详见 [thread-models.md](/concepts/network/thread-models.md)。

## 八、总表:问题 → 参数 → 观测

| 症状 | 关键参数 | 观测命令 |
|------|---------|---------|
| 连接被拒/丢 SYN | `tcp_max_syn_backlog`、`somaxconn` + listen backlog、`tcp_syncookies` | `ss -lnt`、`netstat -s \| grep overflow` |
| accept EMFILE | `ulimit -n`、`fs.file-max`、`fs.nr_open` | `ls /proc/<pid>/fd \| wc -l`、`ulimit -n` |
| 吞吐上不去(长肥管道)| `tcp_rmem`/`tcp_wmem`、`rmem_max`/`wmem_max` | `ss -ti`(看 cwnd/rtt) |
| 端口耗尽/TIME_WAIT 多 | `tcp_tw_reuse`、`ip_local_port_range`、连接池 | `ss -s`、`ss -tan state time-wait \| wc -l` |
| 单核软中断 100% | RSS/RPS/RFS、中断亲和性 | `mpstat -P ALL`(%soft)、`/proc/interrupts` |
| 丢包 + conntrack full | `nf_conntrack_max`、NOTRACK | `dmesg \| grep conntrack`、`conntrack -C` |

## 九、和本仓库其他文档的关系

- **配置入口**:[../../tools/proc/kernel-tuning.md](/tools/proc/kernel-tuning.md)(sysctl / /proc/sys / /sys / 临时 vs 永久)——本篇讲"调什么",那篇讲"往哪写"。
- **观测**:[../../tools/network/ss.md](/tools/network/ss.md)(连接状态、队列积压)。
- **中断/收包**:[../process/interrupts.md](/concepts/process/interrupts.md)(软中断、网卡收包全流程)、[../process/thread-affinity.md](/concepts/process/thread-affinity.md)(中断/worker 绑核)。
- **应用层配套**:[thread-models.md](/concepts/network/thread-models.md)(SO_REUSEPORT)、[epoll.md](/concepts/network/epoll.md)、[overview.md](/concepts/network/overview.md)。

## 十、一句话总结

> **内核网络调优围绕"连接进得来、连接扛得住、收包分得开"三件事:队列(SYN/accept backlog + somaxconn/listen)保证连接进得来、fd 上限和 TCP 缓冲区保证连接扛得住且不撑爆内存、TIME_WAIT/端口范围解决短连接端口耗尽(根治是长连接)、RSS/RPS/RFS + 中断亲和性把收包摊到多核别让单核成瓶颈、conntrack 表调大或关掉防丢包。调什么看本篇,往哪写看 kernel-tuning.md,调完用 ss/mpstat/netstat -s 验证。**
