nginx upstream keepalive 深度调优:默认值、性能对比与 5 个真实 502 案例
1. 踩坑起点:一次 PHP-FPM 挂掉引发的连环 502
上周帮一个电商客户排查生产 502,一开始完全往别处走了。nginx -T 看着配置完全正常,upstream 里 keepalive 32 也配了,location 里 proxy_http_version 1.1 和 proxy_set_header Connection "" 也在,三件套齐了。
但 5 分钟前 php-fpm 有台机器 OOM 重启过一次。重启那一刻,Nginx 端 upstream 池里还有 4 条指向那台机器的 ESTAB 连接,全都是死连接——内核还没收到 RST,Nginx 也不知道后端已经没了。用户请求进池子取出死连接,一发请求直接 502,error.log 满屏 connect() failed (104: Connection reset by peer) while connecting to upstream。
我盯着 ss -tanp | grep :9000 | grep ESTAB 的输出看了两分钟才反应过来——池子里那 4 条连接是"看起来活着"的死连接。这是 keepalive 调优最容易踩的坑:Nginx 侧的健康检查只管"能不能连上",不管"这次请求能不能成功"。死连接在池里躺着,谁抽到谁倒霉。
从此我对 upstream keepalive 三个参数格外敏感,下面把这套东西彻底拆一遍。
2. keepalive 三参数到底是什么
先在 nginx.conf 里搜一遍确认自己在改哪个:
# 找出所有 keepalive 相关配置
nginx -T | grep -i keepalive
# 只看 upstream 里的 keepalive 池大小
grep -n "keepalive" /etc/nginx/conf.d/*.conf /etc/nginx/nginx.conf
# 看当前 nginx 与后端之间的 ESTAB 连接数
ss -tanp | grep ESTAB | awk '{print $4}' | cut -d: -f2 | sort | uniq -c
upstream 上下文里,keepalive N 表示"每个 worker 进程最多缓存 N 条空闲连接"。注意是每个 worker,不是全局。如果 worker_processes 4,实际最大空闲池就是 4 × 32 = 128 条。
http/server/location 上下文里的 keepalive_timeout 是 Nginx 侧的空闲回收时间——不管这条连接是对客户端还是对后端,超过这个时间没被复用就关掉。upstream 场景下默认 60 秒。
keepalive_requests N 是单条连接的复用上限。达到 N 次复用后强制关闭重建。默认 1000,很多人以为配了就够了,但短命后端(比如 Node.js 集群里跑几分钟就重启的 worker)会提前把它打死。
三者语义速查:
| 参数 | 位置 | 默认值 | 单位 | 含义 |
|---|---|---|---|---|
keepalive |
upstream | 8 | 连接数 | 每 worker 空闲连接池大小 |
keepalive_timeout |
http/server/location | 60 | 秒 | 空闲多久后回收 |
keepalive_requests |
http/server/location | 1000 | 请求数 | 单连接复用上限 |
3. 4 类后端的默认值差异
后端不是傻等的——它们自己也有关连接的能力。如果 Nginx 的 keepalive_timeout 比后端长,Nginx 池里就会躺着后端已经关闭的死连接。
| 后端类型 | 默认 keepalive 超时 | 关键配置项 | 生产建议值 |
|---|---|---|---|
| Node.js (Express/Koa) | 5s | server.keepAliveTimeout = 65000 |
≥ 65s(比 nginx 大) |
| Go net/http | 90s | ReadTimeout/IdleTimeout |
≥ 100s |
| Java Tomcat | 20s | keepAliveTimeout |
≥ 60s |
| PHP-FPM | 20s | request_terminate_timeout |
≥ 60s |
关键原则:Nginx keepalive_timeout 必须严格小于后端 keepalive_timeout,建议差 5-15 秒缓冲。这样 Nginx 会先关掉空闲连接,后端永远不会主动关 Nginx 还认为活着的连接。
4. 默认值 8 为什么是坑
算一笔账就明白了。假设你的服务:1000 QPS,平均响应时间 50ms,4 个 worker。
- 单 worker 每秒处理 250 请求,平均响应 50ms,稳定并发 ≈ 250 × 0.05 = 12.5 条活跃连接
- 加波峰余量,按 24 条活跃算
- keepalive 池里空闲的还要另算,池子大小 8 意味着每秒钟有 24 - 8 = 16 条连接是新建的
1000 QPS 下每秒新建 16 条 TCP 连接,看着不多,但每次新建连接都有 TCP 三次握手 + 后端 accept 队列排队 + 文件描述符申请释放。CPU 上表现为 tcp_connect、accept、socket_create 系统调用占比飙升。
我实测过的数据:
| keepalive | QPS(单接口) | 单请求 P99 | CPU%(nginx worker) |
|---|---|---|---|
| 8 | 8500 | 32ms | 42% |
| 32 | 15200 | 18ms | 28% |
| 64 | 17800 | 16ms | 31% |
| 128 | 17400 | 17ms | 35% |
从 8 到 32,QPS 翻了近 80%;32 到 64 还有收益但边际递减;64 到 128 反而下降——超过了 worker_connections 的一半,Nginx 侧 fd 开始紧张。所以生产建议是 32 起步,压力大了再往 64 加,别无脑到 256。
5. keepalive 命中判断:三个命令看穿真相
写完配置一定要验证命中,不然你以为配了其实没生效:
# 1. 看 ESTAB 连接数是否稳定(复用)还是持续增长(新建)
watch -n1 'ss -tanp | grep :8080 | grep ESTAB | wc -l'
# 2. curl 看单次请求新建连接数(应该稳定为 1,多次复用)
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "connects=%{num_connects}\n" http://127.0.0.1/health
done
# 3. 抓包看 FIN 频率(每请求一条 FIN 就是没命中)
tcpdump -i lo -A 'tcp port 8080 and (tcp[tcpflags] & tcp-fin != 0)'
如果第 2 步 num_connects 每次都变,第 3 步每秒能看到大量 FIN——keepalive 池根本没命中。检查顺序:proxy_http_version 1.1 → proxy_set_header Connection "" → upstream 里 keepalive N → Nginx 版本(必须 1.1.5+,老版本不支持 upstream keepalive)。
6. 5 个真实 502 案例
案例 1:keepalive 池 × worker 数超过后端上限
PHP-FPM 配 pm.max_children=5,Nginx worker_processes=4、keepalive=32。4 × 32 = 128 条潜在连接,远超 PHP-FPM 5 个 worker 的处理能力。表现是 PHP-FPM pm.max_children reached 报警,Nginx error.log 报 upstream timed out。
修复:keepalive = pm.max_children × 2 = 10,或者调高 pm.max_children。
案例 2:Node.js 后端 5s 关掉连接,Nginx 75s 还在用
Node.js 默认 keepAliveTimeout=5s,Nginx 默认 keepalive_timeout=60s。第 6 秒开始,Nginx 池里的连接后端已经关了,但 Nginx 不知道。表现为:低并发时正常,突发流量下 502 概率 5%-15%。
修复:Node.js 侧 server.keepAliveTimeout = 65000(65s),或 Nginx 侧 keepalive_timeout 5(激进但有效)。
案例 3:后端滚动重启后旧连接变死连接
K8s 里 Pod 滚动升级,旧 Pod 收到 SIGTERM 之前还在 ESTAB 状态。Nginx 池里指向旧 Pod 的连接立刻变死。表现为:每次发布前后 30 秒 502 高峰。
修复:upstream 加 max_fails=1 fail_timeout=5s,让 Nginx 快速标记失败;或者用 zone backend 64k 让 upstream 共享内存感知实例变化。
案例 4:keepalive_requests=1000 太低导致连接重建堆积
短接口(平均 5ms)在 keepalive_requests=1000 下,每条连接 5 秒就重建一次。Nginx 每秒要新建几百条连接,DNS 解析堆积(如果 upstream 是域名)。表现为 DNS 查询量暴涨,Nginx worker CPU 卡在 accept 上。
修复:keepalive_requests 10000,或者 upstream 用 IP 不走 DNS。
案例 5:HTTP/2 反代 HTTP/1.1 后端
前端 listen 443 ssl http2,后端是普通 HTTP/1.1。很多人以为 keepalive 自动生效——不会。HTTP/2 前端和 HTTP/1.1 后端的连接模型完全不同,Nginx 会在后端这一侧重新建立连接池。如果 location 里少了 proxy_http_version 1.1,Nginx 会用 HTTP/1.0 打后端,每次都新建连接。
修复:location 块显式加三件套,并检查 nginx -V 是否编译了 --with-http_v2_module。
7. 完整生产模板
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
# 客户端长连接
keepalive_timeout 65;
keepalive_requests 10000;
# 后端连接池
upstream backend {
zone backend_pool 64k;
server 10.0.0.11:8080 max_fails=2 fail_timeout=30s max_conns=256;
server 10.0.0.12:8080 max_fails=2 fail_timeout=30s max_conns=256;
keepalive 32; # 每 worker 池大小
keepalive_timeout 60; # 空闲 60s 回收(< 后端超时)
keepalive_requests 10000; # 单连接复用上限
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # 三件套 1
proxy_set_header Connection ""; # 三件套 2
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 超时参数(比上游 keepalive_timeout 长一些)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_next_upstream error timeout http_502 http_503;
}
}
}
8. 性能对比测试
本地压测脚本,Node.js 后端 4 个进程,Nginx 单节点:
# 安装 wrk
git clone https://github.com/wg/wrk.git && cd wrk && make
# 三档 keepalive 各跑 30 秒
wrk -t4 -c100 -d30s http://127.0.0.1/health
实测结果:
| 配置 | QPS | P99 | 连接新建数/秒 |
|---|---|---|---|
| keepalive 8(默认) | 8500 | 32ms | ~120 |
| keepalive 32 | 15200 | 18ms | ~45 |
| keepalive 64 | 17800 | 16ms | ~30 |
| keepalive 128 | 17400 | 17ms | ~35 |
从 8 到 32 的 QPS 提升是最实在的。连接新建数从 120/s 降到 30/s,后端 accept 队列压力骤减。
9. 踩坑清单速查
- 三件套少一行 → 池不命中,永远新建连接
proxy_http_version忘配 → 走 HTTP/1.0,keepalive 完全无效proxy_set_header Connection ""写成"keep-alive"→ 后端强制关闭,池子白配- keepalive 池 × worker 数 > 后端 max_connections → 后端拒绝连接
- Nginx keepalive_timeout > 后端 keepAliveTimeout → 池里躺着死连接
- upstream 是域名且忘加
resolver→ DNS 缓存过期后无法解析 worker_rlimit_nofile太小 → 连接数上限被打爆,报too many open fileskeepalive_requests=1000对短接口太低 → 连接频繁重建- 后端滚动重启前没 drain → 死连接触发 502 高峰
- 检查完配置不 reload,
nginx -s reload不生效 → 忘了nginx -T验证生效配置
FAQ
Q1:nginx upstream keepalive 的默认值是多少?为什么默认值这么小?
keepalive 默认 8,keepalive_timeout 默认 60 秒,keepalive_requests 默认 1000。keepalive 默认值这么小是因为 upstream keepalive 是 nginx 0.8.17 之后才引入的功能,作者保守起见给了小值,怕占用过多后端资源。生产高并发场景(>500 QPS)一般要调到 32 或 64,配合后端 max_connections 和系统 ulimit -n 一起调。低流量的小站保持默认也够用。
Q2:keepalive_timeout 和 upstream 里的 keepalive 有什么区别?
keepalive_timeout 是后端 TCP 连接空闲多久后被 Nginx 主动关闭,控制连接生命周期;upstream keepalive N 是 Nginx 后端连接池的最大空闲数,控制池容量。前者是"多久不用就扔",后者是"最多能存多少"。两个维度都要调:池子太小命中不上,池子太大占 fd;生命周期太短连接频繁重建,太长容易撞后端超时。
Q3:为什么 keepalive 配了还是每次都新建连接?
最常见原因是 location 块里没配 proxy_http_version 1.1 和 proxy_set_header Connection ""。少了 proxy_http_version 1.1,Nginx 用 HTTP/1.0 打后端,HTTP/1.0 默认不支持长连接,池根本不会命中。少了 proxy_set_header Connection "",客户端的 Connection 头会透传到后端,后端一旦看到 Connection: close 就强制关连接。三件套缺哪行都会失效,用 curl -w '%{num_connects}' 一测就知道。
Q4:upstream keepalive 32 是不是越大越好?
不是。池子太大意味着 Nginx 会占用更多空闲 TCP 连接,可能压过后端 max_connections,或者耗尽文件描述符(ulimit -n)。经验公式:keepalive = 后端最大并发数 / worker_processes / 2。比如后端 max_connections=4000,worker_processes=4,那 keepalive = 4000 / 4 / 2 = 500,但通常配 32-64 已经足够。先调 worker 数和后端上限,再倒推 keepalive 值。生产上超过 128 就收益递减,反而可能因为 fd 紧张导致 accept 排队。
总结
nginx upstream keepalive 看似只是三个数字,实际上是把 Nginx、后端、操作系统内核的连接模型串起来的一根线。核心记住三件事:三件套缺一不可、Nginx 超时严格小于后端超时、池大小按后端上限倒推而不是拍脑袋。
排查 502 时先跑这两条命令:
# 看 pool 里连接状态
ss -tanp | grep :8080 | grep ESTAB | wc -l
# 看 keepalive 是否命中
curl -s -o /dev/null -w "connects=%{num_connects}\n" http://127.0.0.1/health
命中了,性能就起来了;不命中,别急着压测,先回去检查配置。