文章总结: 本文基于作者在日均PV近1.5亿的电商前端集群中处理Nginx502故障的实战经验,系统总结了Nginx高并发优化的核心知识点与排查步骤。文章按现象、判断、命令、配置、验证、回滚的闭环结构,详细阐述了worker进程与CPU亲和性、文件描述符上限、连接参数、反向代理缓冲、缓存及内核参数等关键配置的调优方法。提供了可执行的命令和配置片段,强调先观测再调整的工程化思路,旨在帮助运维人员避免常见陷阱并提升系统稳定性。 综合评分: 88 文章分类: 其他
Nginx 高并发优化,我踩过的坑都在这里
点击关注 👉 点击关注 👉
马哥Linux运维
2026年7月1日 18:31 广东
在小说阅读器读本章
去阅读
Nginx 高并发优化,我踩过的坑都在这里
一、问题背景
早些年我接手过一个日均 PV 接近 1.5 亿的电商前端集群,Nginx 跑在最前面做七层反向代理和静态资源服务。一次大促 0 点压测,整个集群的 Nginx 在第 11 分钟开始吐 502,后端 Java 应用集群其实没挂,前端的 Nginx 自己先被拖垮。事后复盘,那一晚上踩过的坑基本涵盖了 Nginx 高并发优化里几乎所有的”经典”问题:worker 数等于 CPU 数却仍然打满、accept4() failed (24: Too many open files) 反复刷屏、TIME_WAIT 堆到几十万、keepalive_requests 设得过小导致上游连接雪崩、proxy_buffer 配错让大响应整个写回客户端造成 worker 阻塞,还有 gzip_types 漏配把 JPEG 反复压缩浪费 CPU,等等。
现在回头看,这些坑并不”高级”,但它们真实地、反复地出现在生产环境里。本文按”现象 → 判断 → 命令 → 配置 → 验证 → 回滚”的闭环来写,记录我在线上真实跑通过的命令、可执行的配置片段、踩坑后留下的指标观察点。文中的所有阈值、参数、文件路径,都是我在至少 5 套生产集群里反复调过的版本;版本不同、发行版不同、CPU 架构不同,落到具体数字会略有差异,本文以指令级行为和判断逻辑为准,不把任何单一数值当作”绝对标准”。
二、适用场景
本文适合以下读者和场景:
- 在生产环境维护一个或多个 Nginx 实例,承载 Web 入口、动静分离、反向代理、API 网关之一的同学。
- 准备做一次 Nginx 高并发改造,希望有一份”哪些参数确实有用、哪些是心理安慰、哪些踩过坑会反噬”的工程化参考。
- 正在排查 502/504/499、TIME_WAIT 异常上升、worker 频繁重启、
Too many open files、CPU 单核打满但总 CPU 余量很大的同学。 - 接手了”前任留下的 nginx.conf”想搞清楚每一条配置到底是干嘛的同学。
不适合的读者:
- 想看 Nginx 新手入门、
location匹配规则、root和alias区别的同学,本文默认你已经知道这些。 - 想看”Ingress/HAP/Lua/动态配置”等深度主题的同学,本文聚焦在原生 Nginx + openresty 之上的核心高并发优化,不深入到运行时扩展。
三、核心知识点
Nginx 高并发优化是一个系统问题,不是单条指令调优。下面是按层次划分的核心知识点清单,本节先把图谱立起来,后面章节逐一展开。
3.1 进程与线程模型
-
Nginx 主进程 + 工作进程的 fork 关系,主进程负责加载配置、管理子进程、监听信号;工作进程负责实际处理请求。
-
一个工作进程在 Linux 下可以同时处理成千上万个连接(事件驱动 + epoll),并不是一个连接一个线程。
-
worker_processes设多大、
worker_cpu_affinity如何绑核,是优化的第一步,也是最容易被”心理调优”带歪的一步。
3.2 事件驱动核心参数
-
use epoll:Linux 下的事件多路复用模型,5.x 之后基本不必再显式写。
-
multi_accept:worker 一次性 accept 多个连接的行为差异,会改变上游连接排队模型。
-
accept_mutex:历史参数,现代 Linux 单队列 listen 已基本无需关心,但要避免和 worker 模型冲突。
3.3 连接相关参数
-
worker_connections:每个 worker 同时能维护的最大连接数。
-
keepalive_timeout、
keepalive_requests、keepalive长连接池(upstream)。 -
reset_timedout_connection、
lingering_timeout、lingering_close。 -
tcp_nodelay、
tcp_nopush在sendfile开启后的搭配行为。
3.4 文件描述符与进程上限
-
worker_rlimit_nofile:单个 worker 能打开的文件描述符上限。
-
系统层
fs.file-max、nofile、/proc/sys/fs/file-nr。 -
Too many open files的常见来源:是 worker 本身、还是共享池被吃光。
3.5 反向代理与上游
-
proxy_pass、
proxy_set_header、proxy_buffer_size、proxy_buffers、proxy_busy_buffers_size、proxy_temp_path。 -
proxy_connect_timeout、
proxy_send_timeout、proxy_read_timeout真实的语义。 -
upstream的负载均衡策略(
round-robin、least_conn、ip_hash、random)、weight、backup、slow_start、max_fails、fail_timeout、keepalive。
3.6 缓存
-
proxy_cache与
fastcgi_cache,命中率、缓存击穿、缓存雪崩的实战配置。 -
open_file_cache、
open_file_cache_min_uses、open_file_cache_valid:静态资源高频打开优化的关键。
3.7 压缩与传输
-
gzip、
gzip_types、gzip_min_length、gzip_comp_level、gzip_static、gzip_proxied。 -
与
brotli模块的搭配。
3.8 内核协同
-
net.core.somaxconn、
net.ipv4.tcp_tw_reuse、net.ipv4.tcp_fin_timeout、net.ipv4.tcp_max_tw_buckets、net.ipv4.ip_local_port_range、fs.file-max。 -
ulimit -n和
nofile的关系,以及 systemd 下LimitNOFILE的覆盖关系。
3.9 监控与可观测
-
stub_status模块、
vts模块、ngx_http_log_module自定义日志格式。 -
ss -s/
ss -tnp state/lsof//proc/<pid>/status现场排查。 -
日志里的关键异常关键字:
accept4() failed、too many open files、worker process ... exited、socket() failed、502/504/499。
四、整体排查或实施思路
Nginx 高并发优化的整体思路是”先观测、再定位、再调整、再回归”。我把它总结成一个我自己反复用的 7 步法:
-
先画出现状
:硬件(CPU 核数、内存型号、网卡是 10G 还是 25G)、系统(
uname -a、cat /etc/os-release、sysctl net.* fs.*)、Nginx 版本(nginx -V)、关键配置(nginx.conf、conf.d/、sites-enabled/)现状如何。 -
跑一遍基线压测
:
wrk、ab、siege、vegeta、tsung任选一个有真实业务复现能力的,用wrk -t8 -c200 -d30s起一个能在 30 秒内稳定复现的基准。 -
看指标、看日志
:CPU 单核跑满是 worker 太少还是事件循环扛不住;连接数打满到
worker_connections是 worker 太少还是连接被占用;Too many open files是 fd 不够还是 fd 泄漏;TIME_WAIT 飙高是短连接太多还是tcp_tw_recycle误用。 -
做出一个最小改动集合
:每条改动都要能解释”这条改哪个指标、有什么副作用、怎么回滚”,不允许”Paste 一份网上抄过来的 nginx.conf”。
-
先在一台灰度实例上验证
:用同一份压测脚本对比 QPS、P99 延迟、错误率、CPU、内存。
-
观察业务指标
:错误率是否有抖动、夜间告警是否被新参数触发、上游应用的可观测指标是否被影响。
-
记录、回滚预案常态化
:所有变更留在版本管理里,回滚脚本沉淀出来。
下面章节是按这个 7 步法把每个知识点展开的实战记录。
五、实战步骤
以下是我按照时间顺序梳理出来的”踩坑-排查-修复-验证-回滚”实战步骤。每一条都对应一个真实的坑和对应的命令、配置、判断逻辑。
5.1 踩坑一:worker_processes auto 跑在 N 核机器上,CPU 单核跑满
现象:CPU 总利用率 70%,但某 worker 始终跑满到接近 100%,其余 worker 闲置。QPS 与单核相当,没有真正利用多核。
判断:Nginx worker 数小于等于 CPU 物理核数。
命令:
bash
# 查看 CPU 物理核数和逻辑核数
lscpu | grep -E '^CPU\(s\)|Socket|Core|Thread'
# 查看当前 worker 数(主进程 fork 出来的)
ps -o pid,comm,pcpu -p $(pgrep -f "nginx: worker process")
预期输出会看到 nginx: worker process 的数量,理论上约等于 worker_processes 指令设置的数量。lscpu 让你确认 Socket(s)、Core(s) per socket、Thread(s) per core 三个数乘积,得到总物理核数和逻辑核数。
修复:
nginx
# /etc/nginx/nginx.conf 的 main 块
worker_processes auto; # 自动取 CPU 核数
# 或者更精细
worker_processes 16; # 物理核数 16
worker_cpu_affinity auto; # 自动绑定亲和性
风险提醒:
worker_processes auto在某些老版本(早于 1.9)下不支持;要确认nginx -V中--with-/模块列表里是否包含需要的能力,配置完一定先nginx -t验证语法,再nginx -s reload。
验证:
bash
# 重新加载
nginx -t && nginx -s reload
# 压测前后对比
wrk -t8 -c200 -d30s https://your.host/api/health
# 看 CPU 分布
top -bn1 -p $(pgrep -d, -f "nginx: worker")
预期效果:压测过程中,多个 worker 共同吃掉 CPU,单个 worker 不再打满,整体 QPS 提升。
回滚:保留旧的 nginx.conf,配置回旧值后 nginx -s reload 即生效。
5.2 踩坑二:worker_cpu_affinity 错位导致 NUMA 跨节点
现象:CPU 用得很满,但内存访问延迟高,应用层逻辑 QPS 不及预期。
判断:worker 跑到另一个 NUMA 节点跨 socket 跨内存。
命令:
bash
# 查看 NUMA 拓扑
lscpu | grep -E 'NUMA|Socket|Core'
numastat -m | head
修复:根据 CPU 物理核数和 NUMA 节点数,显式绑核:
nginx
worker_processes 16;
worker_cpu_affinity0001001001001000000100100100100000010010010010000001001001001000;
或者用 auto(Nginx 1.9+)让自动绑核。不要在老机器/虚机上花太多时间调亲和性,效果有限。
验证:taskset -cp <pid> 查看 worker 的 CPU 亲和集是否符合预期。
5.3 踩坑三:accept4() failed (24: Too many open files) 刷屏
现象:错误日志里反复出现 nginx: [emerg] accept4() failed (24: Too many open files),新连接无法被 accept。
判断:进程级 fd 上限或系统级 fd 上限不足。
命令:
bash
# 看 worker 进程打开了多少 fd
lsof -p <pid> | wc -l
# 看 worker 进程自己的 nofile 限制
cat /proc/<pid>/limits | grep "open files"
# 看系统级
cat /proc/sys/fs/file-nr
sysctl fs.file-max
# 看 systemd 服务配置
systemctl show nginx | grep -i LimitNOFILE
fs.file-nr 三列含义:已分配未使用 / 已分配且使用 / 系统上限。第三列接近或低于整机 fd 数时,系统层已经吃紧。
修复(双层):
nginx
# /etc/nginx/nginx.conf
worker_rlimit_nofile 65535;
worker_connections 16384;
bash
# /etc/security/limits.conf 或 /etc/security/limits.d/99-nginx.conf
nginx soft nofile 65535
nginx hard nofile 65535
root soft nofile 65535
root hard nofile 65535
bash
# /etc/sysctl.d/99-fs.conf
fs.file-max = 2097152
风险提醒:
worker_rlimit_nofile必须 ≥
worker_connections * 2,因为每个连接上下游各占一个 fd。
worker_connections的”4 倍法则”是社区经验值,真实可用值取决于
worker_rlimit_nofile与反向代理缓冲。系统级
fs.file-max调大不代表能使用,要受 PAMlimits.conf限制。systemd 启动的 Nginx 还会再被
LimitNOFILE=覆盖一次,建议在 service unit 里显式设置。
验证:
bash
# 启动后立即检查
cat /proc/$(pgrep -f "nginx: master")/limits | grep "open files"
# 再次压测
wrk -t8 -c200 -d30s https://your.host/api/health
# 错误日志要保持干净
tail -n 100 /var/log/nginx/error.log | grep -i "too many open files"
预期:错误日志中不再出现 Too many open files。
回滚:恢复旧的 nginx.conf,将限制文件、sysctl 文件回退即可。
5.4 踩坑四:TIME_WAIT 堆积到几十万
现象:QPS 抖动、connect() 变慢、ss -s 显示 TIME_WAIT 数远超其它状态。
判断:上游短连接过多,没有 keepalive 长连接池,或者上游服务重启导致重连雪崩。
命令:
bash
# 看各状态连接数
ss -s
# 看 80 端口 TIME_WAIT 比例
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn | head
# 看本机到 upstream 的 TIME_WAIT
ss -tn | grep <upstream_ip> | awk '{print $1}' | sort | uniq -c | sort -rn
修复:在 upstream 中开启 keepalive,并协同内核参数。
nginx
upstream backend {
server10.0.0.11:8080 weight=5 max_fails=3 fail_timeout=10s;
server10.0.0.12:8080 weight=5 max_fails=3 fail_timeout=10s;
keepalive32; # 长连接池中保留 32 个空闲连接
keepalive_timeout60s; # 长连接空闲超时
keepalive_requests1000; # 单条长连接最多跑 1000 个请求后回收
least_conn; # 或者 hash $cookie_user; 一致性 hash
}
server {
location /api/ {
proxy_pass http://backend;
proxy_http_version1.1; # 必须打开 keepalive
proxy_set_header Connection ""; # 清除客户端的 Connection: close
proxy_next_upstreamerror timeout http_502 http_503 http_504;
proxy_next_upstream_tries2;
}
}
风险提醒:
keepalive一定要和
proxy_http_version 1.1配合,且proxy_set_header Connection ""清掉客户端的close头。不要把
keepalive调过大,它会让连接池无法在节点异常时快速剔除,反而把”挂掉的连接”交给业务。
keepalive_requests调到 1000 是一个常见经验值;某些上游网关(Tomcat 默认实现)发现连接 1000 次后会主动断开,所以不能盲目追求大值。
内核参数(按需要调整):
bash
# /etc/sysctl.d/99-network.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_max_tw_buckets = 200000
再次强调风险:
tcp_tw_reuse是新连接分配端口时复用 TIME_WAIT 的端口,不是消除 TIME_WAIT。tcp_tw_recycle在 NAT 环境下会造成连接被丢弃,生产不要打开。tcp_max_tw_buckets过大会让系统容忍更多 TIME_WAIT,要同时观察ss -s,避免内存堆积。
验证:
bash
# 持续观察
watch -n 5 'ss -s; date'
# 压测过程中观察连接分布
ss -tn | grep :80 | awk '{print $1}' | sort | uniq -c | sort -rn
回滚:把 upstream 中的 keepalive 改回不开启,重新 nginx -s reload 即可。
5.5 踩坑五:CLOSE_WAIT 上升 = 服务端没关连接
现象:ss -tan | grep CLOSE_WAIT 数量单调上涨,客户端报 504。
判断:服务端程序没有正确关闭连接。Nginx 这层通常没问题,问题多在 upstream(Java Tomcat 默认、Nginx upstream 子连接等)的 keepalive 实现。
命令:
bash
# 看哪一侧 CLOSE_WAIT 多
ss -tn | grep CLOSE_WAIT | awk '{print $NF}' | awk -F: '{print $1}' | sort | uniq -c | sort -rn
修复:先抓应用端的堆栈和连接泄漏,再考虑调小 keepalive_requests 强制回收:
nginx
upstream backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 16;
keepalive_requests 100; # 关键:让单连接最多跑 100 次,强制回收
keepalive_timeout 30s;
}
风险提醒:
keepalive_requests调得过小会让上游连接频繁重建,反而增加 TIME_WAIT。100~1000 都是常见范围,要看上游服务的实现。
5.6 踩坑六:proxy_buffer 配错,worker 阻塞
现象:返回体较大(> 1MB)的接口响应慢,worker 进程常卡住;/var/log/nginx/error.log 报 upstream sent too big header 或者 *<N> upstream response is buffered to a temporary file。
判断:
- 后端响应没有 fit 进 proxy buffer,全部写到磁盘临时文件
proxy_temp_path。 - worker 在等待磁盘 IO,单机并发能力雪崩。
命令:
bash
# 看 nginx 自己吐的临时文件目录大小
du -sh /var/cache/nginx/proxy_temp 2>/dev/null
ls -lh /var/cache/nginx/proxy_temp/ | head
# 看 error log
grep "buffered to a temporary file" /var/log/nginx/error.log | tail -n 20
修复:
nginx
proxy_buffering on;
proxy_buffer_size 16k; # 容纳 response header
proxy_buffers 8 32k; # 8 个 32k buffer 容纳 body
proxy_busy_buffers_size 64k; # 写回客户端时的最大 buffer
proxy_temp_file_write_size 64k; # 写到磁盘临时文件时单次大小
proxy_max_temp_file_size 1024m; # 单个临时文件最大
proxy_temp_path /var/cache/nginx/proxy_temp 1 2; # 1 级目录 1、2 级目录 2
计算公式(重要):
proxy_buffer_size≥ 上游响应头最大值(一般 4k~16k,少数应用会大到 32k)
proxy_buffers总内存 ≈
buffers_num × buffers_size≈8 × 32k = 256k / worker
proxy_busy_buffers_size≥
proxy_buffer_size + proxy_buffers单 worker 占用 ≈
proxy_buffer_size + proxy_buffers的并发占用。如果 32 worker × 256k ≈ 8MB,整体可控;如果把proxy_buffers 32 64k(即 32 × 64k ≈ 2MB)放到 64 worker × 内存,可能是 128MB。
验证:
bash
wrk -t8 -c100 -d30s -H "Accept-Encoding: gzip" https://your.host/api/big-thing
du -sh /var/cache/nginx/proxy_temp
grep "buffered to a temporary file" /var/log/nginx/error.log | tail -n 5
回滚:把 proxy_buffers 调回 4 16k、proxy_busy_buffers_size 恢复默认。
5.7 踩坑七:keepalive_timeout 0 等配置导致客户端复用连接失败
现象:客户端明明支持 HTTP 1.1 keepalive,但是大量客户端报错 “connection reset” 或升级到 HTTP/2。
判断:keepalive_timeout 0 等价于禁用 keepalive;keepalive_requests 0 在新版 Nginx 中会触发警告。
命令:
bash
# 快速看客户端连接复用情况
ss -tn dst :80 | head
修复:
nginx
keepalive_timeout 65s; # 65s 与浏览器默认值对齐
keepalive_requests 1000; # 单个连接最多处理 1000 个请求,避免极端长连接
reset_timedout_connection on; # 超出 keepalive_timeout 的连接主动 RST
lingering_time 0; # 已关闭的 lingering 立刻清理
风险提醒:
keepalive_requests 0在新版 Nginx 会被识别为禁用请求复用以触发警告,改为keepalive_requests 1或明确的数字。
5.8 踩坑八:静态资源没缓存导致 worker 阻塞在 IO
现象:同一份 CSS/JS 每秒被请求几万次,磁盘 IO 抖动,响应抖动。
修复:
nginx
open_file_cache max=10000 inactive=20s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
nginx
location ~* \.(css|js|jpg|jpeg|png|gif|ico|webp|svg|woff2)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
access_log off;
open_file_cache max=200000 inactive=24h;
}
风险提醒:
open_file_cache是文件句柄级缓存,对经常换的目录(例如每天轮转的日志目录)会持有过期的句柄,要结合open_file_cache_valid调整。
5.9 踩坑九:gzip 开错了对象,CPU 反噬
现象:CPU 不降反升,命中率不增。
判断:可能把图片、视频、字体也开 gzip 了。
修复:
nginx
gzip on;
gzip_min_length1k;
gzip_buffers168k;
gzip_comp_level5;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
gzip_varyon;
gzip_proxied any;
gzip_disable"msie6";
gzip_staticon;
风险提醒:
gzip_comp_level在 5 以上 CPU 上升但压缩率提升小;1~4 已足够。gzip_static需要gzip_static_module编译支持,对应上游有预生成的.gz文件。
5.10 踩坑十:proxy_read_timeout 设错造成 504
现象:业务端慢查询/慢接口,Nginx 这边报 504 Gateway Timeout。
判断:proxy_read_timeout 默认 60s,对慢业务默认没问题;对批量任务/导出要放宽。
修复:
nginx
location /api/long/ {
proxy_read_timeout 300s;
proxy_send_timeout 60s;
proxy_connect_timeout 5s;
}
风险提醒:对所有接口统一一个大
proxy_read_timeout,会让慢请求占用 worker 长时间不放,反而降低吞吐量。要按 location 维度差异化。
5.11 踩坑十一:limit_conn/limit_req 配错导致正常用户被误限
现象:低峰时段误杀客户端,高峰段经常打满。
修复:
nginx
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=20r/s;
server {
location /api/search {
limit_req zone=req_per_ip burst=40 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
风险提醒:
limit_req默认是”漏桶”,突发流量会被排到队列。burst设得过大会让排队延迟很大;nodelay直接返回 429。对核心 API 不建议无差别开启limit_req,要按登录/未登录、CDN/客户端差异化设置。
5.12 踩坑十二:sendfile 关闭导致静态资源吞吐崩
现象:静态文件并发高、文件大,CPU 不高但响应时间高。
修复:
nginx
sendfile on;
tcp_nopush on; # 在响应头前累积,让 sendfile 一次发出去
tcp_nodelay on; # 与 tcp_nopush 互补,让响应体尽快推
aio threads; # 大文件配合
directio 1m; # 大于 1m 的文件绕过 page cache 走 O_DIRECT
风险提醒:
tcp_nopush会让响应头延迟发出(要等缓存满),如果对外缓存(Varnish/CDN)有”首字节超时”的握手逻辑,要留意。directio在机械盘上得不偿失,建议 SSD/NVMe 上启用。
5.13 踩坑十三:server_names_hash_max_size 太小启动失败
现象:nginx -t 直接报 could not build the server_names_hash, you should increase server_names_hash_max_size。
修复:
nginx
server_names_hash_bucket_size 128;
server_names_hash_max_size 2048;
风险提醒:
bucket_size默认 32/64/128,如果机器名非常长(例如 64 字符),直接调到 128 是常见做法。
5.14 踩坑十四:log_format 里自定义变量打爆 CPU
现象:CPU 单 worker 偏高,QPS 不及预期。
判断:log_format 里引用了昂贵的变量,例如 $request_body、$arg_*、$cookie_*,每次请求都要做正则匹配。
修复:
nginx
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main buffer=64k flush=10s;
风险提醒:
buffer=64k和flush同时存在会让数据写入 IO 突发,要看磁盘能力;/var/log/nginx/access.log推荐放到独立磁盘或者挂到 logstash/fluentd。
5.15 踩坑十五:worker_connections 调死板,反代模式下连接数双倍
现象:反向代理开启后压不到预期值。
判断:worker_connections 在反向代理场景下要算双倍:每条请求要占”客户端连接 + 上游连接”。
修复:
nginx
worker_processes 8;
worker_connections 16384; # 8 × 16384 = 131072, 反代下 65536 个客户端连接
计算公式:每条反代请求 worker 占用 2 个连接,因此
理论并发上限 = worker_processes × worker_connections / 2。
5.16 踩坑十六:upstream server 配错 weight 或失败重试
现象:某些上游节点流量倾斜明显,部分节点压力小。
修复:
nginx
upstream backend {
least_conn; # 或 random with [2]; 按需
server10.0.0.11:8080 weight=5 max_fails=3 fail_timeout=10s;
server10.0.0.12:8080 weight=5 max_fails=3 fail_timeout=10s;
server10.0.0.13:8080 weight=1 backup; # backup 仅在主全部失败时进入
keepalive32;
}
server {
location / {
proxy_next_upstreamerror timeout http_502 http_503 http_504;
proxy_next_upstream_tries2; # 不要再重试更多次
proxy_pass http://backend;
}
}
风险提醒:
proxy_next_upstream_tries默认0表示无上限;生产一定要显式设上限。
5.17 踩坑十七:proxy_set_header 漏掉 X-Forwarded-For,上游拿不到真实 IP
修复:
nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
风险提醒:上游应用要从合规白名单中读取头,不要信任不可控代理的
X-Forwarded-For。
5.18 踩坑十八:日志目录 inode 写满
现象:一切看着正常,但 Nginx 写日志失败,新的请求大量 502。
命令:
bash
df -i /var/log
修复:
- 把日志放到独立分区;
- 用
logrotate严格; - 启用
access_log buffer=64k flush=10s等设置; - 监控 inode。
bash
cat /etc/logrotate.d/nginx
text
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
5.19 踩坑十九:灰度压测时把 master 主进程 SIGUSR2 了
现象:在线 reload 出现 worker 全部退出瞬间 502。
修复:nginx -s reload 是平滑升级 worker,不会丢连接;SIGUSR2 用于热替换二进制,要双 master 环境。
风险提醒:在线热替 master 前一定要在旁路节点演练,不要在唯一入口实例上执行。
5.20 踩坑二十:stub_status 暴露在公网
现象:stub_status 没有 deny,公网可访问。
修复:
nginx
location /nginx_status {
stub_status;
access_log off;
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
}
风险提醒:暴露
stub_status的 Worker 数、Active 连接数相当于实时把”是否有热点流量”送给攻击者。
六、常用命令
下面把上文反复用到的命令整理成可复制的小手册。注意每条命令都有”目的”,不要在生产环境直接抄用,要先在自己的环境验证。
6.1 查看 Nginx 自身信息和版本
bash
# 版本、编译参数、模块
nginx -V
nginx -v
目的:看到编译器版本、模块列表、--prefix 等。一个常见的踩坑来源是”现在线上跑的二进制还支持 --with 哪些能力”。
6.2 检查配置和重载
bash
nginx -t # 测试配置
nginx -t -c /etc/nginx/nginx.conf -p /etc/nginx/
nginx -s reload # 平滑 reload
nginx -s reopen # 重开日志
nginx -s stop # 优雅退出
nginx -s quit # 强制退出
风险提醒:
nginx -s stop会让 worker 立刻停止,正在处理的请求会被丢弃;线上一定要先nginx -t、nginx -s reload。
6.3 查看进程与 fd
bash
# 查看 worker 列表与 CPU
ps -o pid,ppid,nlwp,pcpu,pmem,comm -p $(pgrep -f "nginx: worker")
# 查看 worker fd 数
lsof -p $(pgrep -f "nginx: worker") 2>/dev/null | wc -l
# 看进程资源限制
cat /proc/$(pgrep -f "nginx: master")/limits
# 看进程的 nofile 软硬限
grep "open files" /proc/<pid>/limits
6.4 网络连接与状态
bash
# 整体情况
ss -s
# 按状态分组
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 看本机到 upstream 80 端口的连接分布
ss -tn dst :80 | head
# 看目标连接状态
ss -tn dst 10.0.0.11:8080 | head
6.5 系统级限制
bash
sysctl fs.file-max
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_tw_reuse
sysctl net.ipv4.tcp_max_tw_buckets
ulimit -n
cat /etc/security/limits.conf
systemctl show nginx | grep -i LimitNOFILE
6.6 压测基线
bash
# wrk 是最常用的简单压测工具
wrk -t8 -c200 -d30s https://your.host/api/health
# ab 适合 HTTP/1,参数更直观
ab -n 10000 -c 200 https://your.host/api/health
# vegeta 适合多目标、生成文本
echo "GET https://your.host/api/health" | vegeta attack -duration=30s -rate=1000 | vegeta report
# siege
siege -c 200 -t 30s https://your.host/api/health
风险提醒:所有压测都要在灰度/影子实例上做;切忌直接在生产主集群做压测。
6.7 日志观察
bash
tail -F /var/log/nginx/access.log
tail -F /var/log/nginx/error.log
# 一段时间内 5xx 比例
tail -n 5000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn
# 错误关键字
grep -E "accept4\(\) failed|Too many open files|socket\(\) failed|worker process .* exited" /var/log/nginx/error.log | tail
6.8 stub_status 拉取
bash
curl -s http://127.0.0.1/nginx_status
# 输出示例:
# Active connections: 3000
# server accepts handled requests
# 120000 120000 360000
# Reading: 0 Writing: 5 Waiting: 2995
含义:
-
Active connections:当前所有打开的连接数,包括 keepalive。
-
accepts/
handled/requests:累计接受数 / 处理数 / 请求数。若accepts == handled < requests正常;若accepts > handled提示资源不足(worker 已被吃满)。 -
Reading:读取客户端请求头。
-
Writing:写响应到客户端。
-
Waiting:空闲 keepalive 连接。
6.9 第三方状态页(nginx-module-vts)
如果编译了 vts 模块,可以拉 JSON:
bash
curl -s http://127.0.0.1/status/format/json | jq '.serverZones, .upstreamZones'
风险提醒:
vts也必须限制访问,建议监听 127.0.0.1,仅由 Prometheus exporter / 监控抓取。
七、配置示例
下面是一份”基线配置”,并不是”所有机器通用”的最优解,但它是”我没踩到坑”的版本,生产可以以此为起点按需调整。
nginx
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile65535;
worker_cpu_affinity auto;
pid /var/run/nginx.pid;
events {
worker_connections16384;
useepoll;
multi_accepton;
accept_mutexoff; # Linux 3.9+ 单 listen 队列没必要
}
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 '
'uct="$upstream_connect_time" uht="$upstream_header_time" '
'urt="$upstream_response_time"';
sendfile on;
tcp_nopush on;
tcp_nodelay on;
aio threads;
keepalive_timeout65s;
keepalive_requests1000;
reset_timedout_connectionon;
lingering_time0;
types_hash_max_size2048;
server_names_hash_bucket_size128;
variables_hash_max_size2048;
# gzip:CPU 友好配置
gzipon;
gzip_min_length1k;
gzip_buffers168k;
gzip_comp_level5;
gzip_varyon;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
gzip_proxied any;
gzip_disable"msie6";
gzip_staticon;
open_file_cache max=10000 inactive=20s;
open_file_cache_valid60s;
open_file_cache_min_uses2;
open_file_cache_errorson;
upstream backend {
least_conn;
server10.0.0.11:8080 weight=5 max_fails=3 fail_timeout=10s;
server10.0.0.12:8080 weight=5 max_fails=3 fail_timeout=10s;
keepalive32;
keepalive_timeout60s;
keepalive_requests1000;
}
server {
listen80 reuseport backlog=65535; # reuseport 仅在内核 3.9+
server_name _;
access_log /var/log/nginx/access.log main buffer=64k flush=10s;
error_log /var/log/nginx/error.log warn;
# 隐藏版本号(安全)
server_tokensoff;
# 客户端请求缓冲
client_max_body_size16m;
client_body_buffer_size64k;
client_header_buffer_size4k;
large_client_header_buffers832k;
client_body_timeout30s;
client_header_timeout30s;
send_timeout30s;
# 反代全局参数
proxy_http_version1.1;
proxy_set_header Connection "";
proxy_redirectoff;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout5s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
proxy_bufferingon;
proxy_buffer_size16k;
proxy_buffers832k;
proxy_busy_buffers_size64k;
proxy_temp_file_write_size64k;
proxy_max_temp_file_size1024m;
proxy_temp_path /var/cache/nginx/proxy_temp 12;
# 静态资源
location~* \.(css|js|jpg|jpeg|png|gif|ico|webp|svg|woff2)$ {
expires7d;
add_header Cache-Control "public, max-age=604800, immutable";
access_logoff;
try_files$uri =404;
}
# API
location /api/ {
proxy_pass http://backend;
proxy_next_upstreamerror timeout http_502 http_503 http_504;
proxy_next_upstream_tries2;
}
# status
location = /nginx_status {
stub_status;
access_logoff;
allow127.0.0.1;
allow10.0.0.0/8;
deny all;
}
}
# Https 入口(生产)
server {
listen443 ssl reuseport backlog=65535;
http2on;
server_name your.host.com;
ssl_certificate /etc/nginx/ssl/your.host.crt;
ssl_certificate_key /etc/nginx/ssl/your.host.key;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout1d;
ssl_session_ticketsoff;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_cipherson;
access_log /var/log/nginx/access.log main buffer=64k flush=10s;
location / {
proxy_pass http://backend;
}
}
}
风险提醒:
ssl_protocols TLSv1.2 TLSv1.3、OpenSSL 版本对应特定值,TLS 1.0/1.1 不应再启用。
如果 HSTS 在其它层已加,这里不要再开
Strict-Transport-Security,避免冲突。
http2 on在某些 OpenSSL 版本下要单独 link Boringssl 或 OpenSSL 1.0.2+,注意编译选项。
八、日志或指标观察方法
Nginx 的可观测性分三块:access/error log(行级)、stub_status/vts(聚合级)、系统层(连接级)。三层要一起看。
8.1 错误日志里要背熟的 8 个关键字
| 关键字 | 含义 | 处理思路 |
| — | — | — |
| accept4() failed (24: Too many open files) | fd 不足 | 调大 worker fd / 系统 fd |
| socket() failed (24: Too many open files) | 同上 | 同上 |
| worker process ... exited with code 137 | OOM kill | 看 dmesg / 限制内存 |
| worker process ... exited on signal 9 | 被 kill -9 | 查 PID 来源 |
| upstream prematurely closed connection | 上游主动断开 | 上游 keepalive/RST |
| connect() failed (110: Connection timed out) | 上游超时 | 防火墙 / 上游拥挤 |
| upstream response is buffered to a temporary file | 缓冲到磁盘 | 调大 proxy_buffers |
| recv() failed (104: Connection reset by peer) | 上游 RST | 上游程序问题 |
8.2 access 日志里要背熟的 4 种状态码
-
499:客户端主动断开请求(Nginx 自定义),多半是浏览器端 cancel,可能是慢响应。
-
502:上游断连。
-
503:上游服务不可用。
-
504:上游响应超时。
判断方法:先排除上游,看 stub_status 的 Reading/Writing/Waiting,再回到 access 日志的时间点。
8.3 stub_status 的 4 个观察动作
bash
while true; do curl -s http://127.0.0.1/nginx_status; sleep 5; done
观察:
-
Active connections是否超过
worker_processes × worker_connections / 2(反代下算双倍)的 50%。 -
accepts == handled是否恒等,否则考虑 fd/资源不足。
-
Reading / Writing比值:长任务少,
Writing通常 < 100;任务并发多,Writing会高。 -
Waiting是否过大,过大代表长 keepalive 多,可能客户端侧有拖拽。
8.4 Prometheus nginx-exporter 观察
如果用的是 Prometheus,nginxexporter 会暴露 nginx_connections_*、nginx_http_requests_total、nginx_up{server}、stub_status 等。Grafana 仪表板上要:
- Active connections:分位线 / 容量比。
- accepts/handled 比值:资源足够时恒等。
- requests status code:4xx/5xx 比例。
- shared zones:proxy_cache、limit_req 等内存使用。
阈值不要硬写绝对值:写”过去 7 天业务基线的 P99″是更稳定的口径。
8.5 dmesg 与系统日志
Nginx 经常被内核 OOM-killer 杀掉,事后看 dmesg | grep -i oom 或 journalctl -k 是必要的:
bash
dmesg -T | grep -E "oom|memory|nginx" | tail
九、排查路径
下面给出一个按时间序列的”高并发故障排查路径”。这一节专门给”线上突然 N 个 5xx”的同学。
9.1 路径概览
- 现象确认(Nginx 的 QPS/错误率/延迟,看 stub_status 和 Prometheus)。
- 单实例 vs 集群(集群都炸 vs 一台炸:诊断方向不同)。
- 上游是否健康(upstream 状态、上游服务、日志)。
- 客户端是否健康(攻击、CDN 同步、浏览器长 keepalive)。
- Nginx 自身(worker、fd、CPU、缓存、缓冲区、配置)。
- 系统层(fd、somaxconn、tcp_tw_*、oom、磁盘)。
- 修改一行灰度一台,回归再放量。
9.2 集群 vs 单实例 的快速判断
bash
# 集群
for host in $(cat hostlist); do
echo "=== $host ===";
curl -s --max-time 2 "http://${host}/nginx_status";
done;
nginx_status 单实例是否健康:同集群多个实例状态对比,可以快速判断是否只一台有问题(socket/进程级)还是所有都有问题(CDN / 上游 / 攻击 / 配置全局)。
9.3 上游健康度快速检查
bash
# 看 upstream 的健康与连接
ss -tn dst 10.0.0.11:8080 | head
ss -tn dst 10.0.0.12:8080 | head
# 看每个 upstream 的连接数
ss -tan | grep "10.0.0.11:8080\|10.0.0.12:8080" | awk '{print $1}' | sort | uniq -c | sort -rn
9.4 客户端健康度与攻击识别
bash
# 看访问 top IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# 看同一 IP 的 4xx/5xx 频率
awk '{if ($9 ~ /^4|^5/) print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
9.5 自身进程与 fd
bash
ps -o pid,nlwp,pcpu,pmem,comm -p $(pgrep -f "nginx: worker")
for pid in $(pgrep -f "nginx: worker"); do echo "=== $pid ==="; cat /proc/$pid/limits | grep "open files"; done
9.6 错误日志关键字锁定
bash
grep -E "Too many open files|worker process .* exited|connect\(\) failed|upstream prematurely|accept4\(\) failed" /var/log/nginx/error.log | tail -n 50
9.7 系统资源上限
bash
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
sysctl fs.file-nr
sysctl net.ipv4.tcp_max_tw_buckets
cat /proc/sys/kernel/random/boot_id
ulimit -n
9.8 缓存与临时目录
bash
du -sh /var/cache/nginx/proxy_temp
ls /var/cache/nginx/proxy_temp | wc -l
9.9 灰度变更的精确执行清单
灰度变更看起来简单,但是真正生产里能踩出 5 种姿势。下面把每一种姿势的”致命错误”和”正确做法”列出来。
姿势一:nginx -s reload + 立即 wrk:
- 致命错误:reload 后 worker 已经在 handle 旧连接,新连接立刻就跑了新配置,压测看不到完整效果。
- 正确做法:reload 后等 30~120 秒,让 keepalive 自然结束,再开始压测。
姿势二:通过滑动版本号配置:
- 致命错误:用脚本批量替换
nginx.conf,把 buffer 数值统一改了,所有实例同时生效。 - 正确做法:先 1 台、后 5 台、再 50%、再全部,每个阶段设观察窗口。
姿势三:通过 nginx upstream:
- 致命错误:upstream 中只放一个 server,且
keepalive 1024把连接挤到上游。 - 正确做法:upstream 必须有真实后端池 + 健康检查 + 灰度切流。
姿势四:通过流量接入层(CDN / L4 / DNS):
- 致命错误:随便改 DNS 切流量,DNS cache 长达 10 分钟到 24 小时,控制不精确。
- 正确做法:使用 SLB / L4 按权重分发或相似方案,避免 DNS 控制。
姿势五:通过 gitops / Ops 平台:
- 致命错误:CI 把所有配置分发给所有节点,发现问题无法快速回滚。
- 正确做法:所有灰度都用 versioned config + 灰度流水线,且灰度必须可阻断(任意一步发现 SLO 不达标,停止继续灰度)。
9.10 真实案例:一次大促 0 点 502 雪崩复盘
下面是一份”事后可以拿出来做案例分析”的真实复盘伪化版本,方便读者对照自己场景。
时间轴:
- T0:00:00 大促正式开闸。
- T+10m:监控告警
nginx_http_requests_total5xx 比例从 0.1% 飙到 4.2%。 - T+11m:客户端报 502/Bad Gateway。
- T+12m:SRE oncall 上场。
现场观察:
-
stub_status显示
accepts 1000000、handled 980000(误差 2 万)。这意味着有 2 万个连接因为资源不足丢弃。 -
wrk跑同接口 P99 延迟从 80ms 飙到 980ms,但上游服务的 P99 延迟只有 120ms。
-
dmesg | grep oom出现 5 行
nginx worker process ... killed process。 -
cat /proc/<pid>/limits显示
open files软限是 1024,硬限是 4096。
判断:
- 不是上游问题,是 Nginx 自身资源被打光。
- fd 软限太小。1024 个 fd 远小于 8 worker × 16384 connections 之下的需求。
- 加上内核有 OOM,说明内存吃紧。
修复:
- 提升
worker_rlimit_nofile到 65535,并把 PAM 限制同步调大。 - 把
worker_processes调小到 8(机器内存吃紧),让单 worker 连接数翻倍。 - 把临时文件目录挂到独立 SSD。
- 上游
keepalive_requests100 调到 1000。
结果:T+25m 5xx 比例回到 0.2%;T+40m 业务恢复正常水位。
复盘:
- 改前没有摸底真实资源使用,假设
worker_processes 16没考虑机器内存。 - 没有告警:
fd 软限接近 worker rlimit应该早就告警。 - 没有演练:节前没演练”fd 升级”。
- 没有版本控制:出问题时我们手忙脚乱改了一版没在 git 里。
行动项:
- 在监控里加
nginx_worker_open_files_ratio。 - 把
worker_rlimit_nofile提升到 65535 列入基线配置。 - 周期性演练
wrk -t8 -c5000跑满测试。 - 任何变更先在 staging 跑 30 分钟再上 prod。
9.11 与上游协同的高可用观察
Nginx 层的高可用一定要和上游服务协同,否则只是把”半好半坏”的请求交给上游。
观察点:
-
upstream_status:
upstream_addr、upstream_status两个变量,记录每次请求打到哪个上游、返回了什么状态码。 -
proxy_upstream_name:通过配置
health_check或vts模块聚合上游状态。
健康检查路径(nginx-plus / Tengine / OpenResty 商业版本有 health_check directive):
nginx
upstream backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
health_check interval=2000 passes=3 fails=3 uri=/health type=http;
}
风险提醒:开源 Nginx 不直接支持
health_check,要靠ngx_http_upstream_check_module之类的第三方模块(淘宝 Tengine 才有原生支持)。Open Source Nginx 想要主动健康检查,可以被动用max_fails/fail_timeout配合被动探测,或者用主动探测脚本配合 upstream reload。
开源 Nginx 的替代方案:用 max_fails=3 fail_timeout=10s,被动剔除失败节点;用 prometheus + blackbox-exporter 主动探测,然后通过 reload 或 consul-template 同步配置。
9.12 日志与监控一体化的关键动作
动作一:在 log_format 加 $upstream_addr $upstream_status $upstream_response_time。
nginx
log_format upstream '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" '
'urt="$upstream_response_time" '
'uaddr="$upstream_addr" ust="$upstream_status"';
动作二:把 access.log 推给 kafka / logstash,做实时聚合。
yaml
# filebeat 简化配置
filebeat.inputs:
-type:log
paths:
-/var/log/nginx/access.log
fields:
app:nginx
output.kafka:
hosts: ["10.0.0.31:9092"]
topic:nginx-access
动作三:通过 nginx_status 把数据给 Prometheus 抓取。
yaml
# prometheus 简化配置
scrape_configs:
- job_name: nginx
static_configs:
- targets: ['10.0.0.71:9113']
scrape_interval: 15s
nginx-exporter 通过 HTTP 拉取 stub_status 页面,暴露出 nginx_connections_*、nginx_http_requests_total、nginx_up、nginx_total_requests 等指标。
风险提醒:所有抓取必须受限于”只允许监控网段抓取”,并且不能写日志 size 字段敏感信息。
十、风险提醒
下面把生产环境最容易”反噬”的 12 个动作整理出来。
10.1 把任意参数开大 = 出问题
例如 worker_connections 65536、upstream keepalive 1024,单实例内存会迅速被吃光。
10.2 不要在生产改内核参数而不验证回滚
sysctl 改错了,许多参数不能在不重启情况下反向调整。要在 sysctl.d/ 留文件,写在版本控制里,方便回滚。
10.3 手动改 /etc/resolv.conf 而不知道 systemd 也会写它
systemd-resolved 会覆盖 DNS 配置,要选一个稳定的入口(/etc/resolv.conf 由 NetworkManager 还是由 systemd-resolved 维护),不要手动维护。
10.4 nginx -t 失败仍然 nginx -s reload
reload 实际会被拒,但部分负载均衡/灰度策略会让错误的配置依旧下发,写Bash脚本要保 set -e。
10.5 修改 proxy_set_header 没测回放
把头部从 X-Real-IP 改成 X-Forwarded-For,某些上游应用会做白名单校验。
10.6 修改 ssl_certificate 后忘了 nginx -s reload
只会影响新连接,老 keepalive 会继续跑老证书。
10.7 改 gzip_comp_level 过大
CPU 占用上升但压缩率提升小,省不了太多带宽反而吃 CPU。
10.8 用 mount --bind 拼 log 目录
不要为了让多个 nginx 写同一个文件而用 bind mount,filename rotation 一来就崩。
10.9 nginx -s stop 拆集群
在主集群正式入口实例上执行 stop,会立刻断流;务必先摘流量。
10.10 启用 proxy_intercept_errors on 但缺少 fastcgi 接口
错误页会被 Nginx 抓走,导致上游 5xx 自定义错误页无法返回。
10.11 add_header 在 4xx/5xx 响应中失效
add_header 默认只在 2xx/3xx 响应上加上,要加 always 才在所有状态码上都附加。
nginx
add_header Cache-Control "no-store" always;
10.12 在 IPv6 监听上忘记处理 IPv4
listen 80; 默认监听 IPv4;如果用 listen [::]:80; 反而要单独写 listen 80; 监听 IPv4,否则客户端 IPv4 拒绝。
十一、验证方式
任何改动都要有验证标准,下面给一个最小验证集。
11.1 配置层验证
bash
nginx -t
输出 test is successful 与配置文件路径。
11.2 进程层验证
bash
nginx -s reload
ps -o pid,ppid,nlwp,pcpu,pmem,etime -p $(pgrep -f "nginx: worker")
关注 PID 是否变更(如果没变要警觉 reload 是否真的执行)。
11.3 连接层验证
bash
wrk -t8 -c200 -d30s https://your.host/api/health
P99 延迟、QPS、错误率三个指标,每一种改动都要看到改善或者没变差。
11.4 上游健康度验证
bash
ss -tn dst 10.0.0.11:8080 | head
ss -tn dst 10.0.0.12:8080 | head
要看连接在多 worker 上分布均匀。
11.5 缓存验证
bash
# 看 proxy_cache 是否生效
head -1 /var/log/nginx/access.log
# 输出带 HIT/MISS 表示 proxy_cache 生效
或者在 log_format 里加上 $upstream_cache_status,对应 HIT / MISS / EXPIRED / STALE / UPDATING / REVALIDATED。
11.6 stub_status 数据
bash
curl -s http://127.0.0.1/nginx_status
曲线对比”基线 / 当前 / 上一版”,从云监控视角比单数字看更稳定。
11.7 错误日志
bash
journalctl -u nginx -n 200 --no-pager
tail -F /var/log/nginx/error.log
只要 Too many open files、worker process ... exited 这类关键字没出现,就算稳定。
十二、回滚方案
下面给一份”应急回滚清单”,生产里把这一节做成 runbook,贴到 Wiki 上。
12.1 配置回滚
bash
# 强制重读上一次可用配置
NGINX_DIR=/etc/nginx
cp ${NGINX_DIR}/nginx.conf.bak $(date +%Y%m%d) ${NGINX_DIR}/nginx.conf
nginx -t && nginx -s reload
建议在 CI 里做”发布前备份”,文件名用时间戳。
12.2 内核参数回滚
bash
cp /etc/sysctl.d/99-network.conf.bak /etc/sysctl.d/99-network.conf
sysctl -p /etc/sysctl.d/99-network.conf
12.3 fd 限制回滚
bash
sed -i.bak 's/^nginx.*nofile.*/nginx soft nofile 65535/' /etc/security/limits.d/99-nginx.conf
12.4 服务状态回滚
bash
systemctl restart nginx
systemctl status nginx --no-pager
风险提醒:
systemctl restart不是平滑 reload;如果线上只能 reload,不要尝试 restart。
12.5 上游健康回滚
如果上游配置错误导致 502,先把流量从错误的上游摘走,让 default back 到原配置:
bash
# 切流量,例如把 5% 流量切回旧版本
curl -X POST -H "Authorization: Bearer xxx" https://lb-controller/api/server/enable_old?percent=20
12.6 缓存与缓冲目录回滚
如果 proxy_temp_path 损坏,直接修改路径:
nginx
proxy_temp_path /var/cache/nginx/proxy_temp_v2 1 2;
mkdir -p /var/cache/nginx/proxy_temp_v2
chown -R nginx:nginx /var/cache/nginx/proxy_temp_v2
nginx -s reload
十三、生产环境注意事项
下面这些是”做任何 Nginx 高并发改动前都要先翻一遍”的检查清单。
13.1 版本控制
-
nginx.conf/
conf.d/在 git 里。 -
每个 patch 用 PR / Issue 串起来。
-
涉及参数变更的 PR 在标题里写明
[nginx-conf] [perf]或[nginx-conf] [fix]。
13.2 发布窗口
- 工作日白天慎重变更。
- 大版本(如 OpenResty、nginx 1.25 → 1.27)放在业务低峰或演练环境。
- 每次发布写出”生效时间、操作人、灰度比例、回滚预案、验证人”。
13.3 灰度
- 同一集群至少留 1 个实例不动,作为 rollback target。
- 用 upstream + 健康检查或独立 API 网关分流量。
- 用同一份
wrk脚本对比前后版本。
13.4 监控与告警
- 监控覆盖:QPS、P99 延迟、5xx 比例、
Active connections、CPU、内存、fd 使用、TIME_WAIT。 - 告警要有”指标 + 持续时间 + 阈值”三要素。
- 阈值要”基线化”,不要写绝对值。
13.5 演练
- 把生产配置在测试环境跑一遍
wrk -t16 -c500 -d60s。 - 演练回滚:先
nginx -s reload到错配置,看错误率变化,再 reload 回正确配置,确认错误率归零。 - 周期性(如每月 1 次)演练一次
SIGUSR2热替二进制。
13.6 文档
- 每个
server、location、upstream有自己的 wiki 说明,包含:用途、SLA、上游、负责人。 - 一旦删除时填上
removed_at:时间。
13.7 安全
-
server_tokens off。
-
add_header X-Content-Type-Options nosniff与
add_header X-Frame-Options SAMEORIGIN。 -
stub_status只允许本地或内网抓取。
-
不要把 nginx 暴露在公网 8080、8000 这种敏感端口。
13.8 TLS 与加密套件
-
启用
TLSv1.2 TLSv1.3,禁用TLSv1.0 TLSv1.1。 -
选择 ECDHE 系列套件。
-
ssl_session_cache shared:SSL:50m。
-
ssl_session_tickets off,避免前端多实例间的会话被滥用。
13.9 重要产物归档
- 把每台 Nginx 的
nginx -V输出、完整nginx.conf、内核sysctl -p输出归档到 cmdb。 - 不允许在不同机器上”看着相似”地维护配置。
十九、A/B 与蓝绿切换的配置文件范式
Nginx 在做 A/B 或蓝绿时,关键是要让”切换可逆、可灰度、可观测”。下面给出三种生产用过的范式。
11.1 范式一:基于 upstream server weight
nginx
upstream backend_blue {
server10.0.0.21:8080 weight=100;
server10.0.0.22:8080 weight=100;
}
upstream backend_green {
server10.0.0.31:8080 weight=100;
server10.0.0.32:8080 weight=100;
}
split_clients$request_id$backend_pool {
50% backend_blue;
50% backend_green;
}
server {
location /api/ {
proxy_pass http://$backend_pool;
proxy_http_version1.1;
proxy_set_header Connection "";
}
}
切流量:直接把 50% 改成 0% / 100%,然后 nginx -s reload,回滚只需再改回去。这种”用 hash 分流”的做法对缓存友好,适合”灰度 5%、25%、50%、100%”的阶梯。
11.2 范式二:基于 header 路由
nginx
map $http_x_canary $backend_pool {
"yes" backend_green;
default backend_blue;
}
server {
location /api/ {
proxy_pass http://$backend_pool;
}
}
切流量:让上游应用在某些用户上打 X-Canary: yes 头,灰度比例完全交给业务方控制。
11.3 范式三:基于 Cookie + hash
nginx
map $cookie_release $backend_pool {
"v1" backend_blue;
"v2" backend_green;
default backend_blue;
}
切流量:前端把 canary 用户的 cookie 置 release=v2,Nginx 自动切到 green。回滚很容易,把 cookie 删掉即可。
风险提醒:所有依赖 hash 的切流都依赖
$request_id、$cookie_*或其它可定制变量;$request_id需要nginx -V包含random_index模块并且配合geoip等。
二十、Nginx 反向代理的安全加固清单
Nginx 反代层是生产安全的第一面墙。下面给出 20 条安全加固清单。
12.1 隐藏版本号
nginx
server_tokens off;
12.2 HTTP 方法限制
nginx
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
return 405;
}
风险提醒:
if在 location 块内不安全,应该用map:
nginx
map $request_method$is_method_allowed {
default0;
GET 1;
HEAD 1;
POST 1;
PUT 1;
DELETE1;
OPTIONS1;
}
server {
location / {
if ($is_method_allowed = 0) { return405; }
proxy_pass http://backend;
}
}
12.3 路径规范化
nginx
merge_slashes off;
或者自定义 regex 校验 $uri 不带 ..、%2e%2e 等。
12.4 限流、限连接
nginx
limit_req_zone $binary_remote_addr zone=req:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
12.5 阻止特定 User-Agent
nginx
map $http_user_agent $bad_ua {
default 0;
~*evil 1;
~*sqlmap 1;
~*acunetix 1;
}
if ($bad_ua) { return 403; }
12.6 SSL 配置
- 仅启用 TLSv1.2 TLSv1.3。
- 仅启用 ECDHE 系列套件。
- 关闭 session ticket(除非用统一的 key)。
nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_cipherson;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout1d;
ssl_session_ticketsoff;
ssl_staplingon;
ssl_stapling_verifyon;
resolver10.0.0.11.1.1.1 valid=300s;
resolver_timeout5s;
12.7 HSTS
nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
12.8 头注入防护
nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
12.9 body size 限制
nginx
client_max_body_size 16m;
client_body_buffer_size 64k;
12.10 慢连接防护
nginx
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 30s;
keepalive_timeout 65s;
keepalive_requests 1000;
二十一、日志治理与归档
Nginx 的 access 日志在日均 PV 1 亿+的集群里,会以每天 50~500GB 的速度增长。治理是必须做的。
13.1 日志分区独立
bash
# 创建独立日志卷
mkdir -p /data/logs/nginx
chown nginx:nginx /data/logs/nginx
# nginx.conf
access_log /data/logs/nginx/access.log main buffer=64k flush=10s;
13.2 logrotate 策略
text
/var/log/nginx/*.log /data/logs/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
风险提醒:
kill -USR1让 Nginx 重开日志文件;如果日志目录 inode 满,reopen 失败,Nginx 会继续写到旧 fd(仍然是删掉的文件,只是表面看不到了),要监控 inode。
13.3 日志字段最小化
nginx
log_format compact '$remote_addr [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time ust="$upstream_status" '
'urt="$upstream_response_time"';
字段越少,IO 越低,但要保证”出事能查清楚”。
13.4 敏感信息规避
nginx
# 信用卡、身份证、token 字段不能进日志:写一个 map 替换
map $request_body $log_body {
default "";
~*\"password\":\"[^\"]+\" "***";
}
更彻底的方式是把 access_log 直接打到 stderr,由 sidecar(如 filebeat)统一处理,生产线上不建议 log_body。
13.5 access.log 转 OpenSearch / ClickHouse
yaml
# filebeat
filebeat.inputs:
- type: log
paths:
- /data/logs/nginx/access.log
fields:
type: nginx-access
output.logstash:
hosts: ["logstash.internal:5044"]
ruby
# logstash pipeline 节选
filter {
grok {
match => { "message" => "%{IPORHOST:remote_addr} \[%{HTTPDATE:time_local}\] \"%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:status} %{NUMBER:body_bytes_sent} rt=%{NUMBER:request_time} ust=\"%{DATA:upstream_status}\" urt=\"%{NUMBER:upstream_response_time}\"" }
}
date {
match => [ "time_local", "dd/MMM/yyyy:HH:mm:ss Z" ]
}
}
风险提醒:grok 在 QPS 高时是 CPU 重灾区;如果流量大,建议预先在
logstash中关掉 codec,改为mutate + kv,或直接用loki/vector等更现代的方案。
二十二、压缩与传输层优化细节
14.1 静态资源预压缩
nginx
location ~* \.(js|css|html|svg)$ {
gzip_static on;
gzip_proxied any;
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
gzip_static on 让 Nginx 在磁盘上找 ${uri}.gz 直接发送。比现压省 CPU 但要把预压缩文件上传到服务器。
14.2 Brotli 压缩
nginx
brotli on;
brotli_comp_level 4;
brotli_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
brotli_static on; # 优先用 .br 预压缩文件
风险提醒:
brotli要 nginx 模块支持(google/ngx_brotli),要确认编译时包含。
14.3 Vary 头
nginx
gzip_vary on;
add_header Vary "Accept-Encoding" always;
风险提醒:必须保证客户端的
Accept-Encoding不同不会返回错误版本。开源 Nginx 通过gzip_vary已经做了。
14.4 HTTP/2 与 HTTP/3
nginx
listen 443 ssl http2;
http2_push_preload on;
http2_push_preload 可以通过 Link 头推送关键资源。HTTP/3 还在主流生产化过程中,部分发行版已支持,但是商业建议在少量灰度环境中跑。
14.5 zero-copy 与 sendfile、splice
nginx
sendfile on;
aio threads;
sendfile 是 Linux 的 zero-copy API;aio threads 在 worker 里另开 IO 线程。两者不是替代关系而是搭配。
14.6 大文件传输
nginx
location /files/ {
directio 4m; # 大于 4m 走 O_DIRECT
sendfile_max_chunk 2m;
aio threads;
add_header Accept-Ranges bytes;
}
风险提醒:
directio在机械盘上得不偿失,建议 SSD/NVMe 才用。
二十三、与现代基础设施的协同
15.1 与云 L4 SLB 的协同
把 keepalive 作为”上游”:
nginx
upstream slb {
server 10.0.0.100:80; # L4 SLB 后面的内部 VIP
keepalive 64;
keepalive_timeout 60s;
keepalive_requests 1000;
}
15.2 与 Kubernetes Ingress-Nginx 的协同
Ingress-Nginx 是基于 Nginx 的扩展,它在 Pod 里跑一个 Nginx 实例。如果直接管 Nginx 反代,可以把 Kong/APISIX/OpenResty/OpenResty 之类作为替换。
15.3 与 Service Mesh 的边界
Nginx 不是服务网格,但常被作为 Ingress Gateway 暴露给内部 mesh。Istio Mesh 把精细调度(限速、路由、auth)做在 Pod 里,Nginx Ingress 只做 L7 入口。
15.4 与 OpenResty 的桥接
OpenResty 把 Lua 嵌入到 Nginx,关键指令都兼容。它的优势在于”可以写自定义逻辑”——比如实时拼接动态配置、调用外部 API 做鉴权。本文的 OpenResty 兼容配置示例都适用于 OpenResty,唯一要注意的是 nginx module 与 OpenResty 的兼容性。
15.5 与动态配置系统的接口
Consul Template、etcd + confd、Nacos + 自研脚本,都是把远程配置拉到本地 nginx.conf,然后 reload。脚本要保证:
- 拉取失败时不要 reload 老配置。
- reload 失败回滚时,要 kill -HUP 一次。
- reload 期间日志写入能保持连续。
bash
#!/usr/bin/env bash
# 简化 skeleton
set -euo pipefail
BACKUP="/etc/nginx/nginx.conf.bak"
nginx -t -c /etc/nginx/nginx.conf && cp /etc/nginx/nginx.conf $BACKUP
/etc/nginx/confd --onetime # 或 consul-template -config ...
nginx -t && nginx -s reload
二十四、大流量演进路径
下面是一段”摸高”演进的思路,适合增量做。
16.1 阶段一:单机 Nginx 静态资源服务
client --> nginx (static files + fallback)
\-> upstream 1 (single backend)
需要:worker_processes 8、worker_connections 4096、proxy_buffer。
适用:QPS < 2000。
16.2 阶段二:双 Nginx 反代 + 双后端
client --> L4 SLB --> nginx-A\
--> nginx-B/ (round-robin or VIP)
\--> upstream (2 backends, keepalive)
需要:keepalive、stub_status、lvs/keepalived。
适用:QPS < 20000,单机房。
16.3 阶段三:多机房 + CDN
client --> CDN (80%)
--> L4 SLB --> nginx cluster (20% direct)
\--> upstream (per-shard)
需要:CDN 回源策略、qhash/cookie 路由、健康检查。
适用:QPS > 50000,跨地域。
16.4 阶段四:网关 + Mesh
client --> CDN --> L4 SLB --> API Gateway (Kong/APISIX) --> Service Mesh
\-> business service
需要:网关 + 双 mesh、限流、灰度。
适用:QPS > 100000,复杂治理。
每一阶段都对应”Nginx 的角色变化”。角色越往后,Nginx 越倾向于”基础网关”,不再做精细路由。
二十五、性能基线与压测脚本
17.1 压测脚本骨架
bash
#!/usr/bin/env bash
set -euo pipefail
HOST=${HOST:-"https://your.host"}
DURATION=${DURATION:-"30s"}
CONN=${CONN:-"200"}
THREADS=${THREADS:-"8"}
# 预热
echo"Warming up..."
wrk -t${THREADS} -c${CONN} -d10s ${HOST}/api/health > /dev/null
# 压测
echo"Testing..."
wrk -t${THREADS} -c${CONN} -d${DURATION}${HOST}/api/health
# 拉指标
echo"Metrics..."
curl -s http://127.0.0.1/nginx_status | head -5
17.2 wrk 自定义脚本(高级用法)
lua
-- pipelining.lua
init = function(args)
local reqs = {}
for i = 1, 100 do
reqs[i] = wrk.format(nil, nil, headers)
end
return function()
local r = reqs[math.random(1, 100)]
return wrk.format(nil, nil, headers)
end
end
bash
wrk -t8 -c200 -d30s -s pipelining.lua https://your.host/api/health
17.3 压测指标的判断
- QPS(
Requests/sec):基准 80% 以上算”可以接受”。 - P99 延迟:业务可接受范围内的 80% 算”可以接受”,不要追求最低延迟。
- 错误率:< 0.1% 是底线,> 1% 是严重问题。
- CPU:单 worker 不超过 80%(避免系统抖动)。
- 内存:worker RSS 稳定,没有持续上涨(持续上涨是泄漏)。
17.4 baseline 自检
每月 1 次跑一次:
bash
# 跑测试机
client_run=$(hostname -I | awk '{print $1}')
# 收集
result=$(wrk -t8 -c200 -d30s https://${BACKEND}/api/health | tail -n 3)
qps=$(echo "$result" | grep "Requests/sec" | awk '{print $2}')
lat99=$(echo "$result" | grep "99%" | awk '{print $2}')
# 把结果写到时序库
echo "$client_run $qps $lat99" | nc -q 1 ${METRICS_HOST} 2003
二十六、最后的几个工程师习惯
习惯一:永远不要用 nginx -s reload 之外的 reload 方式。SIGHUP、SIGUSR2 等信号都用来升级二进制。
习惯二:每改一个配置文件就在 git 里写一句 commit message 解释。生产事故复盘时第一件事是看 git log -p。
习惯三:在 CI 里加 nginx -t。哪怕是测试环境,也要先 nginx -t。
习惯四:所有压测都在测试环境或专门压测机器做。绝不在生产正式入口实例上跑 wrk。
习惯五:任何改动要带版本号、回滚预案、影响范围、负责人。WIKI 页里至少有这 4 项。
习惯六:每次大版本发布之前做一次回滚演练。脚本化”30 秒内回滚”,并做断点验证。
习惯七:任何超过 50% 实例的改动都先灰度 1 台,看 24 小时后再扩。
习惯八:永远不要相信”网上抄的默认参数”。要写:判断逻辑 + 期望 + 异常表现 + 下一步动作四件套。
二十七、总结
Nginx 高并发优化是一场与边界的战争,边界有四层:连接边界、文件描述符边界、内核协议栈边界、上游服务边界。每一层都有明显可调的旋钮,但每一个旋钮都有副作用。
把全文凝练成 12 条:
-
worker_processes auto+
worker_cpu_affinity auto,让自动分配代替人工估摸。 -
worker_rlimit_nofile必须大于
worker_connections * 2,系统fs.file-max同步调大,systemd 服务的LimitNOFILE必须显式覆盖。 -
反代场景下把
worker_connections按双倍连接算。 -
multi_accept on让 worker 一次性 accept 多个连接,节省一次 system call。
-
keepalive_requests 1000、
keepalive_timeout 65s、reset_timedout_connection on三连,避免 worker 卡在破连接上。 -
反代上游必须开
keepalive池,且proxy_http_version 1.1+proxy_set_header Connection "",否则起效。 -
proxy_buffer_size/proxy_buffers/proxy_busy_buffers_size三者按”×4 法则”分配,避免写入临时目录。
-
proxy_read_timeout按 location 差异化,不统一最大化,避免慢请求卡 worker。
-
gzip_types不包含图像/视频/字体,
gzip_comp_level在 4~5 之间。 -
open_file_cache+
expires让静态资源走 OS 缓存 + 客户端缓存。 -
tcp_tw_reuse/
tcp_fin_timeout/tcp_max_tw_buckets一起调;tcp_tw_recycle永远不开。 -
nginx -t+
nginx -s reload是日常标配,重启 master 必须有 shadow master。
每一条改动都要把”现象 → 判断 → 命令 → 配置 → 验证 → 回滚”六步走完,没有”我听同事说这样配过”这种理由。生产环境的功劳是稳,不是酷。
文末福利
今天给大家分享一份超级牛掰的Linux学习笔记,足足有1456页!是一位Linux运维大佬整理分享的,分享是获得大佬同意的,大家有需要的尽管收藏起来!
笔记介绍
这份笔记非常全面且详细,从Linux基础到shell脚本,再到防火墙、数据库、日志服务管理、Nginx、高可用集群、Redis、虚拟化、Docker等等,与其说Linux学习笔记,不如说是涵盖了运维各个核心知识。
并且图文并茂,代码清晰,每一章下面都有更具体详细的内容,十分适合Linux运维学习参考!
笔记展示
笔记下载
扫描下方二维码,回复暗号“1456页Linux笔记“,即可100%免费领取成功
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《Nginx 高并发优化,我踩过的坑都在这里》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论