上周有个客户把这台机器丢给我,说「你们代码没问题,我们只是压测压到 3000 并发,Nginx 就开始大面积 502」。配置我看了一遍:2 核 4G,Ubuntu 22.04,Nginx 前面挂 upstream 到三个后端节点。应用本身健康检查全绿,Go 写的后端 P99 才 8 毫秒,主机 CPU 常年跑在 15% 上下。按理说 3000 并发对这台机器不算什么。
但我打开 error.log 的时候,第一眼看到的不是上游超时,而是满屏的:
open() "/var/www/html/proxy_temp/..." failed (24: Too many open files)
再看 ss -s:Sockets: 3128 total, 2980 established。当时我才反应过来,问题跟后端一点关系都没有,是 Nginx 自己先把自己掐死了它能打开的文件描述符(fd)不够用了。
如果你的站点也在做压测或者遇到过高峰期 502,先把这两家的机器规格和线路看一遍再动手调参数:lightlayer(独立物理服务器,高规格带宽,适合高流量高并发场景)和vollcloud(香港 CMIN2 大带宽线路,吞吐量高、全国低延迟)。机器本身的带宽和连接数要留够余量,否则参数调得再对也顶不住峰值那一波。

新手排查 502 的时候,习惯性地先去查后端。但 502 和 504 的产生位置其实完全不同:
502 Bad Gateway是 Nginx 主动返回的。它已经 accept 了客户端的连接,但往上游转发的时候失败了上游连不上、连接被拒、上游提前关闭了连接,或者 Nginx 在处理这个请求的过程中自己出了资源错误(fd 耗尽就是其中一种)。
504 Gateway Timeout是 Nginx 等上游等太久了,默认 60 秒没等到响应就断开。这种才更接近「后端慢」。
所以我一般先看 error.log 里的具体错误,而不是看状态码。如果日志里是 upstream timed out 或者 connect() failed (111: Connection refused),那才该去查后端;如果是 too many open files 或者 worker_connections are not enough,那就是 Nginx 自己的锅,跟后端一点关系没有。
还有一个快速判断的办法,直接看 Nginx 的 stub_status 里的这三个数:Active connections、Accepted connections 和 Handled connections。如果 Accepted 明显大于 Handled,说明内核在监听队列就把连接丢掉了,压根没轮到 Nginx 处理。
这是这次故障的核心,也是最容易被忽略的一层。很多人以为 Nginx 的并发上限就是 worker_connections,其实远不止。三层限制是串联的,任何一层最小,整个并发能力就被它卡死。
第一层:进程能打开的 fd 总数(ulimit -n)。
这是操作系统的硬限制。Linux 上每个进程能持有的文件描述符数量由 RLIMIT_NOFILE 决定。查看当前值:
ulimit -n
我那台机器上跑 Nginx 的用户查出来是 1024。是不是很熟悉?很多云镜像的默认 systemd LimitNOFILE 就是 1024,Debian 系老版本更是给到 256。而 worker_connections 默认是 768,两者一比,谁小听谁的。
更麻烦的是,你在 shell 里 ulimit -n 65535 只对当前 shell 及其子进程生效。systemd 拉起来的服务不继承你 ssh 里的设置,得单独配。
第二层:Nginx worker 的连接数(worker_connections)。
默认 512 或 1024,官方文档写的是 1024。它限制的是单个 worker 进程能同时处理多少连接,不是整个 Nginx 实例。
这里有个非常关键、也非常容易被算错的点:一个客户端连接,在 Nginx 看来要占两个连接槽位。一个是客户端侧的 socket,另一个是 Nginx 连到上游后端的那个 socket哪怕是走 upstream keepalive 复用的连接,槽位也算在对应 worker 头上。所以如果你配了 upstream keepalive 连接池,实际占用会比你想的更复杂:一个 worker 要维护 N 条到上游的 keepalive 连接,这 N 条连接本身就占了 N 个槽位。
第三层:内核的监听队列(somaxconn / backlog)。
这个参数控制的是「已完成三次握手、但还没被应用 accept 走」的连接能排队多少。默认值在现代内核上是 4096,但如果 /proc/sys/net/core/somaxconn 被调小了,或者 Nginx 的 listen ... backlog 写小了,突发流量会在内核层直接被丢弃,客户端表现就是连接被重置。

拿那台机器的配置算一遍账。假设 Nginx 有 worker_processes 4(2 核机器,Nginx 默认就是核数),worker_connections 1024(默认),ulimit 是 1024。
单个 worker 的连接槽位是 1024,但这个 worker 自己的 fd 上限只有 1024,而每个请求要占两个 fd(客户端 + 上游),所以它真正能同时处理的请求数是 512。四个 worker 加起来是 2048。这个数字看着比 3000 小一点,还没到瓶颈。
但问题在于:不是所有请求均匀落在四个 worker 上的。Nginx 的负载均衡是按连接分配的,长连接多的那个 worker 会先撞墙。更要命的是,那个 worker 一旦撞上 fd 上限,它 accept 队列里的新连接会被内核丢弃,Accepted 和 Handled 的差值会突然拉大。
我后来在客户机器上实测确认了这一点:把 worker_connections 从 1024 调到 65535 之后,错误日志里的 too many open files 依旧照旧,因为 ulimit -n 还是 1024,Nginx 依然开不出那么多 socket。这就是最典型的「参数改了没效果」改错了层。
顺便说个排查小技巧:改完之后别急着上线,先确认限制真的生效了:
cat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep -i 'open files'
这行输出里的 Max open files 才是 Nginx 进程实际拿到的 fd 上限,比你 ssh 里的 ulimit 值更可信。另外 /proc/$(pgrep nginx | head -1)/fd | wc -l 能看当前实际打开的 fd 数量,这个数逼近上限时就该动手了。
第一步,systemd 层面把 fd 上限抬起来。如果用 systemd 托管 Nginx:
[Service]
LimitNOFILE=1048576
LimitNPROC=infinity
改完 systemctl daemon-reload && systemctl restart nginx 生效。注意 LimitNOFILE 的实际值受内核 fs.file-max 和 fs.nr_open 约束,这两个也得够大:
sysctl -w fs.file-max=2097152
sysctl -w fs.nr_open=2097152
老一点的发行版还有 PAM 的 /etc/security/limits.conf 和 /etc/security/limits.d/,那套是给 login shell 用的,systemd 服务不读它这点也是很多人改了 limits.conf 却一点变化都没有的原因。
第二步,Nginx 配置里把三个数对齐。
worker_processes auto;
worker_rlimit_nofile 1048576;
events {
worker_connections 65535;
multi_accept on;
}
http {
upstream backend {
server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
第三步,内核监听队列跟上。
sysctl -w net.core.somaxconn=8192
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
这三个数要一起看。Nginx 文档里明确说过,worker_connections 的实际可用值不会超过系统 fd 上限,而系统 fd 上限在较老的内核里又会被 RLIMIT_NOFILE 压住。所以只改 nginx.conf 里的 worker_connections,在很多机器上完全是无效功。
改完 nginx -t 验证语法,再 reload。reload 之后可以立刻验证:
nginx -T | grep -E 'worker_connections|worker_rlimit_nofile'
确认 Nginx 真正读到的配置是你写的那份,而不是某个 include 里的旧值。

顺手说下这次配置里另一个隐患。客户原来的 upstream 里写的是 keepalive 64,但 proxy_http_version 还是默认的 1.0。HTTP/1.0 不支持 keepalive,upstream 的连接池等于白配,每次请求都要重新建连,建连速度跟不上并发,就会在 fd 上疯狂堆积。
要用 keepalive,proxy_http_version 必须改成 1.1,并且显式传 proxy_set_header Connection "";(清空客户端的 Connection 头),否则 Nginx 会把客户端的 Connection: close 传给上游,连接照样被断开。
keepalive 数量也不是越大越好。upstream keepalive 64 乘以上游节点数乘以 worker 数,就是每个 worker 要额外维护的连接数,这些都要占 fd 槽位。我一般按「峰值并发 ÷ worker 数」来定,留一半余量就够。设太大反而会因为 fd 提前耗尽而崩。
这次修完之后,我给所有托管的机器都加了三个监控项,成本几乎为零:
1. Nginx worker 的 fd 使用率。用 /proc/<pid>/fd 的数量除以 /proc/<pid>/limits 里的上限,超过 70% 告警。
2. stub_status 的 Active / Handled 差值。这个比 CPU、内存都更早发现问题,因为连接数是「瞬间爆掉」的类型。
3. Accepted 与 Handled 的比值。这个比值持续大于 1,说明有连接在内核层被丢了。
如果用 Prometheus,nginx_upstream_conn_active 和进程级别的 process_open_fds 配上 alertmanager 就行。关键是阈值要设在上限的 70% 而不是 90%fd 耗尽是不可逆的瞬时事件,等到 90% 可能已经来不及了。
另外说一句更根本的:这次问题本质上是用一台 2 核 4G 的机器扛了本该由更多机器扛的并发。参数调对之后确实能撑住 3000,但 CPU 开始往上走了,worker 之间抢 CPU 反而让 P99 从 8 毫秒涨到 60 毫秒。参数修复是止损,扩容才是根治。

整件事复盘下来,我把它记成一句话:502 不一定是后端的问题,先看 error.log 里是不是 too many open files。而 fd 上限要改的话,三处必须一起改systemd 的 LimitNOFILE、Nginx 的 worker_connections 和 worker_rlimit_nofile、以及内核的 somaxconn,只动其中一个基本等于没动。
最后再强调一次顺序:先 ulimit -n,再 worker_connections,最后才考虑加机器。这台客户机器改完之后,同样的 3000 并发压测跑了四十分钟,Active connections 峰值 2870,fd 使用率停在 62%,error.log 里再没有出现过一条 too many open files。