第31章 性能调优:并发模型、连接池、超时与重试、压测
学习目标
- 掌握代理性能的四个维度与并发模型选择
- 用好连接池、超时分级、重试退避,避开"重试放大"
- 调对关键内核参数(fd、conntrack、端口范围、backlog)
- 用正确的压测方法测尾延迟,避开"协调遗漏"陷阱
前置知识
原理
四个性能维度
吞吐 QPS / 带宽(每秒处理多少)
延迟 p50 / p99 / p999(尾延迟比均值更重要!)
并发 同时维持多少连接(fd、内存)
资源 CPU / 内存 / fd(成本)
代理优化的核心是尾延迟(p99/p999),不是均值。用户体验由最慢的那部分请求决定,而代理是每个请求的必经之路。
并发模型
| 模型 | 代表 | 特点 |
|---|---|---|
| 多进程 + 事件驱动 | Nginx | worker 各跑 epoll,SO_REUSEPORT 分摊 accept |
| 多线程 + 事件驱动 | HAProxy(新)、Envoy | 线程间负载均衡 |
| 协程 | Go(Traefik/frp) | goroutine-per-conn(第21章) |
| 异步 | Rust/tokio(pingora) | 无栈状态机(第22章) |
调优要点:worker 数 = CPU 核数、绑核(CPU affinity)减少缓存抖动、SO_REUSEPORT 让多 worker 并行 accept 避免惊群。
连接池:最高杠杆的优化(呼应第03章)
第03章 讲过,代理→后端每次新建 TCP+TLS 太贵。连接池复用是性价比最高的优化:
无池:每请求 1 次 TCP 握手 + 1~2 次 TLS 握手(~100ms+)
有池:复用空闲连接,省掉全部握手
关键参数:池大小(keepalive/max_idle_conns)、空闲超时、单连接最大请求数(防止连接老化打不散负载,第09章 的长连接陷阱)。Nginx 别忘 proxy_http_version 1.1 + 清 Connection(第13章)。
超时分级:少一个就出诡异问题
connect timeout 连后端的超时(快速失败)
read/server timeout 等后端响应(防慢后端拖死)
client/idle timeout 客户端空闲(回收僵尸连接)
整体 timeout 请求总时长上限
太长 → 连接/内存堆积;太短 → 误杀正常慢请求。HAProxy 三超时(第14章)是必修课。
重试与退避:小心放大
重试能提可用性,但用不好会雪崩:
- 幂等性(第10章):POST 别盲目重试
- 重试放大:每层都重试 3 次,3 层就是 27 倍流量 → 把摇摇欲坠的后端彻底打死
- 缓解:指数退避 + 抖动、重试预算(限制重试占比,如 ≤10%)、配合熔断(错误率高就停止打后端)
关键内核参数
| 参数 | 作用 | 症状 |
|---|---|---|
ulimit -n / fs.file-max | 最大打开 fd | 连接数撞顶 → Too many open files |
net.core.somaxconn + listen backlog | accept 队列 | 高并发握手丢失、连接被拒 |
net.ipv4.ip_local_port_range | 出向源端口范围 | 代理→后端连接多时端口耗尽 |
net.ipv4.tcp_tw_reuse | 复用 TIME_WAIT | 短连接下 TIME_WAIT 堆积 |
nf_conntrack_max | conntrack 表大小 | 透明代理/NAT 下表满丢包(第11章) |
TLS 性能
TLS 握手是大头,优化手段:会话复用(session resumption / tickets)、TLS 1.3(1-RTT,0-RTT)、OCSP stapling、硬件/指令集加速(AES-NI)、把 TLS 卸载到代理(第05章)。
压测:避开"协调遗漏"陷阱
最容易被坑的一点:ab、wrk 会严重低估尾延迟。原因是"协调遗漏(coordinated omission)"——当被测系统卡住,压测工具也跟着等,于是漏报了本该发生的慢请求。
正确做法:用
wrk2(或 vegeta)以恒定速率压测(-R),它在固定速率下补偿被遗漏的延迟,p99/p999 才真实。先用低速率找基线,逐步加压找拐点(延迟开始陡升处)。
️ 实现 / 命令
实验一:内核参数调优
# 提高 fd 上限
ulimit -n 1048576
# sysctl 关键项
sudo sysctl -w net.core.somaxconn=65535
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.netfilter.nf_conntrack_max=1048576 # 透明代理/NAT 场景
实验二:Nginx 连接池 + worker 调优
worker_processes auto; # = CPU 核数
worker_rlimit_nofile 1048576;
events { worker_connections 65535; use epoll; }
upstream backend {
server 10.0.0.11:8080;
keepalive 64; # 到后端的连接池
keepalive_requests 10000;
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # 复用必备(第13章)
}
}
实验三:用 wrk2 正确测尾延迟
# 恒定 10000 QPS 压 30 秒,看真实 p99/p999(-R 是关键,避开协调遗漏)
wrk2 -t8 -c200 -d30s -R10000 --latency http://proxy/api
# Latency Distribution:
# 50% 1.20ms
# 99% 18.5ms ← 真实尾延迟(ab/wrk 会报得更乐观)
# 99.9% 120ms
# 逐步加 -R,找到 p99 陡升的拐点 = 系统容量上限
实验四:排查连接耗尽
ss -s # 连接总览,看 TIME_WAIT/ESTAB 数量
ss -tan state time-wait | wc -l # TIME_WAIT 堆积?
cat /proc/sys/net/netfilter/nf_conntrack_count # conntrack 用量 vs max
lsof -p $(pgrep nginx) | wc -l # fd 用量 vs ulimit
排错
| 现象 | 根因 | 解决 |
|---|---|---|
Too many open files | fd 撞 ulimit | 提高 ulimit -n/worker_rlimit_nofile |
| 高并发握手被拒/丢 | backlog/somaxconn 太小 | 调 somaxconn + listen backlog |
| 代理→后端端口耗尽 | 短连接 + 端口范围小 | 连接池复用 + 扩端口范围 + tw_reuse |
| 透明代理/NAT 下偶发丢包 | conntrack 表满 | 调 nf_conntrack_max |
| 尾延迟实测远高于压测报告 | 协调遗漏 | 用 wrk2 -R 恒定速率 |
| 偶发 502,后端没满 | 连接池太小/连接老化 | 调 keepalive、keepalive_requests |
| 重试把后端打挂 | 重试放大 | 退避 + 重试预算 + 熔断 |
本章小结
- 优化核心是尾延迟(p99/p999);并发模型选事件驱动/协程,worker=核数 + 绑核 +
SO_REUSEPORT。 - 连接池是最高杠杆(省握手);超时分级必配齐;重试要防放大(退避+预算+熔断)。
- 调对内核参数:fd、somaxconn、端口范围、conntrack。
- 压测必用
wrk2恒定速率避开协调遗漏,逐步加压找拐点。
下一章 第32章 排错决策树,把全书的故障场景收成一棵决策树——从症状到根因,分段二分快速定位。