Linux 网络性能全景:从协议栈、C10K 到 NAT、抓包与内核调优
Linux 网络性能全景:从协议栈、C10K 到 NAT、抓包与内核调优
Linux 网络性能问题很少是单点问题。
一个看起来只是“接口变慢”的故障,可能发生在 DNS 解析、TCP 建连、套接字缓冲区、协议栈、软中断、网卡队列、NAT 连接跟踪,甚至应用程序自己的 I/O 模型中。也正因为如此,网络性能分析不能只靠某一个命令,更不能看到 CPU 高就立刻调 CPU,看到延迟高就直接改 TCP 参数。
更可靠的方法,是先建立一条完整的心智模型:
应用程序 -> Socket -> TCP/UDP -> IP -> 链路层 -> 网卡 -> 物理网络
然后沿着这条链路回答三个问题:
- 数据包现在走到了哪里?
- 这一层应该观察什么指标?
- 指标异常后,应该用什么工具继续下钻?
本文围绕这条主线,把 Linux 网络协议栈、收发路径、性能指标、C10K/C1000K、高并发 I/O 模型、基准测试、DNS、抓包、DDoS、网络延迟、NAT 与分层优化方法组织成一个完整体系。
一、先建立网络协议栈的整体模型
1. OSI 七层与 TCP/IP 四层
网络协议必须解决两个问题:
- 不同厂商、不同硬件、不同系统之间如何互联;
- 一个网络包从应用程序到网卡的复杂处理流程如何解耦。
OSI(Open System Interconnection)将网络通信抽象为七层:
| OSI 层 | 主要职责 | 常见理解 |
|---|---|---|
| 应用层 | 为应用提供网络服务 | HTTP、DNS、FTP |
| 表示层 | 数据格式转换 | 编码、序列化、加密表示 |
| 会话层 | 维护通信会话 | 会话建立与管理 |
| 传输层 | 端到端传输 | TCP、UDP |
| 网络层 | 寻址、路由、转发 | IP、ICMP |
| 数据链路层 | MAC 寻址、帧传输、错误检测 | Ethernet |
| 物理层 | 比特在物理介质上传输 | 网线、光纤、射频 |
Linux 实际使用的模型更接近 TCP/IP 四层:
| TCP/IP 层 | 对应 OSI | 典型协议/组件 |
|---|---|---|
| 应用层 | 应用层 + 表示层 + 会话层 | HTTP、DNS、FTP |
| 传输层 | 传输层 | TCP、UDP |
| 网络层 | 网络层 | IP、ICMP |
| 网络接口层 | 数据链路层 + 物理层 | Ethernet、MAC、网卡驱动 |
工程交流中经常说“四层负载均衡”“七层负载均衡”,这里通常沿用的是 OSI 的层级语义:
- 四层负载均衡:主要基于 TCP/UDP、IP、端口转发;
- 七层负载均衡:可以理解 HTTP Host、URI、Header 等应用层信息。
2. 网络包为什么要逐层封装
应用程序发送的数据不会原样直接落到网线上,而是逐层添加协议元数据。
以 HTTP over TCP 为例:
1 | |
接收端执行相反过程:逐层解析头部、确认上层协议并解封装。
可以用 Mermaid 表示为:
flowchart TD
A[应用数据] --> B[传输层: 增加 TCP/UDP 头]
B --> C[网络层: 增加 IP 头并进行路由]
C --> D[链路层: 增加帧头/帧尾]
D --> E[网卡发送]
E2[网卡接收] --> D2[链路层解帧]
D2 --> C2[IP 层解析与路由判断]
C2 --> B2[TCP/UDP 定位 Socket]
B2 --> A2[应用程序读取数据]
3. MTU:为什么“包太大”会影响性能
物理链路不能无限制地传输任意大小的数据包。MTU(Maximum Transmission Unit,最大传输单元)定义了接口允许承载的最大 IP 包大小。
传统以太网最常见的 MTU 是:
1 | |
如果 IP 包超过链路允许的 MTU,就可能发生分片或触发与路径 MTU 相关的处理。分片越多,意味着:
- 更多包头开销;
- 更高 PPS;
- 更多协议栈处理;
- 丢一个分片可能影响整个报文重组。
因此,网络吞吐优化不能只看“带宽有多大”,还要看包有多大。
叠加网络尤其容易踩 MTU 的坑。以 VXLAN 为例,在原始报文外又增加 Ethernet、IP、UDP、VXLAN 等头部。如果底层网络仍只允许 1500B,常见做法是把虚拟网卡 MTU 调低,例如 1450 左右,或者让底层网络支持更大的 MTU。
二、Linux 网络栈到底如何收发一个包
Linux 网络栈可以抽象为下面几层:
1 | |
这里最容易被忽略的是:网络路径不仅是协议处理,还包含系统调用、中断、DMA、内存缓冲区与 CPU 调度。
1. 接收路径
一个典型的物理网卡收包过程可以概括为:
- 网络帧到达网卡;
- 网卡通过 DMA 把数据写入驱动管理的接收 Ring Buffer;
- 网卡产生硬中断,通知 CPU 有新数据;
- 硬中断只做尽量少的工作,把更多处理推迟到软中断;
- 内核用
sk_buff等数据结构表示网络包; - 链路层检查帧并识别上层协议;
- IP 层解析地址、协议号并做路由判断;
- TCP/UDP 根据连接信息找到对应 Socket;
- 数据进入 Socket 接收缓冲区;
- 应用程序通过
recv()、read()等系统调用读取数据。
2. 发送路径
发送方向基本与接收相反:
- 应用调用
send()、sendmsg()等 Socket API; - 系统调用进入内核;
- 数据进入 Socket 发送缓冲区;
- TCP/UDP 添加传输层头部;
- IP 层添加 IP 头并查询路由、确定下一跳;
- 根据 MTU/MSS 等规则进行必要的分段或分片处理;
- 链路层解析下一跳 MAC,封装以太网帧;
- 数据进入发送队列;
- 驱动通过 DMA 让网卡读取待发送数据;
- 网卡把帧发送到物理网络。
3. 硬中断与软中断为什么要拆开
如果所有协议处理都放在网卡硬中断里,CPU 会长时间停留在中断上下文中,其他任务很难获得运行机会。
Linux 因此采用“硬中断做快事、软中断做重活”的思路:
- 硬中断:快速确认设备事件,完成最必要的处理;
- 软中断:继续执行协议栈的大部分收发逻辑。
系统中每个 CPU 都有对应的 ksoftirqd/N 内核线程,例如:
1 | |
但不能把“网络协议栈”等同于某一个内核线程。完整网络处理还会涉及硬中断、软中断、驱动、内存分配、kworker 等多种内核机制。
4. Ring Buffer、sk_buff、Socket Buffer 到底在哪里
这三个概念经常混淆。
Ring Buffer
网卡通过 DMA 与内存交换数据时使用的环形队列,归网卡驱动管理。它不是某个用户进程的内存。
sk_buff
sk_buff 是 Linux 网络协议栈最核心的数据结构之一,用于描述和管理网络包。不同协议层之间传递数据时,很多操作只需要移动数据结构中的指针,而不需要每经过一层都复制完整数据。
Socket Buffer
每个 Socket 都有发送和接收缓冲区:
- 接收缓冲区保存已经到达、但应用还没取走的数据;
- 发送缓冲区保存应用已经写入、但尚未完成发送/确认的数据。
这些缓冲区同样位于内核管理的内存中,并不是简单映射成用户空间的一块数组。
sk_buff、Socket 缓冲区、连接跟踪对象等大量网络内核对象通常由 slab 分配器管理,可以从 /proc/slabinfo 观察相关内存占用。
三、网络性能不是一个数字:先建立指标坐标系
分析网络性能,至少要同时关注以下指标。
| 指标 | 含义 | 常见单位 | 典型问题 |
|---|---|---|---|
| 带宽 Bandwidth | 链路理论最大传输速率 | bit/s | 网卡/链路上限 |
| 吞吐量 Throughput | 单位时间成功传输的数据量 | bit/s、B/s | 是否跑满链路 |
| PPS | 每秒网络包数 | packet/s | 小包、转发、软中断压力 |
| 延迟 Latency | 请求或报文往返时间 | ms、us | RTT、握手、应用响应 |
| 并发连接数 | 同时存在的连接数量 | connections | C10K/C1000K |
| 丢包率 | 丢失报文比例 | % | 队列满、链路异常 |
| 重传率 | TCP 重传报文比例 | % | 拥塞、丢包、接收能力不足 |
| 错误数 | 网卡/协议错误 | packets | 校验、carrier、frame 等 |
| 可用性 | 能否成功通信 | - | 路由、防火墙、DNS、服务异常 |
1. 带宽与吞吐量不是一回事
“千兆网卡”说的是理论链路带宽,不代表应用一定能得到 1Gbps 吞吐。
粗略地说:
1 | |
sar 中的 %ifutil 也可以用来估算接口使用率。资料中的计算方式为:
1 | |
2. PPS 为什么对小包特别重要
同样是 1Gbps:
- 如果每个包很大,需要处理的包数量较少;
- 如果都是 64B 小包,CPU 需要处理的包数量会暴涨。
因此网络转发、DDoS、虚拟交换、NAT 网关等场景不能只看 BPS,还必须看 PPS。
经典千兆以太网 64B 小包的理论线速 PPS 可以近似估算为:
1 | |
其中额外的 20B 用来近似计算前导码与帧间距等线速开销。
四、第一轮排查:先看接口、队列和协议栈
1. 看网络接口:优先使用 ip
传统命令 ifconfig 来自 net-tools,ip 来自 iproute2。两者都能看接口信息,但 ip 的能力更完整。
1 | |
重点检查:
- 接口是否
UP; - 是否存在
LOWER_UP,即物理链路是否真正连通; - MTU 是否合理;
- IP、子网、MAC 是否正确;
- RX/TX 的 packets、bytes、errors、dropped 等是否持续增长。
也可以使用:
1 | |
常见异常指标:
| 指标 | 说明 |
|---|---|
errors |
校验错误、帧同步错误等 |
dropped |
包已经进入接收路径,但因资源不足等原因被丢弃 |
overruns |
Ring Buffer 来不及处理,队列溢出 |
carrier |
链路/双工/物理介质等 carrier 问题 |
collisions |
以太网碰撞计数 |
这些计数大多是累积值。排障时最重要的不是“是否大于 0”,而是是否持续增加。
2. 看网卡能力
1 | |
只想看速率:
1 | |
虚拟网卡、云主机或某些驱动环境不一定能返回真实物理带宽,因此工具输出需要结合实际网络架构理解。
3. 看吞吐量与 PPS
1 | |
典型字段:
1 | |
如果看到:
- BPS 很低,但 PPS 极高:优先怀疑小包洪泛、扫描、异常转发;
- BPS 接近带宽上限:优先看链路是否真的饱和;
- PPS 与软中断 CPU 同时升高:继续检查网卡队列、中断分布和协议栈。
4. 看 Socket:优先使用 ss
1 | |
建立连接状态下:
Recv-Q:应用还没读走的数据;Send-Q:已经发送但尚未被对端确认的数据。
如果这两个值持续很大,说明发生了积压:
Recv-Q大:应用处理太慢、线程阻塞、消费不及时;Send-Q大:对端接收慢、链路拥塞、丢包重传或发送路径受阻。
LISTEN 状态下的 Recv-Q / Send-Q
这是一个很容易被旧资料误导的点。
对现代 Linux 的 ss -lnt 输出,通常应理解为:
Recv-Q:当前已完成握手、等待accept()的全连接队列长度;Send-Q:该监听 Socket 的全连接队列最大长度。
半连接队列则是收到 SYN、但尚未完成三次握手的连接集合,主要受 tcp_max_syn_backlog 等机制影响。
5. 看协议栈统计
1 | |
关注:
- active/passive connection openings;
- failed connection attempts;
- connection resets;
- segments sent/received;
- retransmitted;
- checksum errors。
网络性能分析到这里,已经不是“网卡有没有流量”的问题,而是在确认:数据究竟堵在接口、协议栈还是 Socket。
五、从 C10K 到 C10M:高并发真正卡在哪里
C10K 中的 C 可以理解为 Client/Connection。它讨论的是:一台机器如何同时处理 1 万级连接。
C100K、C1000K、C10M 则把数量级继续推到 10 万、100 万和 1000 万。
1. C10K 的根因不是“机器太慢”,而是 I/O 模型不合适
早期服务器即使只有 2GB 内存和千兆网卡,理论上也足以容纳 1 万个很轻量的连接。真正的问题是传统的“一个连接一个进程/线程”模型:
1 | |
要解决 C10K,核心是两个问题:
- 一个线程如何同时等待多个 Socket?
- 如何用更少的线程服务更多连接?
2. 水平触发与边缘触发
I/O 多路复用之前,先区分两种事件通知方式。
Level Triggered(LT,水平触发)
只要 fd 当前仍然可读/可写,就可以持续得到通知。
优点是编程简单,不容易漏事件。
Edge Triggered(ET,边缘触发)
只有 fd 状态发生变化时通知一次。
ET 下收到通知后,应用需要尽可能把数据读/写到 EAGAIN,否则没有处理完的内容可能不会再次触发同样的边缘事件。
3. select、poll、epoll 的本质区别
| 模型 | fd 管理 | 查找就绪 fd | 主要问题 |
|---|---|---|---|
select |
固定大小位图 | 线性扫描 | fd 数量限制、O(N) 扫描 |
poll |
动态数组 | 线性扫描 | 无固定 1024 限制,但仍 O(N) |
epoll |
内核维护事件集合 | 事件驱动 | 更适合大量 fd |
select/poll 每次调用还要在用户态和内核态之间传递 fd 集合;连接越多,成本越高。
epoll 的关键改进是:
- fd 集合长期保存在内核中;
- 只返回真正发生事件的 fd;
- 不必每次完整扫描所有连接。
这就是 Linux 解决 C10K 的核心机制之一。
4. AIO 与 I/O 多路复用不是一回事
epoll 解决的是“哪些 fd 已经可以执行 I/O”。真正的读写仍由应用发起。
AIO(Asynchronous I/O)则是应用发起操作后继续执行,完成后由系统通知结果。
在网络程序里,I/O 多路复用长期是更常见的高并发基础模型。AIO 的控制流更复杂,边界条件也更多。
5. 高性能服务为什么常用 master + worker
典型模式:
flowchart TD
M[Master/Main Process] --> L[bind + listen]
L --> W1[Worker 1]
L --> W2[Worker 2]
L --> W3[Worker 3]
W1 --> E1[epoll / accept]
W2 --> E2[epoll / accept]
W3 --> E3[epoll / accept]
以 Nginx 一类程序为例:
- master 负责初始化、配置加载与 worker 生命周期;
- worker 长期存在,有任务时被唤醒,无任务时休眠;
- 不会为每个请求重复创建进程。
因此,多进程并不等于频繁 fork,也不必然意味着高上下文切换成本。
6. 惊群、EPOLLEXCLUSIVE 与 SO_REUSEPORT
多个 worker 同时等待同一个事件时,可能出现“惊群”:一个事件唤醒很多进程,最终只有一个能真正处理。
Linux 和应用层逐步对这个问题进行了优化:
accept()的经典惊群问题在较早内核中已经改善;- epoll 后续引入
EPOLLEXCLUSIVE; - Nginx 也曾通过
accept_mutex控制 worker 竞争; - Linux 3.9 以后可使用
SO_REUSEPORT,让多个 Socket 绑定相同地址与端口,由内核做请求分配。
SO_REUSEPORT 模型大致是:
flowchart TD
K[Kernel] --> S1[Socket Listener 1]
K --> S2[Socket Listener 2]
K --> S3[Socket Listener 3]
S1 --> W1[Worker 1]
S2 --> W2[Worker 2]
S3 --> W3[Worker 3]
7. 从 C100K 到 C1000K:开始进入系统工程
到了百万连接,I/O 模型已经不再是唯一瓶颈。
假设 100 万连接每个只消耗 16KB:
1 | |
假设其中 20% 是活跃连接,每个活跃连接平均 1KB/s:
1 | |
此时要同时考虑:
- 文件描述符数量;
- Socket/TCP 缓冲区;
- conntrack;
- 内存容量;
- 网卡带宽;
- 多队列网卡;
- 中断亲和性;
- RPS/RFS;
- TSO/GSO/GRO 等 Offload;
- 应用工作模型。
8. C10M:为什么“继续调 sysctl”最终会失效
当并发继续上升到千万级,传统内核网络路径本身就可能成为瓶颈:
1 | |
每个包都走这么长的路径,即使每一层只消耗一点 CPU,累积后也非常可观。
这时通常出现两类更激进的数据路径。
DPDK
DPDK 的思路是绕过传统内核网络协议栈,由用户态程序直接轮询网卡队列。
典型优化包括:
- Poll Mode Driver;
- Huge Pages;
- CPU 绑定;
- 内存对齐;
- NUMA 感知;
- 批处理和流水线。
轮询并不总是低效。当 PPS 极高、几乎一直有包可处理时,避免频繁中断反而可能更高效。
XDP
XDP(eXpress Data Path)基于 eBPF,可以在报文进入完整内核协议栈之前处理它。
适合:
- DDoS 过滤;
- 高性能 ACL;
- 负载均衡;
- 容器网络数据面;
- 高速报文丢弃与重定向。
对大多数业务系统来说,真正合理的结论不是“单机一定要做到 C10M”,而是:在单机优化复杂度开始失控之前,优先通过水平扩展分散请求。
六、性能优化前先做基准测试:按协议层测,而不是只跑一个压测工具
网络性能评估最重要的原则是:
你要先知道自己在测试协议栈的哪一层。
不同层有不同的性能上限和测试工具。
| 层级 | 重点指标 | 常用工具 |
|---|---|---|
| 网络接口/网络层 | PPS | pktgen、hping3 |
| 传输层 | TCP/UDP 吞吐、丢包、抖动 | iperf3、netperf |
| HTTP 层 | QPS、延迟、吞吐 | ab、wrk |
| 真实业务层 | 业务 QPS、P95/P99、错误率 | wrk Lua、JMeter、LoadRunner、流量回放 |
底层性能决定上层性能上限。如果网络层只能处理 10 万 PPS,应用层不可能凭空突破底层包处理能力。
1. 用 pktgen 测 PPS
pktgen 是 Linux 内核自带的发包框架。加载模块后:
1 | |
通常会看到:
1 | |
一个简化的 64B 发包配置如下:
1 | |
结果可以从:
1 | |
读取,重点看:
- packets;
- PPS;
- Mbit/s;
- errors。
2. 用 iperf3 测 TCP/UDP 吞吐
服务端:
1 | |
客户端:
1 | |
重点关注最终的:
Transfer;Bitrate/Bandwidth;- TCP 重传;
- UDP 丢包与 jitter。
不要把 iperf3 测出来的吞吐量叫“网卡带宽”。网卡带宽是物理能力,iperf 测到的是这条链路在当前条件下的实际吞吐能力。
3. 用 ab 看最基础的 HTTP 性能
1 | |
重点字段:
1 | |
HTTP 压测必须看分位数。平均值经常会掩盖长尾。
4. 用 wrk 构造更高效的 HTTP 压测
1 | |
wrk 的优势不仅是自身性能高,还内置 LuaJIT,可以自定义请求。
例如通过脚本加入动态 Header、认证 token、请求路径和 payload:
1 | |
运行:
1 | |
5. 基准测试最容易犯的错误
压测机先成为瓶颈
如果 ab 自己只能制造 1000 QPS,你无法用它证明服务端上限就是 1000 QPS。
测试场景和生产完全不同
静态首页的 QPS 并不能代表真实 API 的性能。真实请求可能包含:
- JSON payload;
- 鉴权;
- 数据库访问;
- RPC;
- 动态响应体;
- 长连接。
只看平均值
网络系统应至少同时观察:
1 | |
七、DNS:很多“网络慢”其实在连接建立之前就已经慢了
DNS(Domain Name System)负责域名和 IP 地址之间的映射,也是网络访问链条中最容易被忽略的一步。
1. DNS 是分层系统
以:
1 | |
为例:
1 | |
常见记录:
| 记录 | 作用 |
|---|---|
| A | 域名 -> IPv4 |
| AAAA | 域名 -> IPv6 |
| CNAME | 别名 |
| NS | 权威 DNS 服务器 |
| MX | 邮件服务器 |
| PTR | IP -> 域名反向解析 |
DNS 属于应用层,常见查询主要使用 UDP 53,特定场景也会使用 TCP。
2. Linux DNS 配置
最基础的 resolver 配置可以查看:
1 | |
例如:
1 | |
内网静态映射还可以放在:
1 | |
3. nslookup:快速检查是否能解析
1 | |
如果解析失败,先确认:
/etc/resolv.conf是否有 nameserver;- DNS 服务器 IP 是否可达;
- UDP/TCP 53 是否被防火墙阻断;
- 是否处于容器、Network Namespace 等独立网络环境。
4. dig +trace:看完整委派链
1 | |
它会依次展示:
sequenceDiagram
participant C as Client
participant R as Recursive Resolver
participant Root as Root DNS
participant TLD as .org DNS
participant Auth as geekbang.org DNS
C->>R: A time.geekbang.org?
R->>Root: NS org?
Root-->>R: .org NS
R->>TLD: NS geekbang.org?
TLD-->>R: geekbang.org NS
R->>Auth: A time.geekbang.org?
Auth-->>R: A record
R-->>C: IP + TTL
实际客户端一般只向递归 DNS 发一次查询,后面的权威链路由递归 DNS 代为完成,并通过缓存减少重复访问。
5. DNS 解析“时快时慢”怎么分析
一个实用的排查顺序:
1 | |
先测延迟:
1 | |
如果某个远端 DNS RTT 为 140ms 且偶发丢包,而另一台 DNS 只有 30ms,那么解析性能差异会非常明显。
6. DNS 缓存
使用本地缓存可以把大多数重复解析变为本地访问。
资料中的示例使用 dnsmasq:
1 | |
把本机 resolver 指向:
1 | |
第一次需要访问上游 DNS,后续在 TTL 有效期内可直接命中本地缓存。
DNS 优化常见思路:
- 缓存;
- 预取;
- 使用延迟更低、更稳定的 DNS;
- 内网部署自有 resolver;
- 移动端等场景使用 HTTPDNS;
- 通过 DNS/GSLB 返回更靠近用户的服务节点。
八、tcpdump + Wireshark:从“猜”升级为“看见报文”
很多网络问题,仅靠 ping、ss、sar 无法定位,因为这些工具看到的是统计结果,而不是某一条连接真实发生了什么。
抓包就是证据链。
1. tcpdump 的基本格式
1 | |
常用选项:
| 选项 | 作用 |
|---|---|
-i eth0 |
指定接口 |
-i any |
所有接口 |
-nn |
不解析主机名和端口服务名 |
-c N |
抓 N 个包后退出 |
-A |
ASCII 显示 payload |
-e |
显示链路层信息 |
-w file.pcap |
写入 pcap 文件 |
2. 常用过滤表达式
1 | |
3. tcpdump 输出怎么看
典型格式:
1 | |
DNS:
1 | |
TCP Flags 常见:
1 | |
4. 一个经典案例:ping 的 RTT 只有 30ms,为什么命令跑了 11 秒
抓包后发现:
1 | |
真正慢的不是 ICMP,而是 ping 在输出主机名时触发的 PTR 反向解析。
解决方法:禁止名称解析。
1 | |
这个案例最重要的不是 -n 参数,而是诊断方式:
工具显示的“总耗时”和协议本身的 RTT 不一致时,抓包去看工具背后额外做了什么。
5. 保存 pcap 给 Wireshark
1 | |
然后把文件复制到带图形界面的机器:
1 | |
Wireshark 的优势是:
- 自动解析协议层;
- Follow TCP Stream;
- 显示重传、Dup ACK、Zero Window 等分析提示;
- Statistics -> Flow Graph;
- Conversation/Endpoint 统计;
- 可以直观看三次握手和连接关闭过程。
6. TCP 四次挥手为什么有时抓到三个包
理论过程通常写成:
1 | |
但如果服务端收到 FIN 时本身也准备关闭连接,可以把 ACK 和自己的 FIN 合并:
1 | |
因此,协议图描述的是允许的逻辑过程,真实抓包可能因为 ACK 合并等优化而减少包数。
九、网络延迟变大:不要只看 ping,要拆成“网络延迟 + 应用延迟”
1. 网络延迟与应用延迟
网络 RTT:
1 | |
应用响应时间则是:
1 | |
所以:
1 | |
2. ICMP 被禁时怎么测
可以用 TCP SYN 测服务端口:
1 | |
或者:
1 | |
这类测试比单纯 ICMP 更接近真正业务访问路径。
3. 单请求不慢,并发一上来就慢
一个很有价值的诊断模式是:
1 | |
这说明链路基础 RTT 正常,问题更可能出现在:
- 应用处理;
- Socket 行为;
- TCP 算法交互;
- 队列。
这时用:
1 | |
再配合:
1 | |
4. Delayed ACK + Nagle:两个“优化”叠加成 40ms 延迟
Delayed ACK 的目标是减少 ACK 包:收到数据后先等一小段时间,如果正好有数据要返回,就把 ACK 捎带出去。
Nagle 算法的目标是减少小包:当还有一个未确认的小分组在途时,暂缓继续发送新的小分组。
单独看都合理,但组合在一起可能形成等待:
sequenceDiagram
participant S as Server (Nagle)
participant C as Client (Delayed ACK)
S->>C: Segment 1
Note over C: 等待是否能捎带 ACK
Note over S: 等 Segment 1 ACK,暂不发 Segment 2
C-->>S: ACK(约 40ms 后)
S->>C: Segment 2
用 strace 可以检查客户端是否设置了 TCP 选项:
1 | |
典型可看到:
1 | |
Nginx 侧如果关闭了 tcp_nodelay:
1 | |
就可能保留 Nagle 行为。改为:
1 | |
即可禁止 Nagle,让小响应不必等待前一个 ACK。
这类问题的关键经验是:
网络优化参数不是越多越好。两个单独看起来都能减少包数量的优化,组合后可能显著增加延迟。
十、NAT:性能问题的核心不是“改地址”,而是连接跟踪
NAT(Network Address Translation)通过修改 IP 包中的地址/端口,实现内外网映射。
常见分类:
1. 静态 NAT、动态 NAT、NAPT
- 静态 NAT:内外 IP 一对一固定映射;
- 动态 NAT:从公网地址池动态分配;
- NAPT:多个内网 IP 通过不同端口共享公网 IP。
Linux 中最常见的是 NAPT。
2. SNAT、DNAT 与双向转换
SNAT
只改源地址/源端口,典型用途是内网访问外网。
1 | |
DNAT
只改目的地址/目的端口,典型用途是把公网入口映射到内网服务。
1 | |
双向映射
入口 DNAT,返回方向根据连接跟踪自动完成相应的逆向转换;也可以组合显式 SNAT/DNAT 规则实现更复杂映射。
3. Netfilter、table、chain
iptables 是 Netfilter 的用户态配置工具之一。
经典表:
| table | 主要用途 |
|---|---|
filter |
包过滤 |
nat |
NAT |
mangle |
修改报文属性 |
raw |
连接跟踪前的原始处理 |
nat 表常见链:
| chain | 时机 | 常见动作 |
|---|---|---|
PREROUTING |
路由判断前 | DNAT |
POSTROUTING |
路由判断后 | SNAT/MASQUERADE |
OUTPUT |
本机产生的包 | 本地 DNAT 等 |
4. SNAT / MASQUERADE 示例
动态出口地址:
1 | |
固定源地址:
1 | |
5. DNAT 示例
1 | |
作为路由/NAT 网关时还要开启转发:
1 | |
持久化时写入 sysctl 配置文件,而不是只依赖临时命令。
6. conntrack 为什么会成为 NAT 的性能核心
Linux 有状态 NAT 需要知道“这个响应包对应哪个原始请求”,所以会维护连接跟踪表。
一个连接通常用以下信息标识:
1 | |
也就是常说的五元组。
连接跟踪的主要内核参数:
1 | |
含义:
1 | |
7. nf_conntrack: table full 是非常直接的证据
如果 NAT 场景下并发突然下降、连接大量超时,先看内核日志:
1 | |
如果出现:
1 | |
说明连接跟踪表已满,新的包会被丢弃。
可以临时调整:
1 | |
但不能只会“把 max 调大”。conntrack 是内存哈希表,容量越大,内存消耗也越高;bucket 太少还会增加哈希冲突和链表查找成本。
资料中的一个估算思路为:
1 | |
具体对象大小与内核版本、架构有关,不应把示例值当成永久常量。
8. 直接看 conntrack 里都是什么连接
1 | |
统计总数:
1 | |
统计 TCP 状态:
1 | |
如果大量记录处于 TIME_WAIT,可以继续看:
1 | |
在短连接极多、且业务允许的情况下,可以评估适当缩短 conntrack 状态超时时间,但需要基于真实流量验证,而不是照抄固定数值。
9. NAT 性能排查的完整链路
资料中用 SystemTap 跟踪 kfree_skb(),发现大量丢包位于 nf_hook_slow,再用 perf 继续展开调用栈,最终看到:
1 | |
这个案例说明了一个非常重要的排障套路:
1 | |
10. 当有状态 NAT 本身就是瓶颈
如果业务根本不需要动态状态跟踪,只做固定映射,可以考虑更轻量的无状态数据面。
极端高 PPS 环境下还可以考虑:
tc/eBPF;- XDP;
- DPDK;
- 专用硬件网关。
优化的根本原则是:不要为了实现一个简单的地址改写,承担不需要的状态维护成本。
十一、DDoS:从“网络性能异常”识别攻击流量
DDoS(Distributed Denial of Service)可以从三个层面耗尽资源:
- 带宽耗尽:链路被流量打满;
- 系统资源耗尽:CPU、内存、连接表、conntrack、Socket 队列被耗尽;
- 应用资源耗尽:恶意请求触发昂贵计算、数据库、DNS 查询等。
1. SYN Flood 的典型特征
攻击者持续发送 SYN,但不完成三次握手:
sequenceDiagram
participant A as Source
participant S as Server
A->>S: SYN
S-->>A: SYN + ACK
Note over S: 等待最后 ACK,连接停留 SYN_RECV
A->>S: SYN
S-->>A: SYN + ACK
大量半连接会占满半连接队列。
2. 怎么从性能指标发现 SYN Flood
先看 PPS/BPS:
1 | |
如果:
1 | |
说明很可能是小包洪泛。
抓包确认:
1 | |
再看半连接:
1 | |
或者传统方式:
1 | |
3. Linux 内核层的缓解手段
查看半连接容量:
1 | |
调整示例:
1 | |
减少 SYN+ACK 重试:
1 | |
开启 SYN Cookies:
1 | |
SYN Cookies 的思想是:服务器收到 SYN 时不必立刻为它长期保存完整半连接状态,而是把连接信息编码进返回序列号;只有客户端真正返回合法 ACK 时,才恢复连接状态。
4. 为什么服务器内部只能“缓解” DDoS
如果恶意流量已经到达服务器:
- 网卡已经收包;
- PCIe/DMA 已经发生;
- IRQ/softirq 已经消耗 CPU;
- 协议栈至少已经处理了一部分。
即使最后在 iptables 丢掉,也已经付出了成本。
所以大规模 DDoS 需要分层防御:
1 | |
如果攻击已经把机房公网带宽打满,服务器本机任何 sysctl 都无法“创造额外带宽”。必须在更上游丢弃流量。
十二、真正的网络优化:沿协议栈逐层做,不要先抄 sysctl
优化前先明确目标。
不同系统的“快”不是一回事:
- NAT 网关:更关心 PPS 和接近线速转发;
- 数据库、缓存:更关心低延迟;
- Web 服务:需要兼顾 QPS、吞吐和延迟;
- 大文件传输:更关心 BPS 与链路利用率;
- 长连接网关:更关心连接数、内存和事件处理效率。
因此优化流程应该是:
flowchart LR
A[明确业务目标] --> B[建立基准]
B --> C[观察指标]
C --> D[定位协议层]
D --> E[只优化瓶颈层]
E --> F[重新压测]
F --> G{是否改善}
G -- 是 --> H[固化配置并监控]
G -- 否 --> C
1. 应用层:先减少不必要的网络工作
I/O 多路复用
高并发网络服务优先避免“一连接一线程”的同步阻塞模型,使用 epoll 等事件驱动模型。
合理的 worker 模型
选择:
- master + workers;
- event loop + worker pool;
SO_REUSEPORT多监听 Socket。
核心目标是:让网络事件处理和业务 CPU 计算解耦,避免一个慢请求拖住事件循环。
长连接替代频繁短连接
短连接每次都要付出:
1 | |
如果业务允许,连接池和 Keep-Alive 往往比调一堆 TCP 参数更有效。
减少 payload
常见手段:
- 二进制序列化;
- 压缩;
- 批量请求;
- 合并小消息;
- 避免重复传输不变化数据。
缓存
缓存既减少业务 CPU,也减少网络 I/O:
- DNS 缓存;
- 应用对象缓存;
- CDN;
- 静态资源缓存。
2. Socket 层:缓冲区不是越大越好
Socket 有读写缓冲区:
rmem太小:高带宽、高 RTT 下可能限制吞吐;wmem太小:发送端容易阻塞;- 太大:每个连接占用更多内存,百万连接时会放大为巨大内存压力。
常见参数:
1 | |
TCP 自动调节参数通常是三个值:
1 | |
一个重要估算是 BDP(Bandwidth-Delay Product,带宽时延积):
1 | |
如果链路是 10Gbps、RTT 是 20ms,要真正跑满链路,窗口/缓冲能力至少要覆盖相当数量的在途数据。
3. Socket 行为参数:TCP_NODELAY、TCP_CORK
TCP_NODELAY
禁用 Nagle,适合低延迟、小消息场景。
程序级:
1 | |
Nginx:
1 | |
TCP_CORK
暂时把小数据“塞住”,等待聚合成更大报文再发送,适合明确知道完整响应边界的场景。
它和 TCP_NODELAY 的目标相反,不能机械地两个都“开了试试”。
4. TCP:围绕连接生命周期优化
TCP 优化必须先理解:
- 三次握手;
- 四次挥手;
- SYN_RECV;
- ESTABLISHED;
- FIN_WAIT;
- CLOSE_WAIT;
- TIME_WAIT;
- 流量控制;
- 拥塞控制;
- Nagle;
- Delayed ACK;
- Keepalive。
大量 TIME_WAIT
先确认为什么会产生:通常是主动关闭连接的一方进入 TIME_WAIT。
更优先的应用层办法是:
- 连接池;
- HTTP Keep-Alive;
- 减少无意义短连接。
系统层可以观察:
1 | |
注意一个常见历史误区:
net.ipv4.tcp_fin_timeout不是用来直接修改 TIME_WAIT 的 2MSL 时长,它主要影响 FIN_WAIT_2 等关闭阶段行为。不要把它当作“TIME_WAIT 秒数”照抄。
另一个历史参数:
1 | |
因为 NAT 等环境下容易导致连接异常,早已被内核删除,不应再作为优化方案。
本地端口范围
1 | |
客户端主动连接外部服务时,需要本地临时端口。微服务、代理、NAT 网关都可能因为大量主动连接而受到临时端口资源限制。
文件描述符
Socket 本质上也是文件描述符。
1 | |
systemd 服务还需要检查:
1 | |
不要只改 ulimit,却忘记服务实际由 systemd 启动。
SYN 队列
1 | |
这类参数主要用于连接突发与 SYN Flood 缓解。调整时要同时观察:
- SYN_RECV;
- accept queue;
- 丢包;
- listen overflow;
- 应用
accept()能力。
TCP Keepalive
1 | |
它适合发现“连接看起来还在,但对端实际上已经消失”的情况。
但业务层如果已经有自己的心跳协议,也要评估是否还需要过于激进的 TCP Keepalive,避免重复探测。
5. UDP:重点是缓冲区和包大小
UDP 没有连接状态和可靠重传逻辑,优化相对简单:
- 调整 Socket/UDP 缓冲区;
- 扩大本地端口范围;
- 控制单个 datagram 大小;
- 尽量避免 IP 分片;
- 应用自己处理丢包、乱序、重试。
UDP “没有重传”并不代表没有性能问题,只是可靠性成本转移到了应用层。
6. 网络层:路由、转发、MTU、ICMP
作为路由器、NAT、容器宿主机时:
1 | |
反向路径校验:
1 | |
它可以帮助过滤明显的源地址欺骗,但复杂多路径/策略路由环境需要谨慎评估。
MTU
1 | |
overlay 网络要保证整个路径 MTU 一致。如果底层仍是 1500,而 VXLAN 又增加额外头部,虚拟接口通常需要更小 MTU。
巨帧
在端到端所有设备都支持时,MTU 9000 一类 Jumbo Frame 可以降低同等吞吐量下的 PPS 和 CPU 开销。
前提是:整条链路一致支持。 中间任意设备不支持都可能产生难查的黑洞或分片问题。
7. 链路层和网卡:把每个 CPU 核真正用起来
高 PPS 场景下,单个 CPU 处理所有中断很容易成为瓶颈。
IRQ affinity / irqbalance
检查:
1 | |
可以通过 IRQ affinity 或 irqbalance 把网卡中断分散到多个 CPU。
RSS
RSS(Receive Side Scaling)利用网卡硬件多队列,把不同流分配到多个 RX queue,再由不同 CPU 处理。
RPS / RFS
当硬件 RSS 不足或需要更细调度时:
- RPS:软件层把包分散到不同 CPU;
- RFS:尽量让软中断处理 CPU 与应用消费该流的 CPU 靠近,提高 cache locality。
8. Offload:让网卡做它擅长的工作
| Offload | 作用 |
|---|---|
| TSO | 网卡完成 TCP 大包分段 |
| GSO | 在更靠近网卡的位置统一分段 |
| UFO | UDP 分片卸载(历史机制) |
| LRO | 网卡接收侧合并多个 TCP 包 |
| GRO | 更通用的接收侧聚合 |
| VXLAN Offload | 网卡处理部分 VXLAN 封装/校验 |
查看:
1 | |
不能简单认为“所有 offload 都应该打开”。例如转发、抓包、虚拟网络等场景下,某些 offload 会改变你看到的包形态,甚至与特定转发逻辑冲突。
9. 队列越大不一定越快
增大:
- Ring Buffer;
- qdisc 队列;
- Socket Buffer;
- backlog;
确实能减少突发丢包,但代价可能是排队时间上升。
换句话说:
1 | |
如果目标是实时交易、RPC、数据库,盲目加大缓冲区可能只是把“丢包”变成“排队 500ms”。
10. QoS 与 Traffic Control
当不同流量竞争同一接口时,可以使用 Linux Traffic Control:
1 | |
通过 qdisc/class/filter 做:
- 限速;
- 优先级;
- 流量整形;
- 模拟延迟/丢包;
- QoS。
这属于“管理竞争关系”,不是凭空提高链路带宽。
十三、网络工具应该怎么记:按“指标”而不是按命令背
1. 从指标找工具
| 想看什么 | 工具 |
|---|---|
| 接口配置/状态 | ip、ifconfig |
| BPS/PPS | sar、/proc/net/dev |
| 进程流量 | nethogs |
| IP/连接流量 | iftop |
| Socket/连接状态 | ss、netstat |
| 网卡速率/Offload | ethtool |
| RTT | ping、hping3 |
| 路由路径 | traceroute、mtr、ip route |
| DNS | dig、nslookup |
| 抓包 | tcpdump、Wireshark |
| NAT/防火墙 | iptables |
| conntrack | conntrack |
| CPU/内核热点 | perf |
| 动态内核跟踪 | SystemTap、bcc/eBPF 工具 |
2. 从工具反推问题
ss
最适合快速确认:
- 连接数;
- TCP 状态;
- Recv-Q / Send-Q;
- 监听端口;
- 进程。
sar
最适合快速确认:
- PPS;
- BPS;
- 接口错误;
- TCP/UDP/ICMP 统计。
tcpdump
最适合回答:
“这个包到底有没有发出去?对端到底有没有回?”
Wireshark
最适合回答:
“这条 TCP 流的交互顺序是什么?哪里出现重传、延迟 ACK、窗口异常或连接关闭?”
perf / eBPF / SystemTap
最适合回答:
“包进入内核后 CPU 到底花在哪里?”
十四、几个特别容易混淆的知识点
1. 服务器连接数并不等于 65535
TCP/UDP 端口是 16 位,但一条连接并不是只用“服务器端口”标识,而是由五元组区分:
1 | |
因此服务器可以在同一个 :80 上同时服务海量客户端:
1 | |
它们是不同连接。
对主动发起连接的一方,本地临时端口确实是重要资源,但更精确地说,限制与源 IP、目标 IP/端口组合及 ip_local_port_range 有关,而不是整台机器永久只能有 65535 条 TCP 连接。
2. sk_buff 不是 Socket 接收缓冲区
sk_buff 是内核描述网络包的数据结构;Socket Buffer 是面向某个 Socket 的收发队列。两者处于不同抽象层次。
3. Bandwidth、BPS、PPS 不能互换
1 | |
64B 小包和 1500B 大包,即使 BPS 一样,CPU 成本可能完全不同。
4. ping 通不代表 TCP 一定通,ping 不通也不代表服务一定挂了
ICMP 可能被禁。
真正排业务端口时可以使用:
1 | |
5. Recv-Q/Send-Q 的含义依赖 Socket 状态
ESTABLISHED 和 LISTEN 的含义不同。排障前先看 State,不要脱离状态解释队列值。
6. TIME_WAIT 多不一定是故障
大量短连接业务天然会产生很多 TIME_WAIT。正确问题应该是:
- 是否耗尽临时端口?
- 是否耗尽 conntrack?
- 是否导致内存异常?
- 是否因为短连接设计本身不合理?
而不是“看到 TIME_WAIT 就把 timeout 调成 1 秒”。
7. NAT 不等于安全边界
NAT 隐藏了内部地址,并不代表应用自动安全。只要端口映射、转发或返回路径允许访问,应用漏洞仍然存在。
十五、一套可直接用于线上排障的网络性能流程
阶段 1:确认现象
先回答:
1 | |
如果连问题类型都没定义清楚,后面所有调优都容易跑偏。
阶段 2:看基础接口
1 | |
确认:
- 链路;
- MTU;
- errors/drop;
- BPS/PPS;
- 路由。
阶段 3:看 TCP/Socket
1 | |
关注:
1 | |
阶段 4:确认 DNS 和路径 RTT
1 | |
如果 IP 访问快、域名访问慢,优先排 DNS。
阶段 5:抓包
1 | |
用 Wireshark 看:
- 握手耗时;
- 重传;
- Dup ACK;
- RST;
- Zero Window;
- Delayed ACK;
- 请求到首字节时间;
- FIN 过程。
阶段 6:怀疑 NAT/防火墙时看 conntrack
1 | |
阶段 7:怀疑内核 CPU 时继续下钻
1 | |
然后再根据热点决定是否需要:
- IRQ affinity;
- RSS/RPS/RFS;
- Offload;
- conntrack 调整;
- Socket buffer;
- XDP/DPDK。
阶段 8:任何优化都必须回到基准测试
优化前:
1 | |
优化后重新用同样流量、同样机器、同样测试窗口测一次。
如果没有数据对比,“感觉快了”不算性能优化。
十六、最终应形成的网络性能心智模型
把全文压缩成一条链路,就是:
1 | |
对应的分析原则也可以压缩成一句话:
先用指标确定是哪一层,再用工具找到证据,最后只优化真正的瓶颈。
Linux 网络性能优化最危险的做法,是从网上复制一份“万能 sysctl.conf”,一次性改几十个参数。网络参数之间存在大量依赖和权衡:缓冲区变大可能提高吞吐却增加延迟,Nagle 和 Delayed ACK 可能相互放大等待,conntrack 增大可以容纳更多状态却会消耗更多内存,Offload 能降低 CPU 也可能改变转发和抓包行为。
真正稳定的优化过程应该是:
1 | |
做到这一点,DNS 超时、Socket 堆积、TCP 重传、NAT table full、SYN Flood、40ms 神秘延迟、软中断跑满、网卡队列丢包,就不再是互不相关的一堆“Linux 网络玄学”,而只是同一条数据路径上发生在不同位置的性能问题。