Skip to content

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 files
  • keepalive_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

命中了,性能就起来了;不命中,别急着压测,先回去检查配置。