本文是「Nginx 避坑」系列第一篇,本系列共四篇
后续篇目:反向代理与负载均衡 → HTTPS、安全与跨域 → 进阶与生产实践
线上出了问题,翻了两小时日志,最后发现是 worker_processes 1 这一行。
这些 Nginx 反向代理的坑,我替你踩完了(反向代理与负载均衡)
https://oa22.cn/bbs.asp?id=38197
你的Nginx正在裸奔:配了HTTPS却从没真正安全过
https://oa22.cn/bbs.asp?id=38196
写了几年Nginx配置,这些错误我见过N多次了
https://oa22.cn/bbs.asp?id=38195
同事改了一行Nginx配置,大促当天系统崩—这5个教训,每一个都是真金白银换来的
https://oa22.cn/bbs.asp?id=38194
排查一圈下来,才意识到——不是业务代码的锅,不是数据库的锅,是Nginx配置从上线第一天就是错的。一直没人注意,直到流量上来了才暴露。
这篇不只列坑,更重要的是把成熟团队真正在用的做法一起写出来。每个问题都给出:为什么这么配、生产里怎么落地、哪些细节容易再次踩坑。对照着检查一遍自己的配置,大概率能发现几个隐患。
先说说为什么基础坑最难发现
进阶的坑,比如 WebSocket 代理断连、upstream keepalive 没配——这些一出问题,现象很明显,很快能定位方向。
基础配置的坑不一样。 它们平时完全没有症状,服务运行一切正常,直到某个临界点:流量突然上来、换了服务器、做了迁移——问题才爆出来。那个时候你往往先去查应用层,查数据库,绕了一大圈,才想到可能是Nginx本身。
所以把基础打扎实,比什么都值。
第一块:基础配置里的坑与最佳实践
坑1:worker_processes 写死了数字
这是见得最多的一个。很多配置文件里直接写着:
worker_processes 1;
你的服务器是8核16核,这一行相当于主动放弃了大半算力。CPU跑满,但大部分核都是闲着的。
✅ 最佳实践:用 auto,让Nginx自己决定
worker_processes auto;
auto 读取 /proc/cpuinfo,有几个核启几个worker,完全不用手动维护,换机器了也不需要改配置。
⚠️ 容器环境的额外处理
Docker 用 --cpus=2 限制资源时,auto 读到的是宿主机核心数,可能一下子启几十个worker出来,反而互相竞争。
成熟的做法是在启动脚本里动态计算:
#!/bin/sh
# entrypoint.sh
CORES=$(nproc)
sed -i "s/worker_processes auto/worker_processes $CORES/" /etc/nginx/nginx.conf
exec nginx -g "daemon off;"
用 exec 启动 Nginx 还有一个额外好处:Nginx 直接成为 PID 1,容器收到 SIGTERM 时可以正确优雅退出,不会强制中断在途请求。
坑2:只改了Nginx,忘了改系统限制
高并发时突然出现:
24: Too many open files
翻Nginx配置,worker_connections 已经设到65535了,怎么还会不够?
因为系统层面的限制没动。Nginx的配置和操作系统的 ulimit 是两回事,两边都要改。
✅ 最佳实践:三个地方一起改
第一处,nginx.conf:
worker_rlimit_nofile 65535;
events {
worker_connections 65535;
use epoll; # Linux 下显式指定 epoll,性能最好
multi_accept on; # 一次性接受所有新连接,减少事件循环次数
}
第二处,系统限制(/etc/security/limits.conf):
nginx soft nofile 65535
nginx hard nofile 65535
第三处,如果用 systemd 管 Nginx(推荐):
[Service]
LimitNOFILE=65535
改完记得 systemctl daemon-reload && systemctl restart nginx,limits.conf 的修改需要重启才生效。
容量规划公式备忘:
理论最大并发 = worker_processes × worker_connections
反向代理实际并发 ≈ 上面的结果 ÷ 2
反向代理场景每个客户端连接还要占一个 Nginx→Backend 的连接,文件描述符消耗翻倍,规划时要留出足够余量。
坑3:include 文件顺序没管好
配置写了,reload 也成功了,但就是不生效,或者请求走错了虚拟主机。
根因往往是 default_server 冲突——/etc/nginx/conf.d/ 下多个文件都定义了 default_server,哪个文件名字母顺序靠前,哪个生效,行为完全不可预期。
✅ 最佳实践:用文件名前缀控制加载顺序
/etc/nginx/conf.d/
├── 00-default.conf # 默认 server,永远最先加载
├── 10-api-service.conf
├── 20-static-site.conf
└── 30-admin.conf
把 default_server 单独放 00-default.conf,业务配置从 10- 开始。这个约定简单,多人协作时也不容易出问题。建议把这个命名规则写进团队 Wiki,作为约定俗成的工程规范。
坑4:reload 前从来不检查语法
nginx -s reload 失败后,老 worker 进程继续在跑,新配置没有生效。你以为改好了,其实根本没加载,然后开始排查业务代码……
✅ 最佳实践:检查+reload 写成一条命令
nginx -t && nginx -s reload
&& 保证语法错误时直接终止,不执行 reload。
更进一步,成熟团队会把 Nginx 配置纳入 Git 管理,每次变更走 PR + 自动化检查流水线:
# CI 流水线里的检查步骤示例
docker run --rm -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf nginx nginx -t
任何人提交的配置变更,在合并之前就能发现语法错误,不再依赖个人操作习惯。
另外,想看当前 Nginx 实际加载了哪些配置(包含所有 include 文件),用:
nginx -T # 大写 T,输出完整配置,排查"改了但没生效"时非常好用
坑5:pid 文件权限
用非 root 用户启动 Nginx,经常遇到:
nginx: [error] open() "/var/run/nginx.pid" failed (13: Permission denied)
✅ 最佳实践:用 systemd 管理,不手动指定 pid 路径
交给 systemd 处理 pid 文件,不要在 nginx.conf 里自己指定,省去一类权限问题:
systemctl start nginx
systemctl enable nginx # 设置开机自启
systemctl status nginx # 查看运行状态
如果实在需要非 root 启动且不用 systemd,临时方案是改到有写权限的路径:
pid /var/run/nginx/nginx.pid; # 提前创建目录并授权给 nginx 用户
第二块:性能调优,几个被误解的配置
误区1:gzip 对所有类型都开
jpg、png、gif 本身就是压缩格式,再压一遍几乎没效果,只是白白消耗 CPU。
另一个高频错误是压缩级别设太高:
gzip_comp_level 9; # 不推荐
级别 9 和级别 6 的压缩率差距不超过5%,但 CPU 消耗差距很大。9 是最高压缩率,不是最佳实践。
✅ 最佳实践:只压文本,级别定6
gzip on;
gzip_vary on; # 告知代理服务器分别缓存压缩和非压缩版本
gzip_comp_level 6; # 压缩率与CPU消耗的平衡点,行业公认
gzip_min_length 1024; # 1KB以下不压,压缩开销大于传输收益
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml+rss
image/svg+xml;
# ⛔ 绝对不要加:image/jpeg image/png image/gif video/*
gzip_vary on 很多人漏掉这一行。它的作用是在响应头里加 Vary: Accept-Encoding,告诉 CDN 和代理分别缓存压缩版和非压缩版,防止把压缩内容发给不支持 gzip 的客户端。
误区2:sendfile / tcp_nopush / tcp_nodelay 三选一
这三个参数看着像是互相矛盾:
sendfile on; # 零拷贝传文件
tcp_nopush on; # 等数据凑够了再发
tcp_nodelay on; # 立即发,禁用 Nagle 算法
tcp_nopush(等着发)和 tcp_nodelay(立即发)看起来逻辑相反,但实际上不冲突:
tcp_nopush 只在 sendfile 传文件阶段生效,把文件数据积累成大包再发,减少 TCP 包数量tcp_nodelay 在 keepalive 连接复用阶段生效,后续请求立即发送,降低延迟
✅ 最佳实践:三个一起开
sendfile on;
tcp_nopush on;
tcp_nodelay on;
Nginx 在不同处理阶段会自动切换策略,三个同时开是所有主流 Nginx 配置模板的标准写法。
误区3:keepalive_timeout 没细想
太长:大量空闲连接占着文件描述符,流量高峰容易打满。太短:客户端频繁握手,延迟和 CPU 消耗都上去了。
✅ 最佳实践:
keepalive_timeout 65; # 65秒,适合大多数 Web 服务
keepalive_requests 1000; # 单个连接最多复用1000次再断开重建
keepalive_requests 这个参数很多人不知道,Nginx 老版本默认值只有 100,单个连接处理100个请求就关闭重建,高并发下会产生额外的握手开销。改成1000是合理的上限,同时也防止连接因为长期复用而累积内存不释放。
误区4:proxy_buffer 完全没配
后端响应慢时,没有 buffer 的 Nginx 会直接透传数据,Nginx 和后端之间的连接就一直挂着不释放,并发数很快跑满。
✅ 最佳实践:按场景分级配置
通用场景(API服务、普通页面):
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
proxy_temp_file_write_size 64k;
大文件下载场景(报表导出、文件服务):
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 16 64k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 256k;
两套配置分别放在不同的 location 块里,按业务类型精细化控制,不要对所有接口一刀切。
误区5:open_file_cache 一行都没配
每次静态文件请求,Nginx 都要做 open() 和 stat() 系统调用。几百 QPS 时感觉不出来,上了几千 QPS 这就是可量化的瓶颈。
✅ 最佳实践:四行配置,零成本提升
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on; # 连"文件不存在"的结果也缓存,减少无效 stat
open_file_cache_errors on 这一行经常被漏掉。不缓存 404 的话,每次访问不存在的资源都要做系统调用,爬虫或者恶意请求很容易在这里打出瓶颈。
第三块:location 匹配,最容易自我怀疑的地方
优先级搞错了
Nginx location 匹配优先级,从高到低:
=^~~- 无修饰符的普通前缀匹配(最低)
最经典的翻车场景:
# ❌ 陷阱配置
location /static/ {
root /var/www;
}
location ~* \.(jpg|png|gif)$ {
expires 30d;
root /var/www;
}
GET /static/logo.png 会走哪个?正则那个。普通前缀匹配没有 ^~,挡不住正则继续尝试。
✅ 最佳实践:静态资源目录一律加 ^~,顺手把缓存策略也加上
location ^~ /static/ {
root /var/www;
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~* \.(jpg|png|gif|ico|woff2)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
^~ 加上之后,/static/ 下的请求不会再被正则抢走。同时把缓存策略一并配好,是静态资源 location 的完整最佳实践。
try_files 有两个高频坑
SPA 应用几乎人人都写这行:
try_files $uri $uri/ /index.html;
坑一:最后一个参数写 /index.html,Nginx 会做内部重定向,重新走一遍 location 匹配。如果匹配到的 location 本身还有 try_files,就会循环。
✅ 正确做法:用命名 location 完全隔离 fallback 逻辑:
location / {
try_files $uri $uri/ @spa_fallback;
}
location @spa_fallback {
root /var/www/app;
try_files /index.html =404;
}
坑二:try_files 最后一个参数不能是完整 URL:
# ❌ 错的
location / {
try_files $uri $uri/ http://127.0.0.1:8080;
}
✅ 正确写法:配合命名 location 做代理
location / {
try_files $uri @backend;
}
location @backend {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
if 指令——能不用就不用
Nginx 官方有篇文章叫 "If is Evil",这不是夸张。
location 块里用 if + proxy_pass,if 块的指令会覆盖外层的,但继承关系又不完整,产生极难排查的 bug:
# ❌ 危险写法,行为不可预测
location / {
if ($request_method = POST) {
proxy_pass http://backend_post;
}
proxy_pass http://backend_get;
}
✅ 最佳实践:用 map 替代所有条件分支
# 在 http 块里定义
map $request_method $backend {
POST http://backend_post;
default http://backend_get;
}
location / {
proxy_pass $backend;
}
map 在请求进来之前就计算好变量值,没有 if 的继承问题,逻辑清晰,性能也更好。
if 的唯一安全使用场景:只配合 return 或 rewrite 做跳转:
# ✅ 这样用是安全的
if ($host != "www.example.com") {
return 301 https://www.example.com$request_uri;
}
alias 末尾斜杠别漏
# root:location 路径追加在 root 后面
location /static/ {
root /var/www;
# /static/a.js → /var/www/static/a.js ✅
}
# alias:替换掉 location 匹配的部分
location /static/ {
alias /var/www/assets/; # ← 末尾斜杠不能少,和 location 末尾对称
# /static/a.js → /var/www/assets/a.js ✅
}
alias 末尾少一个斜杠,路径会拼成 /var/www/assetsa.js,4XX 报错,然后你开始怀疑自己文件放错目录了……
记忆规律:location 末尾有斜杠,alias 末尾也必须有斜杠,对称就对了。
最后两件事
一、一份可以直接用的生产基础配置
把本篇所有最佳实践整合到一起,注释说明每个关键配置的用意:
user nginx;
worker_processes auto; # 自动匹配 CPU 核心数
worker_rlimit_nofile 65535; # 与系统 ulimit 保持一致
error_log /var/log/nginx/error.log warn; # 生产用 warn,不要用 debug
pid /var/run/nginx.pid;
events {
worker_connections 65535;
use epoll; # Linux 最优事件模型
multi_accept on; # 一次性接受所有新连接
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 带响应时间的日志格式,排查慢请求时救命
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" rt=$request_time '
'urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
# 请求大小与超时,防慢速攻击
client_max_body_size 10m;
client_body_buffer_size 128k;
client_header_buffer_size 4k;
large_client_header_buffers 4 32k;
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
# gzip:只压文本,级别定6
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css application/json
application/javascript text/xml
application/xml image/svg+xml;
# 静态文件描述符缓存,高 QPS 场景效果明显
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
server_tokens off; # 隐藏版本号,安全加固基础项
include /etc/nginx/conf.d/*.conf;
}
二、上线前自查清单
新项目上线前或者旧系统排查时,对照这份清单逐项确认:
基础配置
□ worker_processes 是否用了 auto(容器环境需额外处理)
□ worker_rlimit_nofile 与 worker_connections 是否一致
□ 系统 ulimit / systemd LimitNOFILE 是否同步修改
写了几年Nginx配置,这些错误我见过n多次了
□ conf.d 文件命名是否有前缀控制加载顺序
性能配置
□ gzip 是否排除了图片/视频类型
□ gzip_comp_level 是否是 6,gzip_vary 是否开启
□ sendfile / tcp_nopush / tcp_nodelay 三个是否都开启
□ keepalive_requests 是否显式配置
□ proxy_buffer 是否按业务场景分级配置
□ open_file_cache 及 open_file_cache_errors 是否开启
location 配置
□ 静态资源目录是否有 ^~ 修饰符
□ 是否有在 location 里用 if + proxy_pass 的危险写法
□ try_files 最后一个参数是否是路径/命名location(不是完整URL)
□ alias 末尾斜杠是否和 location 末尾对称
基础篇到这里。下一篇聊反向代理和负载均衡——那里有一个把无数人坑过的斜杠问题,以及 upstream keepalive 没配导致大促雪崩的真实事故复盘。
如果这篇对你有帮助,转发给还在踩这些坑的同事吧。
阅读原文:原文链接
该文章在 2026/3/27 16:37:23 编辑过