LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

Nginx 高性能配置调优与反向代理最佳实践

admin
2026年11月9日 11:41 本文热度 498

 一、问题背景:性能问题往往不是"参数没调",而是"用错了" 

先说三个真实场景,都是我在生产环境里反复遇到的。

场景一:QPS 上不去,CPU 却不高。

一台 8 核 16G 的 Nginx,前面挂着四个 Java 后端,官方压测工具报 3000 QPS 时响应时间突然从 30ms 跳到 800ms。登上去看,top 里 Nginx worker 的 CPU 加起来只有 40%,ss -s 显示 timewait 八万多,netstat -anp | grep nginx | grep -c ESTAB 只有几百。也就是说:连接建立得飞快、销毁得也飞快,每个请求都在重新握手。根因是 upstream 没开 keepalive,Nginx 到后端每个请求都是新 TCP 连接,后端的 accept 队列和 TIME_WAIT 端口耗尽把吞吐摁住了。

场景二:大文件下载把内存吃光。

静态资源站点,用户反馈"下 2G 的镜像包,Nginx 内存直接涨到 12G 然后被 OOM Killer 干掉"。看配置,proxy_buffering on,proxy_buffers 8 1m,proxy_busy_buffers_size 8m。看起来每连接只占 8MB,但 1500 个并发下载就是 12GB。而且 proxy_max_temp_file_size 默认 1024m,磁盘写满后开始报 write() failed (28: No space left on device) while reading upstream。真正的解法不是调小 buffer,而是这类大文件根本不该用 proxy_buffering on。

场景三:一条 if 写错,整个站点 502。

有人在 location 里写:

   nginx
if ($http_user_agent ~* "curl") {
    return 403;
}
proxy_pass http://backend;

if 在 location 里是"邪恶的"(这是 Nginx 官方 wiki 的用词,IfIsEvil),在某些指令组合下会创建隐式的嵌套 location,导致 proxy_pass 的 URI 处理行为不符合预期,甚至直接不生效。正确做法是 map 或者独立的 location。

这三个场景的共同点:都不是靠改 worker_processes 解决的。性能调优的前提是搞清楚瓶颈在哪一层,否则调参就是碰运气,甚至会放大问题。

这篇文章围绕"一台 Nginx 从默认配置变成能扛生产流量"这条主线展开,覆盖:进程与连接模型、事件驱动与多路复用、静态文件传输路径、反向代理连接复用与缓冲、超时体系、限流与熔断、缓存、TLS 优化、日志与可观测性、故障排查。每一节都给出配置、生效方式、验证命令和判断依据。

 二、适用场景与不适用场景 

2.1 适用场景

本文的配置策略针对以下场景做过验证:

  • 反向代理网关:Nginx 作为七层入口,后面是应用集群(Java/Go/Python/Node 都可能),需要连接复用、超时控制、灰度分流、限流。
  • 静态资源与对象存储前端:大量图片、视频、安装包、前端构建产物,需要 sendfile、缓存、Range 支持。
  • TLS 卸载点:所有 HTTPS 在 Nginx 终止,内部走 HTTP,需要 HTTP/2、OCSP Stapling、会话复用。
  • 多站点虚拟主机:单机承载几十个 server 块,需要合理的 server_name 匹配策略。
  • API 网关基础能力:路由、鉴权头透传、请求体大小控制、访问日志审计。

2.2 不适用场景

有些场景不适合套用本文的配置,需要单独讨论:

  • 需要复杂业务逻辑的网关:Lua 脚本、JWT 校验、请求改写、协议转换。这类场景应该用 OpenResty 或 APISIX,本文不会展开。
  • 服务网格内部流量:Envoy 在 xDS 动态配置、熔断指标、mTLS 上更成熟,Nginx(尤其开源版)动态配置能力弱,配置重载有成本。
  • 需要频繁动态改配置的场景:开源版 Nginx 改配置必须 reload(哪怕 -s reload 是平滑的,也有内存翻倍和旧 worker 退出的窗口)。如果一天要改几十次路由,应该用带控制面的方案。
  • 纯四层负载:只需要 TCP/UDP 转发、不做七层解析,LVS 或 HAProxy 的 mode tcp 性能更好、路径更短。Nginx 的 stream 模块能用,但不是它的最强项。
  • 超大规模长连接(百万级):单机百万并发需要内核参数、文件描述符、内存全面配合,且要考虑单点故障。这个量级通常用 LVS + 多台 Nginx 分层。

2.3 版本边界

本文的配置以开源版 Nginx 稳定分支 1.30.x 为基线(写作时的最新稳定版为 1.30.6),同时标注了 1.29.x / 1.31.x 引入的行为变化,因为这些变化会直接影响"默认值"的理解。

开源版和商业版(NGINX Plus)的差异必须说清楚,否则照抄 Plus 文档会踩坑:

能力开源版NGINX Plus
动态 upstream 增删(API)不支持支持
zone 共享内存同步 upstream 状态支持支持
主动健康检查(health_check)不支持(只有被动 max_fails)支持
sticky 会话保持1.29.6 起开源可用支持
least_time 负载算法1.31.0 起开源可用早就有
ngx_http_keyval_module1.9.0 起开源可用支持
JWT / OIDC不支持支持
实时活动监控面板不支持支持

关键提醒:网上大量教程里的 health_check、api、state、zone_sync 都是 Plus 专属,开源版写了会报 unknown directive,启动直接失败。这是最常见的"抄错配置"来源。

 三、验收标准:先定义"高性能"是什么 

调优前必须先把目标量化,否则无法判断调完是好是坏。建议按下面五个维度定基线:

维度指标采集方式参考目标(反向代理场景)
吞吐QPS / RPS压测工具 + stub_status达到后端能力的 1.5 倍余量
延迟P50 / P95 / P99访问日志 $request_timeP99 < 后端 P99 × 1.3
资源worker CPU、RSS、fd 数top、/proc/<pid>/status、ls /proc/<pid>/fd | wc -l单 worker RSS 稳定不增长
连接active / reading / writing / waitingstub_statuswaiting 占比 60%~80% 正常
错误5xx 比例、upstream_* 计数日志 + stub_status5xx < 0.1%

这里有个容易被忽略的点:Nginx 的 $request_time 是从读到请求第一个字节到发出响应最后一个字节的时间,包含后端处理时间和网络传输时间;而 $upstream_response_time 只算后端处理时间。两者之差就是"Nginx 自身 + 网络 + 客户端慢"的部分。如果这个差值很大,说明问题不在后端,而在客户端慢速读取或者 Nginx 到客户端的链路。

   nginx
# 日志里同时记录两个时间,才能拆分责任
log_format timing '$remote_addr - $request_id [$time_local] "$request" '
                  '$status $body_bytes_sent '
                  'rt=$request_time urt=$upstream_response_time '
                  'uaddr=$upstream_addr ustatus=$upstream_status '
                  'host=$host ua="$http_user_agent"';

$upstream_response_time 在多次重试或内部跳转时会输出成 0.010, 0.020 这样的逗号列表,长度和 $upstream_addr 一致,对应每次尝试。用 awk 统计时要取最后一个或求和,别直接当数字用,否则 awk 会把它当字符串比较。

 四、环境假设 

本文所有配置和命令基于以下环境,如果你的环境不同,结论可能有出入:

项目规格说明
操作系统CentOS Stream 9 / Rocky Linux 9(内核 5.14+)net.core.somaxconn 等参数的默认值与内核版本强相关
Nginx开源版 1.30.x(本文以 1.30.6 为基线)1.29.x / 1.31.x 的差异单独标注
安装方式官方 
nginx.org 仓库 RPM
与发行版自带包的编译参数、模块差异较大
CPU8 核worker_processes auto 会起 8 个 worker
内存16 GB决定 buffer 和 cache 的可用空间
后端Java(Spring Boot,Tomcat)/ Go后端 keepalive 行为不同,会影响连接复用效果
TLSOpenSSL 3.0+ssl_ech_file 需要支持 ECH 的 OpenSSL 分支
部署单机,前置 LVS/云 SLB多机场景需要考虑会话和缓存一致性

关于安装来源:发行版仓库(dnf install nginx)的版本通常滞后,而且编译参数不同(比如默认不带 --with-http_v3_module)。生产环境建议用 nginx.org 官方仓库,或者自己编译并固定参数,这样升级和回滚都可控。

   bash
# 官方仓库(Rocky/RHEL 系)
sudo tee /etc/yum.repos.d/nginx.repo > /dev/null <<'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF

sudo dnf install -y nginx
nginx -V   # 记录编译参数,升级对比用

nginx -V 的输出要存档到配置管理里。每次升级前后对比 configure arguments,能快速发现"新版本少了某个模块"这类问题。

 五、前置检查:动手前必须确认的事 

5.1 确认版本与编译参数

   bash
nginx -v                          # 版本号
nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--with|^--without' | sort

预期输出:能看到 --with-http_ssl_module、--with-http_v2_module、--with-http_realip_module、--with-http_stub_status_module 等。

常见异常:

  • 没有 --with-http_v2_module,写 http2 on 会报 unknown directive "http2" 或者被静默忽略(取决于写法)。
  • 没有 --with-http_realip_module,前面挂 SLB 时拿不到真实客户端 IP,日志全是 SLB 的内网地址。
  • 没有 --with-stream_module,四层转发配置整体不可用。

判断依据:nginx -V 输出的 configure arguments 是唯一可信来源,别信文档,也别信 nginx -m(它只列已加载的动态模块,静态编译的模块不显示)。

5.2 确认当前配置的语法与生效状态

   bash
nginx -t                    # 只检查语法
nginx -T                    # 检查并输出完整生效配置(含 include)

nginx -T 非常重要:配置文件有 include 时,你看到的 .conf 文件不等于实际生效的配置。排查"改了没生效"时,第一步永远是 nginx -T | grep -A5 '关键词'。

注意:nginx -t 只检查语法,不检查语义。下面这些它都不会报错:

  • proxy_pass 指向一个不存在的 upstream 名字(语法通过,运行时 502)。
  • ssl_certificate 文件存在但已过期(语法通过)。
  • root 指向的目录不存在(语法通过,运行时 404)。
  • log_format 里引用了不存在的变量(这个会报错,属于少数)。

5.3 确认文件描述符上限

   bash
# 看 systemd 服务实际生效的限制
systemctl show nginx -p LimitNOFILE
# 看主进程实际打开的 fd 上限
cat /proc/$(cat /run/nginx.pid)/limits | grep -i 'open files'
# 当前 worker 打开的 fd 数
for p in $(pgrep -P $(cat /run/nginx.pid)); do echo "$p: $(ls /proc/$p/fd | wc -l)"; done

判断依据:LimitNOFILE 至少要大于 worker_connections × worker_processes。8 个 worker × 10240 连接 = 81920,再加上日志文件、监听 socket,实际需要 10 万以上。如果发现 LimitNOFILE=1024,那么 worker_connections 10240 就是一句空话——连接数在 1024 附近就会被 too many open files 打断。

注意:worker_connections 限制的是每个 worker 进程的连接数,不是全局。而且每个到后端的连接也占一个 fd,反向代理场景下实际可服务的客户端连接大约是 worker_connections 的一半。

5.4 确认内核参数

   bash
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.ip_local_port_range \
       net.ipv4.tcp_tw_reuse net.core.rmem_max net.core.wmem_max \
       fs.file-max net.ipv4.tcp_fin_timeout

这几个参数的作用和调整依据:

参数默认值(常见)作用调整依据
net.core.somaxconn4096(5.4+ 内核)listen() 的 backlog 上限Nginx listen ... backlog= 不能超过它
net.ipv4.tcp_max_syn_backlog1024~8192半连接队列长度SYN 洪水或高并发建连时需调大
net.ipv4.ip_local_port_range32768 60999本地端口范围反向代理场景下 Nginx 主动连后端,端口会耗尽
net.ipv4.tcp_tw_reuse0 或 2允许复用 TIME_WAIT 连接发起新连接主动发起连接多的一侧(Nginx)建议设 1
fs.file-max极大系统级 fd 上限一般不用动,LimitNOFILE 才是瓶颈

ip_local_port_range 是反向代理场景的隐形瓶颈。默认范围只有约 2.8 万个端口,如果 Nginx 到后端不做连接复用,高 QPS 下会在几分钟内耗尽,报 Cannot assign requested address。所以第五节和第七节的 keepalive 是配套的:不解决连接复用,调端口范围只是把问题推迟。

5.5 确认后端是否支持长连接

这是最容易被跳过的一步。Nginx 侧配了 keepalive 32,但后端每个请求都回 Connection: close,那连接复用等于没开。

   bash
# 直连后端,观察响应头
curl -sv -o /dev/null http://127.0.0.1:8080/health 2>&1 | grep -i '^< '

判断依据:

  • 响应头里有 Connection: keep-alive(HTTP/1.1 默认不发这个头,但会保持连接)→ 支持长连接。
  • 响应头里有 Connection: close → 后端主动关闭,需要改后端。
  • Tomcat:检查 server.tomcat.max-keep-alive-requests(默认 100)和 connectionTimeout。
  • Go net/http:默认支持,但 Server.IdleTimeout 要设。
  • PHP-FPM:需要 fastcgi_keep_conn on 且 FPM 侧支持。

同时要确认后端的 keepalive_timeout 大于 Nginx 的 keepalive_timeout(upstream 块的),否则后端先关,Nginx 复用一个已关闭的连接就会收到 upstream prematurely closed connection,触发重试,反而更慢。

 六、核心知识:Nginx 为什么快,以及快在哪一步 

要把参数调对,得先知道 Nginx 的架构约束在哪。这一节讲四个东西:进程模型、事件驱动、连接生命周期、内存分配。

6.1 进程模型:master + worker

Nginx 启动后是两类进程:一个 master,N 个 worker。

   bash
ps -eo pid,ppid,cmd | grep '[n]ginx'

预期输出:

   text
1234     1 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
1235  1234 nginx: worker process
1236  1234 nginx: worker process
...

master 只做四件事:读配置、绑定端口、管理 worker(fork/退出/信号)、reopen 日志。它不处理任何请求。所有请求都由 worker 处理。

这个模型带来三个直接推论:

推论一:worker 数不该超过 CPU 核数。

   nginx
worker_processes auto;   # 等于 CPU 核数

超过核数只会增加上下文切换。唯一的例外是有大量阻塞型操作(比如访问慢速网络文件系统、执行大量 subrequest),这时可以略微超过,但在现代内核上收益很小。

推论二:每个 worker 是单线程事件循环,任何阻塞操作都会拖死整个 worker。

这意味着:不要在 access_log 里指向网络文件系统、不要用会阻塞的 auth_request 后端、不要滥用 proxy_pass 到响应极慢的服务。一个 worker 卡住,它手里那几千个连接全部卡住。

推论三:共享状态必须通过共享内存。

limit_req_zone、limit_conn_zone、proxy_cache_path 的 keys_zone、upstream 的 zone,都要显式声明共享内存大小。多个 worker 之间靠这块内存同步计数。如果 zone 给得太小,会出现 zone "xxx" is exhausted 日志,限流和缓存统计开始失准。

   nginx
# 限流 zone:10m 大约能存 16 万个 key(每个 key 约 64 字节 + 状态)
limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=conn_perip:10m;

# upstream 状态共享(被动健康检查的失败计数)
upstream backend {
    zone backend_state 64k;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
}

没有 zone 的 upstream,每个 worker 独立计算 max_fails,导致同一个故障后端被不同 worker 反复探测,重试次数不可控。

6.2 事件驱动:epoll 与 worker_connections

Linux 上 Nginx 用 epoll(use epoll;,现代版本自动选择,无需显式配置)。一个 worker 用一个 epoll 实例管理它的所有连接。

   nginx
events {
    worker_connections 10240;
    multi_accept on;         # 一次 accept 取完队列中的所有连接
    accept_mutex off;        # 现代内核建议 off
    use epoll;
}

几个参数的真实含义:

  • worker_connections:每个 worker 能同时打开的最大连接数(fd 数)。由于每个客户端连接 + 每个到后端的连接各占一个 fd,反向代理场景下实际客户端并发 ≈ worker_connections / 2。
  • multi_accept on:新连接到来时一次性 accept 队列里的所有连接。配合 somaxconn 调大使用。在高并发建连场景收益明显;低并发场景会略微增加延迟(因为一次循环里多做了工作)。
  • accept_mutex off:老版本用互斥锁避免"惊群",现代内核(2.6+)的 epoll 已经用 EPOLLEXCLUSIVE 解决了,所以官方默认就是 off。网上很多教程还写 accept_mutex on,这是过时建议。

容量计算公式:

   text
最大并发连接数 = worker_processes × worker_connections / 2   (反向代理场景)
所需 fd 上限   ≈ worker_processes × worker_connections + 冗余 20%

8 核 × 10240 / 2 = 40960 并发客户端连接,需要 LimitNOFILE ≥ 8 × 10240 × 1.2 ≈ 98304。

6.3 连接的生命周期与四个状态

stub_status 输出的 Active connections 会拆成三部分:

   nginx
server {
    listen 127.0.0.1:8081;
    location /nginx_status {
        stub_status;
        access_log off;
        allow 127.0.0.1;
        deny all;
    }
}
   bash
curl -s http://127.0.0.1:8081/nginx_status

预期输出:

   text
Active connections: 1024
server accepts handled requests
 1258362 1258362 3874210
Reading: 3 Writing: 12 Waiting: 1009

四个数字怎么读:

  • accepts / handled:接受的连接数 / 处理的连接数。两者不一致说明有连接被丢弃(通常是 fd 不够或 worker_connections 到了上限)。
  • requests:总请求数。requests / handled = 平均每连接的请求数。这个值如果在 1.0 附近,说明 keepalive 没生效。
  • Reading:正在读请求头的连接(通常很小,个数级)。
  • Writing:正在写响应的连接。
  • Waiting:keepalive 空闲连接。正常情况下应该占 Active 的 60%~80%。

判断依据:

  • accepts - handled > 0 且持续增长 → 查 worker_connections、LimitNOFILE、somaxconn。
  • requests / handled ≈ 1 → 客户端或中间设备在破坏 keepalive,检查 keepalive_timeout 和前置 SLB 的空闲超时。
  • Waiting 接近 0 且 Writing 很高 → 后端慢或客户端慢速读取,请求堆积。

6.4 内存:哪些地方会吃内存

Nginx 的内存占用不像 Java 那样有个大堆,它是分布式的,容易误判。主要消耗在:

来源单连接/单实例消耗控制指令风险
连接结构体约 1~2 KB(含读写 buffer 初始分配)worker_connections—
请求头 bufferclient_header_buffer_size 默认 1klarge_client_header_buffers超长 header 走大 buffer
请求体 bufferclient_body_buffer_size 默认 8k/16kclient_max_body_size超过则落盘
代理响应 bufferproxy_buffers 8 4k/8kproxy_buffering大响应 × 高并发 = 内存炸弹
代理临时文件写到 proxy_temp_pathproxy_max_temp_file_size磁盘写满
缓存proxy_cache_path ... max_size=直接在磁盘磁盘写满
共享内存 zone声明多少占多少zone启动即分配
TLS 会话缓存ssl_session_cache shared:SSL:10m10m ≈ 4 万个会话—

最容易出事的是 proxy_buffering on + 大响应。默认 proxy_buffers 8 4k(小文件)或 8 8k(大文件),proxy_busy_buffers_size 默认是 2 × proxy_buffers。一个连接最多占用 (8+16) × 8k ≈ 192KB 的 buffer。1000 个并发的 50MB 文件下载 = 192MB,看起来还好;但如果有人把 proxy_buffers 调成 8 1m,就是 1000 × 24MB = 24GB,直接 OOM。

判断依据:

   bash
# 看单个 worker 的 RSS
ps -o pid,rss,vsz,cmd -p $(pgrep -P $(cat /run/nginx.pid) | tr '\n' ',' | sed 's/,$//')

单个 worker RSS 稳定在 20~80MB 是正常范围(取决于配置和缓存)。持续增长不回落,通常是 buffer 泄漏信号——优先查 proxy_buffering 和上游响应大小。

 七、整体实施思路 

调优不是一次性动作,而是一条有顺序的路径。顺序错了会白干。

7.1 六个阶段的顺序

   text
① 系统层调优(内核参数、fd、文件系统)
   ↓ 没有这一步,后面所有连接数配置都是假的
② 进程与事件模型(worker_processes、worker_connections、keepalive 语义)
   ↓ 决定容量天花板
③ 静态文件传输路径(sendfile、tcp_nopush、open_file_cache、aio)
   ↓ 静态资源占比高的站点收益最大
④ 反向代理核心配置(连接复用、buffer、超时、重试)
   ↓ 动态请求的延迟和稳定性主要靠这一层
⑤ 保护性配置(限流、限连、超时、请求体大小、缓存)
   ↓ 防止单点故障扩散成雪崩
⑥ TLS 与 HTTP/2/3 优化(会话复用、OCSP、密码套件)
   ↓ 影响首字节时间

为什么是这个顺序:每一层的效果依赖上一层。比如第④步的 keepalive 32 需要第①步的 ip_local_port_range 和第②步的 worker_connections 配合;第⑥步的 TLS 握手优化在第①步 fd 不够时毫无意义,因为连握手的机会都没有。

7.2 每一步都要有验证

不能"改完就上线"。每一步至少验证三件事:

  1. 配置生效:nginx -T | grep 确认配置被加载;nginx -s reload 后看 error.log 无报错。
  2. 行为改变:用具体命令观察指标变化(stub_status、ss、日志字段)。
  3. 没有副作用:确认旧行为没被破坏(比如 keepalive 开了之后,有没有出现 upstream prematurely closed)。

7.3 变更安全基线

Nginx 配置变更看起来风险低(reload 是平滑的),但有几类变更会造成真实故障:

变更类型风险缓解措施
worker_processes / worker_connectionsreload 期间新旧 worker 并存,内存和 fd 可能翻倍在低峰期做,确认 LimitNOFILE 有余量
listen 端口/协议变更新端口未放通防火墙,直接不可用先加防火墙规则再 reload
ssl_certificate 更换证书链不完整导致部分客户端失败用 openssl s_client 验证完整链
proxy_pass 目标变更后端未就绪导致 502先检查后端健康,灰度单台 Nginx
限流规则上线阈值过低直接误杀正常流量先 limit_req ... dry_run 观察(商业版)或先设高阈值
client_max_body_size 调小上传功能立刻失败与业务方确认最大上传尺寸

reload 的真实行为:nginx -s reload 并不会中断已有连接。master 重新读配置,启动新 worker,旧 worker 停止接受新连接,处理完手头请求后退出。所以:

  • 长连接(WebSocket、大文件下载)会让旧 worker 存活很久,内存会阶段性翻倍。
  • 如果旧 worker 迟迟不退出,说明有长连接没结束。看 ps 里旧 worker 还在不在,用 nginx -s quit(优雅退出)而不是 stop。

验证 reload 成功:

   bash
nginx -t && nginx -s reload
sleep 1
tail -20 /var/log/nginx/error.log | grep -iE 'emerg|alert|error' || echo "no error"

 八、实战一:系统层与内核参数 

8.1 目标

让系统层面不成为瓶颈:fd 够用、端口够用、连接队列够深、TCP 行为符合反向代理场景。

8.2 文件描述符

   bash
# /etc/security/limits.d/nginx.conf
cat > /etc/security/limits.d/nginx.conf <<'EOF'
nginx soft nofile 102400
nginx hard nofile 102400
EOF

但对 systemd 管理的 Nginx,limits.conf 不一定生效,因为 systemd 服务默认忽略 PAM limits。以实际生效的为准:

   bash
# /etc/systemd/system/nginx.service.d/limits.conf
mkdir -p /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=102400
EOF
systemctl daemon-reload
systemctl restart nginx

验证:

   bash
cat /proc/$(cat /run/nginx.pid)/limits | grep -i 'open files'

预期输出:

   text
Max open files            102400               102400               files

异常:如果还是 1024,说明 nginx.service 里自己写了 LimitNOFILE=1024(某些发行版包会写)。用 systemctl cat nginx 看完整的 unit,drop-in 的优先级高于主 unit,但主 unit 里如果用了 LimitNOFILE= 会覆盖默认值。检查顺序:systemctl cat nginx | grep -n LimitNOFILE。

注意 restart 而不是 reload:limit 只能在进程启动时设置,reload 不改变已有 worker 的限制。所以改 fd 上限需要 restart,这会有短暂中断——在低峰期做,或者用前面挂载的 LVS/SLB 摘流后再做。

8.3 内核参数

   bash
# /etc/sysctl.d/99-nginx.conf
cat > /etc/sysctl.d/99-nginx.conf <<'EOF'
# 连接队列:必须 >= nginx listen backlog
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# 本地端口范围:Nginx 主动连后端时用,默认 2.8 万在高 QPS 下不够
net.ipv4.ip_local_port_range = 10240 65000

# TIME_WAIT 复用:Nginx 是主动连接方,设 1 允许复用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# TCP 缓冲区:大带宽高延迟链路需要
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# 连接跟踪表(有 conntrack 时)
net.netfilter.nf_conntrack_max = 1048576
EOF

sysctl --system

逐项说明调整依据:

  • somaxconn 与 tcp_max_syn_backlog:Nginx 的 listen ... backlog=511 默认值是 511,而且内核会把它截断到 somaxconn。要真正调大 backlog,两边都要改:listen 80 backlog=8192; + somaxconn=65535。
  • ip_local_port_range:范围是 65000-10240 = 54760 个端口。假设每秒钟 5000 个新连接到后端,TIME_WAIT 持续 60 秒,就需要 30 万个端口——所以光调端口不够,必须开 keepalive。这里的调整是给"开了 keepalive 但仍有突发"留余量。
  • tcp_tw_reuse = 1:只在主动发起连接时复用 TIME_WAIT 套接字,且需要 TCP 时间戳开启(默认开)。对 Nginx→后端这条链路有效,对客户端→Nginx 无效(那条链路的 TIME_WAIT 在客户端)。
  • 不要设 tcp_tw_recycle:该参数在 Linux 4.12 已移除,且在 NAT 环境下会导致连接随机失败。老教程里还能看到它,忽略即可。

验证:

   bash
sysctl net.core.somaxconn net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse
ss -s    # 看 timewait 数量

预期 ss -s 输出:timewait 在几千到几万之间。如果十万以上且持续增长,说明连接复用没做好,回到第五节确认后端 keepalive。

8.4 文件系统与日志

日志写入是最容易被忽略的性能杀手。高 QPS 下访问日志能吃掉可观的 CPU 和 IO。

   nginx
# 减小日志体积和 IO:缓冲写 + 精简格式
access_log /var/log/nginx/access.log timing buffer=64k flush=5s;

buffer=64k 表示攒够 64KB 或 5 秒才落盘。代价是日志延迟——崩溃时最后几秒的日志可能丢失。审计类日志不能这么配。

如果日志量极大(每秒几万条),考虑:

  • 关闭高频健康检查路径的日志:在对应 location 里 access_log off;。
  • 用 map 条件跳过静态资源日志:

   nginx
map $request_uri $loggable {
    ~*\.(js|css|png|jpg|jpeg|gif|ico|woff2?)$  0;
    default                                     1;
}
access_log /var/log/nginx/access.log timing if=$loggable;

map 的 if=$loggable 里,值为 0 或空字符串时不记录。这是 Nginx 访问日志的条件记录机制,比在 location 里写 access_log off 灵活(后者只能整块关闭)。

关于日志切割:用 logrotate + postrotate 发 USR1 信号,让 Nginx 重新打开日志文件:

   bash
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

kill -USR1(即 nginx -s reopen)是必须的,否则 Nginx 继续往已删除的 inode 写,磁盘空间不会释放。这个现象很典型:df 显示磁盘满,du 加起来却很小,lsof | grep deleted 能看到 Nginx 持有的已删除文件。

 九、实战二:静态文件传输路径 

静态资源是 Nginx 最强的场景。默认配置已经不错,但有几处能显著提升吞吐。

9.1 sendfile:绕过用户态拷贝

   nginx
sendfile on;

原理:传统 read() + write() 是"磁盘 → 内核页缓存 → 用户缓冲区 → 内核 socket 缓冲区 → 网卡",两次拷贝。sendfile() 直接"页缓存 → socket 缓冲区",省掉用户态这一次拷贝和两次上下文切换(严格说是零拷贝的一种,配合 TCP_NOPUSH 与 splice 效果更好)。

判断依据:静态文件占比越高收益越大。纯反向代理场景 sendfile 不起作用(内容来自上游 socket,不是文件)。

9.2 tcp_nopush / tcp_nodelay:一对反向的参数

   nginx
tcp_nopush on;      # 只在 sendfile on 时有效
tcp_nodelay on;     # 默认就是 on

这两个名字容易混,实际效果相反:

  • tcp_nopush on:等数据攒满一个 MSS 或整个响应头发完再发,减少小包。配合 sendfile 使用,减少报文数量。
  • tcp_nodelay on:禁用 Nagle 算法,小包立即发送,降低延迟。对 keepalive 连接上的小响应很重要。

为什么两个能同时开:Nginx 的处理逻辑是——响应头单独一个小包立即发出(tcp_nodelay),响应体用 tcp_nopush 攒满再发。Linux 上 Nginx 会在发完头部后自动调整。所以标准配置就是两个都 on。

不要做的事:tcp_nodelay off。这会让小响应被 Nagle 算法延迟最多 40ms,P99 直接爆炸。

9.3 open_file_cache:缓存文件元信息

   nginx
open_file_cache          max=10000 inactive=60s;
open_file_cache_valid    80s;
open_file_cache_min_uses 2;
open_file_cache_errors   on;

作用:缓存 open() 的结果和文件元信息(大小、修改时间、inode),避免每次请求都走一遍 stat() 系统调用。

参数含义:

  • max=10000:最多缓存 1 万个文件描述符。
  • inactive=60s:60 秒内没被访问的条目被移除。
  • valid=80s:每 80 秒校验一次缓存条目的有效性(重新 stat)。
  • min_uses=2:至少被访问 2 次才进缓存,防止一次性文件污染。
  • errors=on:缓存"文件不存在"的结果,减少 404 的 stat 开销。

验证方式:

   bash
# 用 strace 看 stat 调用次数
strace -c -p $(pgrep -P $(cat /run/nginx.pid) | head -1) -e trace=stat,openat

压测 10 秒后看 stat 的调用次数。开启 open_file_cache 后应显著下降。

风险:valid 时间过长会导致文件更新后 Nginx 仍返回旧内容或旧大小。对频繁更新的文件(比如前端发布的 index.html)要么缩短 valid,要么用版本化文件名(app.a1b2c3.js)配合长缓存。这是发布后"用户看到旧页面"的经典原因之一。

9.4 静态资源的完整 location 配置

   nginx
location /static/ {
    alias /data/www/static/;
    expires 30d;
    add_header Cache-Control "public, immutable";

    sendfile on;
    tcp_nopush on;
    aio threads;                      # 大文件异步 IO,1.29+ 稳定
    directio 4m;                      # 大于 4m 的文件绕过页缓存
    output_buffers 4 256k;

    access_log off;
    gzip_static on;                   # 优先用预压缩的 .gz
    etag on;
}

location = /index.html {
    root /data/www;
    add_header Cache-Control "no-cache, must-revalidate";
    etag on;
    # 入口 HTML 绝不能长缓存,否则发布不生效
}

几个关键点:

  • alias 和 root 的区别:alias 是替换匹配到的前缀,root 是拼接。上面 /static/a.png 用 alias 得到 /data/www/static/a.png,用 root /data/www/static 得到 /data/www/static/static/a.png。用错就是 404。
  • directio 4m 与 aio threads:大文件用直接 IO 避免污染页缓存,小文件走 sendfile。两者可以共存,Nginx 按文件大小选择路径。
  • expires 30d + immutable:带 hash 的静态资源可以放心长缓存。immutable 告诉浏览器"这个 URL 的内容永不改变,不用发条件请求"。
  • gzip_static on:需要提前生成 .gz 文件。如果文件不存在,回退到动态压缩(需要 gzip on)。

9.5 Range 请求与大文件

   nginx
location /download/ {
    alias /data/downloads/;
    max_ranges 1;               # 限制并发 Range,防 DoS
    add_header Accept-Ranges bytes;
    # 断点续传依赖 Nginx 自动处理 Range,不要自己写逻辑
}

max_ranges 1 是重要的防护:一个请求带几百个 Range 头会让 Nginx 做大量 IO,是已知的放大攻击手法。设为 1 时,每个请求只允许一个 Range 段;带多个 Range 的请求部分超出限制的部分会被忽略。

不要做的事:不要用 add_header Content-Length 手工设置长度。Nginx 自己算,手工设置会导致截断或头部冲突。

 十、实战三:反向代理连接复用(收益最大的一步) 

这一节是全文性价比最高的部分。在绝大多数"QPS 上不去"的案例里,根因都在这里。

10.1 版本变化:默认值已经改了

必须在开头说清楚,因为网上大量教程还是老版本的写法:

版本变化影响
1.29.7 起proxy_http_version 默认值从 1.0 改为 1.1不再需要显式写 proxy_http_version 1.1(但写了也没坏处)
1.29.7 起upstream keepalive 默认启用,每 worker 32 条不写也会有复用,但数量偏少
1.29.7 起不再默认向上游发送 Connection 头以前要写 proxy_set_header Connection "";,现在默认就不发
1.29.4 起proxy_http_version 2 可用支持 h2c 到上游(不常见,多数后端不支持)
1.29.6 起sticky 会话保持开源可用不需要商业版

判断依据:如果你的 Nginx 是 1.29.7 之前的版本,必须显式写 proxy_http_version 1.1; 和 proxy_set_header Connection "";。如果是之后的版本,仍然建议显式写出来——显式比依赖默认值可靠,因为默认值会随版本变。

10.2 正确的 upstream 配置

   nginx
upstream app_backend {
    # 共享内存:让所有 worker 共享失败计数
    zone app_backend 64k;

    # 负载算法(开源版可用 range)
    least_conn;

    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.13:8080 max_fails=3 fail_timeout=10s;

    # 连接池:每个 worker 保持 64 条空闲连接
    keepalive 64;
    keepalive_requests 1000;      # 单条连接最多处理 1000 个请求后主动关闭
    keepalive_timeout 60s;         # 空闲 60 秒后关闭
    keepalive_time 1h;             # 单条连接最长存活 1 小时

    # 1.31.0 起开源可用:按响应头时间选最快的
    # least_time header;
}

每个参数的作用和取值依据:

  • keepalive 64:每个 worker 的连接池大小。不是总连接数。8 个 worker × 64 = 512 条到后端的空闲连接。
    • 太小 → 复用率低,频繁建连。
    • 太大 → 后端承受更多空闲连接(Tomcat 默认 maxConnections 8192,够用;但如果后端是连接数敏感的数据库代理,要克制)。
    • 经验值:后端实例数 × 每实例期望并发 / worker 数,再留 20% 余量。
  • keepalive_requests 1000:一条连接处理多少个请求后关闭。默认值在 1.29.x 已大幅提升(老版本默认 100)。设 1000 是为了定期重平衡负载——如果一条连接长期绑在某台后端,新加入的实例分不到流量。这是"扩容后新机器没流量"的隐藏原因。
  • keepalive_timeout 60s:空闲多久关闭。必须小于后端的 idle timeout。Tomcat 默认 connectionTimeout 是 20 秒(这是读超时,不是 idle),但 maxKeepAliveRequests 到达后会关。如果后端先关而 Nginx 不知道,就会用到已关闭的连接。
  • keepalive_time 1h:单条连接最长存活时间。防止长期连接累积状态问题。

10.3 版本新增:keepalive ... local

1.29.7 给 keepalive 加了可选参数 local:

   nginx
keepalive 64 local;

作用:优先复用本 worker 自己创建的连接,减少跨 worker 的锁竞争。在高并发下能降低 keepalive 连接池的锁开销。

适用条件:只在明确观察到锁竞争(perf 显示大量 ngx_shmtx_lock 自旋)时才需要。普通场景收益不明显,不要盲目加。

10.4 proxy 块的完整配置

   nginx
location /api/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;

    # Host 头:决定后端看到什么 Host
    proxy_set_header Host $http_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_set_header X-Request-ID $request_id;

    # 清空 Connection,让 Nginx 自己管理连接复用
    proxy_set_header Connection "";

    # 关闭 buffering?先别急着关,见下文
    proxy_buffering on;
    proxy_buffers 8 8k;
    proxy_buffer_size 8k;
    proxy_busy_buffers_size 16k;

    # 超时
    proxy_connect_timeout 3s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;

    # 重试
    proxy_next_upstream error timeout http_502 http_503 http_504;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 5s;
}

逐项讲清楚为什么这么写:

proxy_set_header Host $http_host 还是 $host?

这是最常被忽略的区别:

  • $host:优先取请求行的 host,没有则取 Host 头,都没有则取 server_name。会被 server_name 兜底。
  • $http_host:原样取 Host 头。如果没有 Host 头则为空。
  • $proxy_host:取 proxy_pass 里的主机名(默认值)。

推荐 $http_host,因为后端通常要靠 Host 做多租户或路由,原样透传最安全。但如果客户端可能不发 Host 头(HTTP/1.0),$http_host 为空会出问题。折中方案:

   nginx
map $http_host $backend_host {
    ""      $host;
    default $http_host;
}
proxy_set_header Host $backend_host;

proxy_set_header Connection "" 会不会有问题?

在 1.29.7+ 默认就不发 Connection 头了,这行是幂等的。在 1.29.7 之前必须写,否则 Nginx 会把客户端的 Connection: close 原样转给后端,导致每个请求都新建连接。

proxy_buffering 该开还是关?

场景建议原因
普通 API,响应小(< 1MB)on(默认)后端能尽快释放 worker 线程,吞吐更高
SSE / 流式输出off必须实时推送
大文件下载(> 10MB)off 或调大 proxy_max_temp_file_size避免写临时文件;同时 proxy_max_temp_file_size 0 可禁用临时文件
WebSocketoff(其实 upgrade 后自动不缓冲)—

SSE 的正确配置:

   nginx
location /api/stream {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    proxy_buffering off;
    proxy_cache off;
    gzip off;                    # gzip 会攒数据,破坏流式
    chunked_transfer_encoding on;

    proxy_read_timeout 3600s;    # 长连接必须调大
    proxy_send_timeout 3600s;
    add_header X-Accel-Buffering no;
}

add_header X-Accel-Buffering no 是给后端看的:如果后端是 Node/Java,它可能自己设置了缓冲,这个头告诉它关掉。某些框架(如 Spring 的 ResponseBodyEmitter)会尊重这个头。

超时参数的语义:

参数含义默认调整依据
proxy_connect_timeout与后端建连超时60s必须缩短,2~5s。建连失败应该快速失败
proxy_send_timeout两次写操作之间的间隔60s后端处理慢但不读请求体时会触发
proxy_read_timeout两次读操作之间的间隔60s不等于总处理时间,是"多久没收到数据"
proxy_next_upstream_timeout重试的总时限0(不限)配合 tries 防止重试拖死请求

proxy_read_timeout 的误解最多。它不是"后端必须在这个时间内返回完整响应",而是"两次 recv() 之间的最大间隔"。一个 10 分钟才返回的报表查询,只要中间有数据(或连接没断),就不会超时。

重试配置的风险:

proxy_next_upstream 默认只对 error timeout 重试,不包含 http_502/503/504。加上之后要小心:

  • 只对幂等请求重试。GET/HEAD 可以,POST 不行——后端可能已经处理了。
  • Nginx 无法知道业务语义,所以正确的做法是在应用层用 $request_method 判断:

   nginx
map $request_method $can_retry {
    GET     1;
    HEAD    1;
    default 0;
}

upstream app_backend {
    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    # ... 
}

location /api/ {
    proxy_next_upstream error timeout;
    proxy_next_upstream_tries $can_retry;   # 非幂等请求不重试
}

proxy_next_upstream_tries 0 表示不重试。用 map 控制是个实用技巧。

10.5 验证连接复用是否生效

这是必须做的验证,否则你不知道配置有没有起作用。

   bash
# 方法一:看 Nginx 到后端的连接数(ESTAB 应该稳定而非持续增长)
ss -tn state established '( dport = :8080 )' | wc -l

# 方法二:连续发 100 个请求,看建连次数
# 在后端侧看 accept 计数增长,或者用 tcpdump 数 SYN
tcpdump -i any -c 200 -nn 'tcp[tcpflags] & tcp-syn != 0 and dst port 8080'

# 方法三:看 Nginx 的 stub_status,requests/handled 比值
curl -s http://127.0.0.1:8081/nginx_status

判断依据:

  • 100 个请求只看到个位数 SYN → 复用生效。
  • 100 个请求 100 个 SYN → 复用没生效。依次检查:proxy_http_version 是否为 1.1、Connection 头是否被清空、后端是否回 Connection: close。

10.6 一个真实对比

同一台机器、同 4 个后端,只改连接复用:

配置QPSP99 延迟后端 SYN/sNginx TIME_WAIT
proxy_http_version 1.0(1.29.7 前的默认)2800780ms11200无法建立(端口耗尽)
proxy_http_version 1.1 + Connection "",无 keepalive6200210ms2480035000
上一行 + keepalive 641450042ms数十数百

第三行的提升不是"调参调出来的",是消除了每个请求三次握手的固定成本。每个请求省下 0.1~0.3ms 的握手 + 内核协议栈开销,在 1 万 QPS 下就是每秒省下 1~3 秒的 CPU 时间。

 十一、实战四:超时体系与故障隔离 

11.1 三层超时怎么配

一个请求经过"客户端 → Nginx → 后端",超时必须层层递减,才能保证最外层先放弃:

   text
客户端超时(浏览器/SDK)  >  Nginx 超时  >  后端超时
     60s                     30s              20s

如果顺序反了(后端 60s,Nginx 30s):Nginx 30 秒断开,后端还在跑 30 秒,资源白白浪费,日志里只有 Nginx 的 504,后端却显示"成功完成"。

如果 Nginx 比客户端长(客户端 20s,Nginx 30s):客户端先断,Nginx 继续等 30 秒,占着连接和 buffer,还可能触发重试,放大后端压力。

11.2 完整的超时清单

   nginx
# 客户端侧
client_header_timeout   10s;    # 读完请求头的时间
client_body_timeout     30s;    # 读完请求体的时间(两次读之间)
send_timeout            30s;    # 向客户端写响应的间隔
keepalive_timeout       65s;    # 客户端连接空闲保持时间

# 后端侧
proxy_connect_timeout    3s;
proxy_send_timeout      30s;
proxy_read_timeout      30s;

# 上游连接相关的 TCP 参数
proxy_socket_keepalive  on;     # 开启 TCP keepalive 探测死连接

proxy_socket_keepalive on 值得单独说:它让 Nginx 到后端的连接开启 SO_KEEPALIVE。如果后端机器直接宕机(没有 FIN),Nginx 不会立刻发现,会一直复用一个已死的连接。开启后,内核会在探测失败后关闭连接,Nginx 下次取到就会失败并重试。代价是探测周期很长(默认 2 小时),所以主要靠 keepalive_timeout 和 max_fails 兜底,socket_keepalive 是补充。

keepalive_timeout 65s 为什么是 65 而不是 60:常见负载均衡器(AWS ALB、阿里 SLB)的空闲超时是 60 秒。Nginx 设 65 秒,让自己后关,避免在 SLB 已关闭的连接上发请求导致偶发 502。这是一个实践经验,不是硬性规则。

11.3 缓冲与限速的组合

   nginx
# 防慢客户端(Slowloris)
client_header_timeout 10s;
client_body_timeout 20s;
client_max_body_size 50m;
client_body_buffer_size 128k;

# 大上传走磁盘缓冲
client_body_temp_path /var/cache/nginx/client_temp 1 2;

# 限制单连接带宽,防拖死(单位 bytes/s,0 为不限)
# limit_rate 0;

慢客户端是真实威胁:一个客户端用 10 字节/秒读取响应,Nginx 的连接、buffer 全被占用。send_timeout 只在"完全不动"时触发,慢速读取不触发。这时需要 limit_rate 或 limit_conn 限制。

   nginx
# 按 IP 限制并发连接数
limit_conn_zone $binary_remote_addr zone=conn_perip:10m;
limit_conn_status 429;
limit_conn_log_level warn;

server {
    limit_conn conn_perip 20;
    # ...
}

注意 limit_conn 的语义:它限制的是同一时刻的并发连接数,不是请求速率。下载大文件的用户会长时间占用连接,容易被误伤。必须结合 limit_conn_dry_run on(1.17.6+,开源版可用)先观察:

   nginx
limit_conn_dry_run on;    # 只记录日志不实际拒绝

日志里会出现 limiting connections by zone "conn_perip", dry run,用这个先跑一周,确认阈值合理再关掉 dry-run。这是开源版上很有价值的功能,很多人不知道。

11.4 后端健康与熔断

开源版只有被动健康检查,靠 max_fails / fail_timeout 实现:

   nginx
upstream app_backend {
    zone app_backend 64k;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=10s backup;
}

语义:在 fail_timeout(10 秒)内失败 max_fails(3 次),则该 server 在接下来的 fail_timeout 内被标记为不可用,不再分配请求。

几个坑:

  • max_fails=0 表示禁用失败计数,不是"永不失败"。写反了会导致故障节点永远被选中。
  • 只有网络错误和超时算失败,http_502 等不算(除非配了 proxy_next_upstream http_502)。
  • 没有 zone 时每个 worker 独立计数,8 个 worker 意味着实际失败阈值是 3 × 8 = 24 次。
  • 故障恢复是自动的:fail_timeout 过后会自动重新分配请求试探,不需要人工干预。但试探失败会再次进入不可用。

灰度摘流的标准做法:不要依赖 max_fails 摘节点(太慢,要等 10 秒以上)。正确方式是改配置 reload,把 server 标记为 down:

   nginx
server 10.0.1.11:8080 down;   # 立即不分配流量,但保留在列表里

reload 是平滑的,down 的节点不再收到新请求,已有请求正常完成。这比直接从列表里删掉更安全(删掉会让 keepalive 池里的连接被直接清理)。

11.5 502 / 504 / 499 的判断路径

这三个状态码在 Nginx 侧的含义完全不同:

状态码谁产生的含义首要排查
502Nginx后端返回无效响应 / 连接被拒 / 连接被重置后端是否在监听、后端是否提前关闭连接
504Nginx后端在 proxy_read_timeout 内没有响应后端处理慢、后端线程池满、后端等锁
499Nginx客户端提前关闭连接客户端超时设置、用户主动取消

502 的细分:

   bash
# error.log 里的关键行
grep -E 'upstream sent|connect\(\) failed|recv\(\) failed|upstream prematurely' /var/log/nginx/error.log | tail -20

  • connect() failed (111: Connection refused) while connecting to upstream → 后端没监听或端口错。查 ss -lntp | grep 8080。
  • upstream prematurely closed connection while reading response header → 后端接受了连接又立刻关闭。常见于后端线程池满、后端 keepalive_timeout 比 Nginx 短、后端崩溃重启。
  • upstream sent invalid header → 后端返回了畸形响应(比如返回了纯二进制、或者 HTTP 版本不对)。
  • no live upstreams while connecting to upstream → 所有后端都被标记失败。这是故障扩大的信号,要立即介入。

504 的判断:

   bash
# 对比 Nginx 记录的 upstream 响应时间
awk '$0 ~ /urt=/ {print}' /var/log/nginx/access.log | tail -100 | grep -o 'urt=[0-9.]*' | sort -t= -k2 -n | tail -10

如果 urt 接近 proxy_read_timeout,说明后端确实超时。如果 urt=-,说明根本没连上(那是 502 的范畴)。

499 的判断:

499 大量出现,说明客户端等不及了。两个方向:

  1. 客户端超时设得太短(比如 SDK 默认 3 秒)。
  2. Nginx 或后端确实慢,客户端放弃。此时看 499 请求的 $request_time 分布——如果集中在某个值附近,就是客户端超时。

499 有个副作用:客户端断开后,Nginx 默认会继续从后端读完响应再关闭(避免脏连接进入 keepalive 池)。这会浪费后端资源。可以用:

   nginx
proxy_ignore_client_abort on;   # 默认 off,客户端断开后继续读完后端响应

保持默认 off 是对的:客户端断开 Nginx 就停止读,后端写入会失败并释放线程,反而更快。设为 on 会让后端白干。

 十二、实战五:限流与防雪崩 

12.1 limit_req:漏桶算法

   nginx
# $binary_remote_addr 比 $remote_addr 省一半内存(4 字节 vs 7~15 字节)
limit_req_zone $binary_remote_addr zone=api_perip:10m rate=20r/s;
limit_req_zone $server_name          zone=api_total:10m rate=2000r/s;

limit_req_status 429;
limit_req_log_level warn;

location /api/ {
    # burst=40:允许突发 40 个请求排队;nodelay:排队的不延迟处理
    limit_req zone=api_perip burst=40 nodelay;
    limit_req zone=api_total  burst=200;
    # ...
}

rate 的单位:r/s 或 r/m。rate=20r/s 表示平均每秒 20 个请求,Nginx 内部按毫秒精度换算(每 50ms 放行一个)。

burst 与 nodelay 的语义:

  • 不带 nodelay:超出的请求排队,按 rate 的节奏逐个放行,响应会变慢但不报错。适合"宁可慢也不要失败"的场景。
  • 带 nodelay:突发额度内的请求立即处理,超出 burst 的直接 429。适合 API 场景。

burst 大小的取值依据:burst = 允许的瞬时峰值 - rate × 处理时长。比如页面加载会同时发 30 个请求,rate=20r/s,那么 burst 至少要 30 才不会误伤首页加载。

多级限流的顺序:Nginx 会按配置顺序检查所有 limit_req,任何一个超限就拒绝。所以先配粗粒度(按 IP),再配细粒度(按接口) 会先拒绝掉异常 IP,减少后续判断。

12.2 按 URL 差异化限流

   nginx
# 登录接口严格限流,防撞库
map $uri $login_key {
    ~^/api/login     $binary_remote_addr;
    default          "";
}

limit_req_zone $login_key zone=login:10m rate=1r/s;

location = /api/login {
    limit_req zone=login burst=5 nodelay;
    proxy_pass http://app_backend;
}

关键技巧:map 的 default 返回空字符串时,limit_req_zone不会对该请求计数(key 为空则跳过)。这样就能做"只对特定路径限流"。

注意:1.17.0 起 limit_req 支持在 location 里用变量控制是否启用:

   nginx
set $limit_key $binary_remote_addr;
if ($uri !~ ^/api/login) {
    set $limit_key "";   # 空 key 不计数
}

但 if 有副作用,优先用 map。

12.3 全局限流(保护后端)

按 IP 限流保护不了"大量不同 IP 同时打一个热点接口"。需要按 server 或按 upstream 限流:

   nginx
# 按虚拟主机总速率限制
limit_req_zone $server_name zone=per_vhost:10m rate=5000r/s;

server {
    server_name api.example.com;
    limit_req zone=per_vhost burst=1000 nodelay;
}

更好的方案:结合 $upstream_addr 做"按后端实例限流",但这需要 Lua 或者 Plus 的 limit_req_zone 扩展。开源版只能用 $server_name 或自定义变量(比如从请求头取)。

   nginx
# 从请求头取租户 ID,按租户限流
map $http_x_tenant_id $tenant_key {
    ""      "";
    default $http_x_tenant_id;
}
limit_req_zone $tenant_key zone=per_tenant:10m rate=100r/s;

安全提示:$http_x_tenant_id 是客户端可控的。如果前面的网关或鉴权层已经清洗过这个头(覆盖为可信值),才可以用。否则攻击者随便换头就绕过限流。

12.4 限流的效果验证

   bash
# 用 ab 或 wrk 压测,观察 429 比例
wrk -t4 -c100 -d10s --latency http://127.0.0.1/api/test

# 单独数 429
awk '$9 == 429 {c++} END {print c+0}' /var/log/nginx/access.log

预期:rate=20r/s + burst=40,100 并发压 10 秒,前面的 40 个突发请求通过,之后每秒约 20 个通过,其余 429。

异常情况:

  • 全部 429:rate 单位写错(rate=20 会被当成 20 r/s,但如果写成 rate=20r/m 就是每分钟 20 个)。
  • 完全不限流:limit_req_zone 的 key 变量为空(比如 $binary_remote_addr 在 stream 模块里不可用)。验证方法:

   bash
# 看 zone 状态需要商业版 API,开源版只能靠行为判断
# 检查配置是否被加载
nginx -T | grep limit_req

12.5 熔断的替代方案

开源版没有真正的熔断器。能做的近似方案:

  1. max_fails 快速摘除:max_fails=2 fail_timeout=30s,配合 proxy_next_upstream http_503。
  2. error_page 兜底静态页:

   nginx
proxy_intercept_errors on;
error_page 502 503 504 = @fallback;

location @fallback {
    internal;
    # 注意:return 的 content-type 默认是 text/plain,要改
    default_type application/json;
    return 503 '{"code":503,"msg":"service temporarily unavailable"}';
}

default_type 必须写在 return 之前才生效——Nginx 按书写顺序执行指令,return 会立即结束该阶段的处理,写在它后面的 default_type 不会被执行。所以正确顺序是:

   nginx
location @fallback {
    internal;
    default_type application/json;
    return 503 '{"code":503,"msg":"service temporarily unavailable"}';
}

  1. 上游主动降级:在 location 里用 if 判断时间窗口,或者用 ngx_http_geo_module 按 IP 段做灰度降级。

不要用 if 做复杂逻辑。Nginx 的 if 在 location 内是"伪指令",官方文档明确写了"if is evil"。能用 map 就用 map,能用多 location 就用多 location。

 十三、实战六:proxy_cache 与缓存策略 

13.1 缓存的基础配置

   nginx
# 定义缓存区(http 块)
proxy_cache_path /data/nginx/cache
                 levels=1:2
                 keys_zone=api_cache:100m
                 max_size=10g
                 inactive=60m
                 use_temp_path=off;      # 1.7.10+,避免临时目录跨文件系统拷贝

# 定义缓存 key
proxy_cache_key "$scheme$request_method$host$request_uri";

参数含义与取值依据:

参数含义取值依据
levels=1:2两级目录,第一级 1 个字符,第二级 2 个每个目录下文件数控制在几百以内,避免单目录 inode 过多
keys_zone=api_cache:100m元信息共享内存100MB ≈ 80 万条缓存 key
max_size=10g磁盘上限按磁盘容量和预期命中率定
inactive=60m60 分钟未访问则删除与业务的内容更新频率匹配
use_temp_path=off临时文件和缓存文件同目录避免跨设备 rename 失败(必须 EXDEV)

use_temp_path=off 是个容易忽略的坑:如果 proxy_temp_path 和 proxy_cache_path 在不同文件系统,Nginx 会用"拷贝 + 删除"代替 rename,大文件下性能暴跌。

13.2 在 location 里启用缓存

   nginx
location /api/products/ {
    proxy_pass http://app_backend;

    proxy_cache api_cache;
    proxy_cache_valid 200 301 302 10m;    # 成功响应缓存 10 分钟
    proxy_cache_valid 404 1m;             # 404 缓存 1 分钟,避免穿透
    proxy_cache_valid any 1m;

    proxy_cache_methods GET HEAD;         # POST 默认不缓存
    proxy_cache_min_uses 1;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_background_update on;     # 后台异步更新过期缓存
    proxy_cache_lock on;                  # 同一 key 并发请求只有一个回源
    proxy_cache_lock_timeout 5s;

    # 缓存状态写入响应头,便于排查
    add_header X-Cache-Status $upstream_cache_status always;
}

proxy_cache_use_stale 是缓存最重要的价值:后端挂了,还能用旧缓存顶住。

  • error timeout:连不上或超时。
  • updating:缓存正在后台更新时,返回旧的。
  • http_502/503/504:后端返回错误时用旧的。

注意:use_stale 返回的旧响应状态码仍是原始状态码(比如 200),不是 5xx。这对用户体验是好事,但要注意它可能返回过期很久的数据。配合 proxy_cache_background_update on 使用,能保证"先返回旧数据,后台刷新"。

proxy_cache_lock 的必要性:缓存过期瞬间,100 个并发请求同时回源,后端被打穿(缓存击穿)。proxy_cache_lock on 让只有一个请求回源,其余等待。proxy_cache_lock_timeout 5s 后未拿到结果的不再等,自己回源(防止长时间阻塞)。

13.3 缓存 key 的设计

   nginx
# 按设备类型区分缓存
proxy_cache_key "$scheme$host$request_uri$http_accept_encoding";

# 按语言区分
map $http_accept_language $lang_key {
    ~*zh  zh;
    ~*en  en;
    default zh;
}
proxy_cache_key "$scheme$host$request_uri$lang_key";

不要做的:

  • 把 $cookie_* 放进 key。Cookie 值千变万化,缓存命中率接近 0,还会撑爆 keys_zone。
  • 把 $remote_addr 放进 key。除非确实需要按 IP 缓存,否则等于没缓存。
  • 缓存带 Set-Cookie 的响应。默认 proxy_cache 不缓存带 Set-Cookie 的响应(这是正确的),如果强行缓存要用 proxy_ignore_headers Set-Cookie,这会把 A 用户的会话 cookie 给 B 用户,是严重的安全事故。

13.4 缓存的验证与清理

   bash
# 验证缓存是否生效:连续两次请求,看 X-Cache-Status
curl -sI http://127.0.0.1/api/products/1 | grep -i x-cache-status
# 第一次:MISS,第二次:HIT

$upstream_cache_status 的可能值:

值含义是否需要关注
MISS未命中,回源首次请求正常
HIT命中正常
EXPIRED缓存过期,回源正常
STALE用了过期缓存(use_stale 生效)后端有问题,需告警
UPDATING缓存正在后台更新正常
REVALIDATED用 If-Modified 验证后仍有效正常
BYPASS被 proxy_cache_bypass 跳过看业务预期

清理缓存:开源版没有清缓存的 API。三种方式:

  1. 删除文件 + reload:rm -rf /data/nginx/cache/* 然后 nginx -s reload。注意:只删文件不 reload,Nginx 的共享内存里还有索引,可能返回 404 或乱码。
  2. proxy_cache_purge:需要第三方模块 ngx_cache_purge,官方包不带。
  3. 版本化 URL:最推荐。/api/products/1?v=2,key 变了自然穿透。不需要清缓存。

方式 1 的风险:缓存文件量大的时候,rm -rf 会产生大量 IO,且 Nginx 在缓存区被清空期间的所有请求都会回源,可能打挂后端。应该在低峰期做,或者分批删。

 十四、实战七:TLS 与 HTTP/2、HTTP/3 

14.1 现代 TLS 配置

   nginx
server {
    listen 443 ssl;
    http2 on;                    # 1.25.1 起用独立指令,旧的 listen ... http2 已废弃
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # 协议版本:TLS 1.2 起步,1.3 优先
    ssl_protocols TLSv1.2 TLSv1.3;

    # 密码套件:TLS 1.3 的套件由 OpenSSL 固定,这里的配置只影响 1.2
    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_ciphers off;   # TLS 1.3 时代应设 off,让客户端决定

    # 会话复用:显著降低握手开销
    ssl_session_cache shared:SSL:50m;   # 50m ≈ 20 万个会话
    ssl_session_timeout 1d;
    ssl_session_tickets on;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/example.com.chain.pem;
    resolver 223.5.5.5 119.29.29.29 valid=300s;
    resolver_timeout 5s;

    # 安全头
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

几个关键点说明:

ssl_prefer_server_ciphers off:这是 1.11.0 起的默认值。老教程写 on 是为了防止客户端选弱套件,但在 TLS 1.3 + AEAD 套件普及后,on 反而会让性能更好的客户端优先套件失效。

ssl_session_cache shared:SSL:50m:不加这个,默认是 none(1.23.4 起默认 shared:SSL:10m,但显式声明更可控)。每个会话约 256 字节,50m 能存约 20 万个。多 worker 必须用 shared:,否则每个 worker 独立缓存,命中率除以 worker 数。

OCSP Stapling 的验证:

   bash
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -status 2>/dev/null | grep -A3 'OCSP Response Status'

预期输出:

   text
OCSP response: 
======================================
OCSP Response Data:
    OCSP Response Status: successful (0x0)

如果显示 no response sent,说明 stapling 没工作。常见原因:resolver 没配(Nginx 需要自己解析 OCSP 服务器地址)、证书链不完整(ssl_trusted_certificate 没配或者配错了文件)。

注意:OCSP Stapling 的价值在下降。Let’s Encrypt 已经宣布停止 OCSP 服务(2025 年起),改用 CRL。如果证书来自 LE,配置 stapling 没有意义,还可能因为 OCSP 服务器不可达而拖慢握手。

http2 on 的版本差异:

  • 1.25.1 之前:listen 443 ssl http2;
  • 1.25.1 起:listen 443 ssl; + http2 on;
  • 1.25.1 起用旧写法会报 the "listen ... http2" directive is deprecated 警告。

HTTP/3 的配置(需要编译 --with-http_v3_module,OpenSSL 3.2+ 或 quictls):

   nginx
listen 443 quic reuseport;
listen 443 ssl;
http2 on;

# 告诉浏览器可以用 h3
add_header Alt-Svc 'h3=":443"; ma=86400' always;

ssl_early_data on;    # 0-RTT,注意重放风险

ssl_early_data on 有重放风险:0-RTT 数据可能被攻击者截获重放。只对幂等请求(GET)开放,或者在应用层做 nonce 校验。对 POST 接口开 early_data 是危险的。

14.2 HTTP/2 的调优与陷阱

   nginx
# 每连接并发流(默认 128)
http2_max_concurrent_streams 128;

# 1.29.x 起默认行为变化:HTTP/2 支持 RFC 9218 优先级(1.31.7 完整实现)
# 老版本的 http2_push 已被移除(1.25.1 起)

HTTP/2 的常见误区:

误区一:上了 HTTP/2 就快了。 对高延迟链路(移动网络、跨国)收益明显,因为多路复用省掉了队头阻塞和多次握手。但对低延迟内网,收益很小。

误区二:HTTP/2 一定要开 server push。http2_push 已在 1.25.1 被移除,因为实际效果差(浏览器缓存了还会推,浪费带宽)。现在用 103 Early Hints 替代:

   nginx
# 1.29.0 起开源可用
early_hints "Link" "</app.css>; rel=preload; as=style";

误区三:HTTP/2 不用管连接数。 实际上 HTTP/2 把"每域名 6 个连接"的限制打开了,浏览器可能一个域名就用几十个连接。http2_max_concurrent_streams 设得太大(默认 128)会让单个连接就能压满后端线程池。

14.3 TLS 的性能开销

TLS 握手(尤其是完整握手)是 CPU 密集的。RSA 2048 的完整握手约消耗 1ms CPU,ECDSA P-256 约 0.3ms。10 万次/秒的新建连接就是 100 秒 CPU 时间。

优化手段按收益排序:

  1. 会话复用(ssl_session_cache + ssl_session_tickets):复用率 80% 以上时收益最大。
  2. 换 ECDSA 证书:比 RSA 快 3 倍,但老客户端(Windows 7 及以下)不支持。可以用双证书(RSA + ECDSA)兼顾。

   nginx
ssl_certificate     /etc/nginx/ssl/example.com.ecdsa.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.ecdsa.key;
ssl_certificate     /etc/nginx/ssl/example.com.rsa.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.rsa.key;

Nginx 会按客户端能力自动选择。

  1. TLS 1.3:1-RTT 握手(会话复用时 0-RTT),比 1.2 少一个往返。
  2. 启用 TLS 卸载卡 / QAT:大规模场景才需要。

验证 TLS 版本分布:

   nginx
# 在日志里记录协议和套件
log_format tls '$remote_addr [$time_local] "$request" $status '
               'proto=$server_protocol ssl=$ssl_protocol cipher=$ssl_cipher '
               'sni=$ssl_server_name sess_reused=$ssl_session_reused';

$ssl_session_reused 为 r 表示复用,. 表示新建。统计复用率:

   bash
awk '{for(i=1;i<=NF;i++) if($i ~ /^sess_reused=/) print $i}' /var/log/nginx/tls.log | sort | uniq -c

判断依据:复用率低于 60% 要查原因(会话缓存太小、客户端不支持、中间设备剥离)。

 十五、实战八:日志与可观测性 

15.1 日志字段的选择

日志不是越详细越好——每增加一个变量,Nginx 就要多做一次取值和格式化,高 QPS 下是可观的 CPU 开销。

最小可用集(约 10 个字段,覆盖 90% 排查场景):

   nginx
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 ustat=$upstream_status';

排障增强集(+5 个字段,用于链路追踪):

   nginx
log_format trace '$remote_addr - [$time_local] "$request" $status '
                 '$body_bytes_sent rt=$request_time '
                 'urt=$upstream_response_time uaddr=$upstream_addr ustat=$upstream_status '
                 'rid=$request_id host=$host scheme=$scheme '
                 'ct=$content_type cs=$upstream_cache_status';

几个重要变量的说明:

变量含义注意
$request_time从读首字节到发完响应的总时间(秒,毫秒精度)含客户端网络时间
$upstream_response_time后端响应时间,多次尝试用逗号分隔不能直接当数字用
$upstream_addr后端地址,多次尝试用逗号分隔与上面对齐
$upstream_status后端状态码- 表示没连上
$request_id自动生成的 16 进制 ID不写入日志就白生成了
$upstream_bytes_received从后端收到的字节数排查"后端返回巨大响应"
$connection_requests当前连接上的第几个请求判断 keepalive 效果

$request_id 的正确用法:它需要在 proxy_set_header 里传给后端,后端再打到自己的日志里,才能做全链路串联:

   nginx
proxy_set_header X-Request-ID $request_id;

如果客户端自己带了 X-Request-ID(比如移动端生成的),要优先用客户端的:

   nginx
map $http_x_request_id $rid {
    ""      $request_id;
    default $http_x_request_id;
}
proxy_set_header X-Request-ID $rid;

安全提示:透传客户端提供的 X-Request-ID 前要校验格式(长度、字符集),否则会有人往日志里注入换行符,制造假日志。

15.2 用 awk 做日志分析

不装 ELK 也能做基础分析。以下都是可直接运行的命令。

   bash
# TOP 10 请求量最大的 URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# TOP 10 响应时间最长的请求(需要日志里有 rt=)
grep -o 'rt=[0-9.]*' /var/log/nginx/access.log | cut -d= -f2 | sort -rn | head -10

# P50 / P95 / P99 响应时间
grep -o 'rt=[0-9.]*' /var/log/nginx/access.log | cut -d= -f2 | sort -n | \
  awk '{a[NR]=$1} END {print "P50="a[int(NR*0.5)], "P95="a[int(NR*0.95)], "P99="a[int(NR*0.99)], "count="NR}'

# 状态码分布
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# 5xx 的 TOP URL
awk '$9 ~ /^5/ {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# 按分钟统计 QPS
awk -F'[][]' '{print $2}' /var/log/nginx/access.log | cut -d: -f1-3 | uniq -c | sort -k2

# 上游响应时间 TOP(注意处理逗号分隔的多次尝试)
grep -o 'urt=[0-9.,]*' /var/log/nginx/access.log | \
  awk -F= '{n=split($2,a,","); s=0; for(i=1;i<=n;i++) s+=a[i]; print s}' | sort -rn | head -10

awk -F'[][]' 的用法:Nginx 日志的时间格式是 [10/Oct/2026:14:23:45 +0800],用方括号做分隔符,$2 就是时间部分。这个技巧比正则快很多。

注意日志格式和 awk 字段的对应关系:上面命令里 $7 是 URL、$9 是状态码,这只对 combined 格式成立。如果你自定义了格式,字段位置会变。生产环境建议保持标准格式,或者用 awk 按 $i 定位前先 head -1 确认。

15.3 stub_status 之外的指标

Nginx 开源版没有 Prometheus 端点。要接入监控,有两个方案:

方案一:nginx-prometheus-exporter(官方推荐)

   bash
# 需要先启用 stub_status
curl -s http://127.0.0.1:8081/nginx_status

# 启动 exporter
docker run -d --name nginx-exporter -p 9113:9113 \
  nginx/nginx-prometheus-exporter:1.4.0 \
  --nginx.scrape-uri=http://192.168.1.10:8081/nginx_status

注意:stub_status 只提供 7 个指标(active/accepts/handled/requests/reading/writing/waiting)。想拿到 upstream 的响应时间、缓存命中率,必须靠日志,或者用 nginx-module-vts 这类第三方模块。

方案二:日志转指标

用 mtail 或 grok_exporter 从日志提取指标:

   text
# mtail 规则示例:/etc/mtail/nginx.mtail
counter nginx_requests_total by status
counter nginx_request_duration_seconds

/^(\S+) - \S+ \[[^\]]+\] "(\S+) (\S+) [^"]*" (\d{3}) .* rt=([0-9.]+)/ {
    nginx_requests_total[$4]++
    nginx_request_duration_seconds += $5
}

关键指标与告警建议:

指标来源告警阈值建议
5xx 比例日志> 1% 持续 5 分钟
P99 延迟日志 rt> 基线 2 倍持续 10 分钟
accepts - handledstub_status> 0 且持续增长(fd 不够)
waiting / activestub_status< 20%(连接复用失效)
499 比例日志> 5%(客户端放弃)
X-Cache-Status: STALE日志出现即告警(后端故障)
上游 connect() failederror.log出现即告警

阈值必须结合业务基线。上面给的是通用起始值,上线后要按自己的流量特征调整。比如一个只有 10 QPS 的内部系统,5xx 比例 1% 就是每分钟 6 个错误,可能完全不可接受。

 十六、故障排查:六个典型场景的闭环 

16.1 场景一:QPS 上不去,CPU 不高

现象:压测 3000 QPS 时延迟从 30ms 涨到 800ms,Nginx 和后端 CPU 都不高。

初判:CPU 不高说明不是计算瓶颈,而是等待。等待的位置可能是:TCP 建连、后端线程池、锁。

检查:

   bash
# ① 连接复用是否生效
curl -s http://127.0.0.1:8081/nginx_status
# 看 requests / handled 的比值,接近 1 说明没复用

# ② TIME_WAIT 数量
ss -s | grep -i timewait

# ③ Nginx 到后端的连接数
ss -tn state established '( dport = :8080 )' | wc -l

# ④ 后端 accept 队列是否溢出
nstat -az TcpExtListenOverflows TcpExtListenDrops

指标判读:

  • TcpExtListenOverflows 增长 → 后端 accept 队列满,后端处理不过来或 somaxconn 太小。
  • TIME_WAIT 十万以上 → 没复用连接。
  • established 到后端的连接数 = QPS × 平均响应时间,如果远大于这个值,说明连接泄漏。

根因:proxy_http_version 为 1.0(或没设 keepalive),每个请求新建 TCP 连接,端口和 CPU 都消耗在建连上。

修复:按第十节配置 proxy_http_version 1.1 + keepalive 64 + Connection ""。

验证:重压后 requests / handled 应上升到 10 以上,TIME_WAIT 降到几千。

回滚:把 keepalive 去掉即可,无状态变更。

复盘:压测前先确认连接复用,不要等上线才发现。

16.2 场景二:502 间歇性出现

现象:每小时几十个 502,不成片,随机分布。

初判:间歇性说明不是后端完全挂掉(那会全部 502),而是特定条件下的连接失败。

检查:

   bash
# 看 error.log 的具体错误信息
grep -E 'upstream (prematurely|sent)' /var/log/nginx/error.log | tail -50

# 统计各后端被 502 的次数
awk '$9 == 502 {print $9}' /var/log/nginx/access.log | wc -l
grep -o 'uaddr=[0-9.:]*' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

典型错误信息与根因:

error.log 内容根因处理
upstream prematurely closed connection while reading response header后端 keepalive_timeout 比 Nginx 短,或后端线程池满主动关闭调大后端 keepalive;调小 Nginx upstream keepalive_timeout
connect() failed (111: Connection refused)后端进程挂了或端口不对查后端进程;查 ss -lntp
upstream sent too big header后端响应头超过 proxy_buffer_size调大 proxy_buffer_size 16k;
no live upstreams所有后端都被 max_fails 摘除立即介入,看后端健康
recv() failed (104: Connection reset by peer)后端进程崩溃、被 OOM 杀、或 SO_LINGER 强关查后端日志和 dmesg

最典型的一个:upstream prematurely closed connection

根因几乎总是"后端先关连接"。Tomcat 的 maxKeepAliveRequests 默认 100,处理 100 个请求后主动关闭。而 Nginx 不知道,从池里取出这条连接发请求,写入成功但读不到响应 → 502 → 重试。

修复(两侧都要改):

   nginx
# Nginx 侧:让 Nginx 先关,而不是让后端关
upstream app_backend {
    keepalive 64;
    keepalive_requests 90;      # 小于后端的 100,Nginx 主动关
    keepalive_timeout 50s;      # 小于后端的 idle timeout
}
   properties
# Tomcat 侧:server.xml 或 application.properties
server.tomcat.max-keep-alive-requests=1000
server.tomcat.connection-timeout=20000

验证:grep -c 'prematurely closed' /var/log/nginx/error.log 在修复后应停止增长。

回滚:改回原值,reload。

16.3 场景三:504 集中出现

现象:某个接口在高峰期大量 504,urt 接近 proxy_read_timeout。

初判:后端处理慢,不是连接问题。

检查:

   bash
# 找出 504 的 URL
awk '$9 == 504 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

# 看这些请求的 upstream 响应时间
grep 'ustat=504' /var/log/nginx/access.log | grep -o 'urt=[0-9.,]*' | sort | uniq -c | sort -rn | head

# 后端线程池状态(Java 场景)
curl -s http://10.0.1.11:8080/actuator/metrics/tomcat.threads.busy

判断依据:

  • 所有后端都 504 → 共同依赖(数据库、缓存、下游服务)有问题。
  • 只有某台后端 504 → 该实例有问题(GC、线程池配置小、机器负载高)。
  • urt 恰好等于 proxy_read_timeout → 纯粹的等待超时。

修复方向(按优先级):

  1. 先止血:调大 proxy_read_timeout(治标,但能减少用户可见错误)。注意:调大会让慢请求占用连接更久,可能引发连锁。
  2. 限流保护:对慢接口单独限流,防止打满线程池。
  3. 查根因:后端慢的原因(SQL、锁、GC、下游超时)另案处理。

不要做的:不要一上来就调大 proxy_read_timeout 到 300s。这会把 504 变成"用户等 5 分钟",体验更差,而且连接和内存占用翻几倍。

16.4 场景四:上传大文件失败

现象:前端上传 100MB 文件,报 413 或 502。

413 Request Entity Too Large:client_max_body_size 太小(默认 1m)。

   nginx
client_max_body_size 100m;

502 出现在上传过程中:通常是 client_body_buffer_size 太小导致落盘,而 client_body_temp_path 磁盘满了:

   bash
df -h /var/cache/nginx/client_temp
ls /var/cache/nginx/client_temp | wc -l
grep 'No space left' /var/log/nginx/error.log

修复:把 client_body_temp_path 指到大磁盘,并清理残留(Nginx 异常退出时会留下临时文件)。

上传慢:client_body_timeout 默认 60s,是"两次读之间的间隔"。慢速网络上传大文件,只要不断流就不会超时。但如果中间有停顿超过 60s,会报 client intended to send too large body 或直接断开。

正确的上传配置:

   nginx
location /upload/ {
    client_max_body_size 200m;
    client_body_buffer_size 1m;
    client_body_temp_path /data/nginx/client_temp 1 2;
    client_body_timeout 300s;

    proxy_pass http://app_backend;
    proxy_request_buffering off;      # 边收边发,不落盘,适合大文件
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

proxy_request_buffering off 让 Nginx 把请求体边收边转发给后端,省掉"完整落盘再读出来"的一轮磁盘 IO。代价:后端要能处理慢速上传(后端自己的读超时要够);如果需要重试,请求体已经发出去了一部分,无法重放。所以 proxy_request_buffering off 与 proxy_next_upstream 是有冲突的,开了 buffering off 就不要指望重试。

16.5 场景五:reload 后配置"不生效"

现象:改了配置、nginx -s reload 也执行了,行为没变。

检查步骤:

   bash
# ① reload 是否真的成功
nginx -t
# 语法有错时 reload 会失败,但错误可能只在 error.log 里

tail -20 /var/log/nginx/error.log

# ② 实际生效的配置是什么(含 include)
nginx -T | grep -A10 'server_name api.example.com'

# ③ worker 是否重启了(PID 变了)
ps -eo pid,lstart,cmd | grep '[n]ginx: worker'

常见原因:

原因判断方法
配置文件有 include,改的文件没被包含nginx -T 里搜不到你改的内容
存在多个 nginx.conf(比如 /etc/nginx/nginx.conf 和 /usr/local/nginx/conf/nginx.conf)ps -ef | grep nginx 看 -c 参数
server_name 没匹配上,走了默认 server日志里 host= 字段确认
location 优先级搞错(= > ^~ > 正则 > 前缀)nginx -T 看所有匹配的 location
浏览器/客户端缓存用 curl -H 'Cache-Control: no-cache' 验证
reload 时旧 worker 还在处理旧配置的连接ps 里看是否有多个 worker

location 匹配优先级(必须记牢):

   text
1. = /exact/path         精确匹配,最高优先级
2. ^~ /prefix/           前缀匹配且不再检查正则
3. ~ regex               正则匹配(区分大小写),按出现顺序
4. ~* regex              正则匹配(不区分大小写),按出现顺序
5. /prefix/              普通前缀匹配,取最长匹配

最容易踩的坑:正则 location 的顺序很重要。location ~ \.php$ 写在 location ~ /api/ 前面,会导致 /api/x.php 走 PHP 处理而不是 API 代理。

16.6 场景六:内存持续增长

现象:Nginx worker 的 RSS 从 30MB 涨到 2GB,不回落。

检查:

   bash
# 各 worker 的 RSS
ps -o pid,rss,cmd -p $(pgrep -P $(cat /run/nginx.pid) | tr '\n' ',' | sed 's/,$//')

# 看是否有大量活跃连接
curl -s http://127.0.0.1:8081/nginx_status

# 看是否有大响应的请求正在处理
ss -tn state established '( sport = :80 )' | wc -l

可能原因与判断:

原因判断依据处理
proxy_buffering on + 大响应 + 高并发proxy_buffers 配置过大,活跃连接数高调小 buffer,或对大文件 proxy_buffering off
proxy_cache 的 keys_zone 太大共享内存在启动时就分配,不算 RSS 增长正常现象,不用处理
内存碎片RSS 涨但 smaps 显示实际使用不多定期 reload(新 worker 从干净状态开始)
连接泄漏active 持续增长不降查 keepalive 配置、客户端行为
第三方模块泄漏只在特定模块启用后出现逐个禁用排查

判断内存是否"真"在用:

   bash
grep -E 'Rss|Pss' /proc/<worker_pid>/smaps_rollup

Pss 比 Rss 更准(共享内存按比例分摊)。如果 Pss 远小于 Rss,说明大部分是共享内存(正常)。

reload 是有效的缓解手段:新 worker 从干净状态开始,旧 worker 处理完现有请求后退出。但这是"缓解"不是"解决",根本原因还要查。频繁 reload 本身有成本(连接重置、缓存冷启动),不能当成常态方案。

 十七、常用命令速查与判断依据 

这一节把前面散落的命令集中起来,按"我要干什么"组织,每条都给出判断依据。

17.1 配置检查与生效

   bash
# 语法检查(不改任何东西)
nginx -t

# 输出完整生效配置(含 include 展开)
nginx -T

# 只输出某个 server 块的配置
nginx -T | awk '/server_name api.example.com/,/^}/'

# 平滑重载(不断已有连接)
nginx -s reload

# 优雅停止(等现有请求处理完)
nginx -s quit

# 立即停止(中断所有连接,危险)
nginx -s stop

# 重新打开日志文件(logrotate 用)
nginx -s reopen

# 查看编译参数
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--'

# 查看已加载的动态模块
nginx -m

判断依据:nginx -t 返回 0 且输出 syntax is ok / test is successful 才能 reload。有任何一个 emerg,reload 会失败且旧配置继续生效——这是好事(不会因为改错配置挂掉),但容易误以为"改了没生效"。

17.2 连接与状态

   bash
# 状态页
curl -s http://127.0.0.1:8081/nginx_status

# 当前连接总数(按状态)
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c

# 到某个端口的 ESTAB 连接数
ss -tn state established '( dport = :8080 )' | wc -l

# 按源 IP 统计连接数(找异常客户端)
ss -tn state established '( sport = :80 )' | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20

# TIME_WAIT 统计
ss -tan state time-wait | wc -l

# 端口范围使用情况(判断是否耗尽)
ss -tan | awk 'NR>1 {print $4}' | grep -o ':[0-9]*$' | cut -d: -f2 | sort -n | uniq | wc -l

判断依据:

  • ESTAB 到后端的连接数应该接近 upstream 实例数 × keepalive × worker 数,不应随时间线性增长。
  • 单个源 IP 的连接数上千 → 可能是攻击、爬虫或者客户端没复用连接。
  • TIME_WAIT 十万以上 → 连接复用失效或者 ip_local_port_range 太小。

17.3 进程与资源

   bash
# 所有 nginx 进程
ps -eo pid,ppid,rss,etime,cmd | grep '[n]ginx'

# worker 数与 CPU 核数对比
echo "worker: $(pgrep -P $(cat /run/nginx.pid) | wc -l), cpu: $(nproc)"

# 每个 worker 的 fd 使用
for p in $(pgrep -P $(cat /run/nginx.pid)); do echo "$p $(ls /proc/$p/fd | wc -l)"; done

# 主进程 fd 上限
cat /proc/$(cat /run/nginx.pid)/limits | grep -i 'open files'

# worker 内存明细
ps -o pid,rss,vsz,cmd -p $(pgrep -P $(cat /run/nginx.pid) | tr '\n' ',' | sed 's/,$//')

判断依据:worker 数 == CPU 核数 是期望值。worker 数超过核数 2 倍以上,检查 worker_processes 是否被写死成大数字。单个 worker 的 fd 数接近 worker_connections 时,说明到了上限。

17.4 日志排查

   bash
# 实时跟踪错误(去掉健康检查噪音)
tail -f /var/log/nginx/error.log | grep -v 'health'

# 最近 5 分钟的 5xx
awk -v d="$(date -d '5 min ago' '+%d/%b/%Y:%H:%M')" '$0 ~ d && $9 ~ /^5/' /var/log/nginx/access.log

# 按状态码统计
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# 慢请求 TOP 20(rt > 1s)
awk -F'rt=' '{if (NF>1) {split($2,a," "); if (a[1]+0 > 1) print a[1], $0}}' /var/log/nginx/access.log | sort -rn | head -20

# 上游错误分类
grep -oE 'upstream (prematurely|sent|timed out)[^,]*' /var/log/nginx/error.log | sort | uniq -c | sort -rn

# 某个 request_id 的完整链路
grep 'a1b2c3d4e5f6' /var/log/nginx/access.log

注意:awk -F'rt=' 依赖日志格式里有 rt= 字段。如果你的 log_format 没有,先加上,否则所有基于它的命令都失效。

17.5 压测与验证

   bash
# 简单并发压测(ab,注意 ab 对 keepalive 支持不好)
ab -n 10000 -c 200 -k http://127.0.0.1/api/test

# wrk(支持 keepalive,更适合 Nginx)
wrk -t8 -c400 -d30s --latency http://127.0.0.1/api/test

# 带 header 压测
wrk -t8 -c400 -d30s -H 'Host: api.example.com' -H 'Authorization: Bearer xxx' http://127.0.0.1/api/test

# 只测建连能力
h2load -n 10000 -c 100 https://api.example.com/

# TLS 握手测试
openssl s_time -connect api.example.com:443 -new -time 10

ab 的 -k 参数:开启 keepalive。不加 -k 时 ab 每个请求新建连接,测出来的 QPS 会远低于实际能力。这是"ab 测出来和实际差很多"的主要原因。

17.6 TLS 检查

   bash
# 证书链与有效期
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates

# 完整证书链
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | \
  grep -E 'i:|s:'

# 支持的协议版本
for v in tls1 tls1_1 tls1_2 tls1_3; do
  r=$(echo | openssl s_client -connect api.example.com:443 -servername api.example.com -$v 2>&1 | grep -c 'Cipher is')
  echo "$v: $r"
done

# OCSP Stapling
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -status 2>/dev/null | \
  grep -A2 'OCSP Response Status'

# 会话复用
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -reconnect 2>&1 | grep -i 'reused'

判断依据:

  • tls1 和 tls1_1 应该返回 0(不支持),否则有合规风险。
  • 证书链应该至少两级(叶子 + 中间 CA),缺中间 CA 会导致部分客户端(Android 老版本)握手失败。
  • -reconnect 输出里出现 Reused 说明会话复用生效。

17.7 抓包与深度排查

   bash
# 抓 502 现场的 HTTP 交互
tcpdump -i any -s0 -A 'tcp port 8080 and tcp[tcpflags] & (tcp-push|tcp-ack) != 0' -c 100

# 统计各状态码的 TCP 连接重置
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPAbortOnClose

# 查看 Nginx 的 syscall 分布(性能瓶颈定位)
strace -c -f -p $(pgrep -P $(cat /run/nginx.pid) | head -1)

# 用 perf 看 CPU 热点
perf top -p $(pgrep -P $(cat /run/nginx.pid) | head -1)

nstat 的三个关键指标:

指标含义出现增长说明
TcpExtListenOverflowsaccept 队列溢出后端 accept 太慢,somaxconn 太小
TcpExtListenDropsaccept 队列满导致丢弃同上,更严重
TcpExtTCPAbortOnClose有未读数据时关闭连接客户端提前断开,或 Nginx 主动 abort

 十八、生产注意事项 

18.1 配置管理的纪律

Nginx 配置是代码,不是手改的文本。 生产环境应该有:

  1. 版本控制:所有 .conf 文件进 Git,包括 conf.d/、snippets/。
  2. 变更审查:配置变更走 PR,至少一个人 review。重点看 proxy_pass 目标、server_name、location 优先级。
  3. 发布流程:nginx -t → 灰度单台 → 观察指标 → 全量 → 归档。不能直接在全部机器上 reload。

为什么强调这个:Nginx 配置的"改完没事"是假象。一个 location 优先级写错,可能只影响某个特定路径的请求,而那个路径恰好是低频接口,一周后才被发现。

18.2 reload 的安全边界

nginx -s reload 看起来无风险,实际有三类副作用:

副作用表现缓解
旧 worker 不退出长连接(WebSocket、SSE、大文件下载)让旧 worker 存活数小时监控 worker 数;必要时 nginx -s quit 后重启
内存阶段性翻倍新旧 worker 并存确认内存有 2 倍余量
缓存冷启动reload 不清缓存,但新 worker 的连接池是空的短暂的回源增加

长连接场景的 reload 策略:

   bash
# 1. 先摘流(前置 SLB 摘掉这台)
# 2. reload
nginx -s reload
# 3. 观察旧 worker 是否退出
watch -n2 'ps -eo pid,etime,cmd | grep "[n]ginx: worker"'
# 4. 超过预期时间(如 30 分钟)仍未退出,强制优雅退出
nginx -s quit

nginx -s quit 会让 worker 处理完现有请求后退出,不接受新连接。但如果有 WebSocket 长连接一直不断,worker 会一直不退出。

18.3 变更的执行窗口

变更类型推荐窗口原因
加 location / 改静态资源规则任意(低峰更好)影响面小
改 upstream 目标 / 权重低峰连接池重建,短暂抖动
改 worker_processes / worker_connections低峰 + 摘流需要 restart 或内存翻倍
改 TLS 证书任意,但要先验证reload 生效
改 limit_req 阈值低峰,先 dry-run阈值过严会误杀
升级 Nginx 版本低峰 + 摘流 + 完整回归需要 restart

18.4 升级的风险与检查清单

升级 Nginx 版本(尤其是跨大版本)可能改变默认值。1.29.7 的 proxy_http_version 默认值变化就是例子。

   bash
# 升级前
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--' | sort > /tmp/nginx-modules-before.txt
cp -a /etc/nginx /etc/nginx.bak.$(date +%F)

# 升级
dnf upgrade nginx

# 升级后
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--' | sort > /tmp/nginx-modules-after.txt
diff /tmp/nginx-modules-before.txt /tmp/nginx-modules-after.txt
nginx -t && systemctl restart nginx

升级检查清单:

  • [ ] configure arguments 是否变化(模块增减)
  • [ ] 是否有废弃指令(nginx -t 会警告)
  • [ ] 默认值是否变化(查 release notes 的 “Changes with nginx X.Y.Z” 里的 “Change:” 行)
  • [ ] 回归:静态资源、API 代理、WebSocket、上传、HTTPS
  • [ ] 回滚包已准备(dnf downgrade nginx 或保存旧 RPM)

一定要看 release notes 的 “Change” 而不是 “Feature”。行为变更(默认值、语义)比新功能重要得多,往往藏在不起眼的位置。

18.5 权限与安全

   nginx
# 隐藏版本号
server_tokens off;

# 禁止访问隐藏文件
location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
}

# 限制状态页访问
location /nginx_status {
    stub_status;
    allow 10.0.0.0/8;
    allow 127.0.0.1;
    deny all;
}

# 禁止执行上传目录中的脚本
location ~* ^/upload/.*\.(php|jsp|sh|py)$ {
    deny all;
}

server_tokens off 的实际效果:把 Server: nginx/1.30.6 变成 Server: nginx。这能减少针对性扫描,但不能替代及时打补丁。真正的安全来自版本更新。

status 页必须限制访问。暴露 stub_status 会泄露流量特征,是很常见的低危漏洞。

18.6 CVE 与补丁

Nginx 历史上高危漏洞不多,但有几个必须知道:

CVE影响版本类型处置
CVE-2021-23017< 1.21.1DNS resolver 堆溢出升级
CVE-2019-9511/9513/9516< 1.17.3HTTP/2 DoS(内存放大)升级
CVE-2021-3618< 1.21.0多协议混淆(ALPACA)升级,TLS 严格校验
CVE-2023-44487HTTP/2 相关Rapid Reset(HTTP/2 层间接影响)升级,限并发流

订阅官方安全通告:https://nginx.org/en/security_advisories.html。遇到新 CVE 时,先判断自己的配置是否受影响(比如没开 HTTP/2 就不受 HTTP/2 漏洞影响),再做升级决策。

18.7 容量规划

资源估算方法备注
内存基础 50MB + 连接数 × 200KB + 缓存 zonebuffer 大小决定系数
CPU静态文件约 1 核 / 5000 QPS;TLS 约 1 核 / 3000 握手实测为准
fdworker_processes × worker_connections × 1.2反向代理翻倍
磁盘日志(按 QPS × 日志行长度 × 保留天数)+ 缓存 max_size日志压缩后约 1/10
端口QPS × 平均连接时长,不超过 ip_local_port_range 大小开了 keepalive 后基本不成问题

这些数字是量级参考,不是精确值。真实容量必须在目标环境上压测得出。同一份配置在 NVMe 和机械盘上的静态文件 QPS 能差 10 倍。

 十九、一套完整的生产配置 

把前面的内容整合成一份配置,可以直接作为新项目的起点。

   nginx
# /etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;
worker_rlimit_nofile 102400;

error_log /var/log/nginx/error.log warn;
pid       /run/nginx.pid;

events {
    worker_connections 10240;
    multi_accept       on;
    accept_mutex       off;
    use                epoll;
}

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 rt=$request_time '
                    'urt=$upstream_response_time uaddr=$upstream_addr '
                    'ustat=$upstream_status rid=$request_id host=$host '
                    'cache=$upstream_cache_status';

    access_log /var/log/nginx/access.log main buffer=64k flush=5s;

    # ---------- 基础传输 ----------
    sendfile           on;
    tcp_nopush         on;
    tcp_nodelay        on;
    keepalive_timeout  65s;
    keepalive_requests 1000;

    server_tokens off;
    types_hash_max_size 2048;
    client_max_body_size 50m;
    client_body_buffer_size 128k;
    client_header_timeout 10s;
    client_body_timeout   30s;
    send_timeout          30s;

    # ---------- 文件缓存 ----------
    open_file_cache          max=20000 inactive=60s;
    open_file_cache_valid    80s;
    open_file_cache_min_uses 2;
    open_file_cache_errors   on;

    # ---------- 压缩 ----------
    gzip on;
    gzip_vary on;
    gzip_comp_level 5;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_types text/plain text/css text/xml application/json application/javascript
               application/xml+rss application/atom+xml image/svg+xml;
    # 注意:不要 gzip 图片和视频,它们已经是压缩格式

    # ---------- 限流 zone ----------
    limit_req_zone  $binary_remote_addr zone=perip:20m  rate=50r/s;
    limit_req_zone  $server_name       zone=pervhost:10m rate=5000r/s;
    limit_conn_zone $binary_remote_addr zone=connip:20m;
    limit_req_status  429;
    limit_conn_status 429;
    limit_req_log_level warn;

    # ---------- 缓存路径 ----------
    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=apicache:100m
                     max_size=20g inactive=60m use_temp_path=off;

    # ---------- upstream ----------
    upstream app_backend {
        zone app_backend 64k;
        least_conn;
        server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
        server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
        server 10.0.1.13:8080 max_fails=3 fail_timeout=10s;
        keepalive 64;
        keepalive_requests 900;
        keepalive_timeout 50s;
    }

    # ---------- 状态页 ----------
    server {
        listen 127.0.0.1:8081;
        server_name _;
        location /nginx_status {
            stub_status;
            access_log off;
            allow 127.0.0.1;
            allow 10.0.0.0/8;
            deny all;
        }
    }

    include /etc/nginx/conf.d/*.conf;
}
   nginx
# /etc/nginx/conf.d/api.conf
map $http_x_request_id $rid {
    ""      $request_id;
    default $http_x_request_id;
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_stapling        on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/example.com.chain.pem;
    resolver 223.5.5.5 valid=300s;

    limit_req  zone=pervhost burst=2000 nodelay;
    limit_conn connip 50;

    # ---------- API ----------
    location /api/ {
        limit_req zone=perip burst=100 nodelay;

        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $http_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_set_header X-Request-ID $rid;

        proxy_connect_timeout 3s;
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;

        proxy_next_upstream error timeout;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 5s;
    }

    # ---------- 可缓存的只读接口 ----------
    location /api/catalog/ {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $http_host;
        proxy_set_header X-Request-ID $rid;

        proxy_cache apicache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
        proxy_cache_key "$scheme$host$request_uri";

        add_header X-Cache-Status $upstream_cache_status always;
    }

    # ---------- SSE ----------
    location /api/stream {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $http_host;
        proxy_set_header X-Request-ID $rid;

        proxy_buffering off;
        proxy_cache off;
        gzip off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        add_header X-Accel-Buffering no;
    }

    # ---------- 静态资源 ----------
    location /static/ {
        alias /data/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        gzip_static on;
        aio threads;
        directio 4m;
        open_file_cache max=50000 inactive=300s;
        open_file_cache_valid 600s;
        open_file_cache_min_uses 1;
    }

    location = /index.html {
        root /data/www;
        add_header Cache-Control "no-cache, must-revalidate";
        etag on;
    }

    # ---------- 兜底 ----------
    location / {
        return 404;
    }
}

这份配置的说明:

  • worker_rlimit_nofile 102400 是 Nginx 自己的限制,必须小于等于 systemd 的 LimitNOFILE,否则以较小者为准。
  • 静态资源的 open_file_cache 单独覆盖为更长的 valid(600s),因为静态文件很少变;API 路由不用文件缓存。
  • location / 返回 404 而不是上楼默认站点,避免误把根路径暴露出去。
  • 限流分两级:/api/ 按 IP(perip),整个 vhost 按总量(pervhost)。两级都要有,否则单个 IP 的正常速率(50r/s)乘以大量 IP 仍能打死后端。

 二十、总结 

这篇文章讲了八件事,最后收成四句可执行的话。

第一句:先量,再调。 没有基线就没有优化。开工前用 stub_status、$request_time 分布、ss -s 建立基线,调完对比。没有基线的"调优"只是在制造变更。

第二句:瓶颈通常在连接层,不在参数层。 复盘我遇到的绝大多数"QPS 上不去",根因是连接复用失效、fd 不够、或者后端 keepalive 配置不匹配。worker_processes、worker_connections 这些参数很重要,但它们决定的是天花板,不是实际吞吐。真实吞吐取决于最短的那块板。

第三句:默认值会变,配置要显式。 1.29.7 改了 proxy_http_version 的默认值,这类变化会让"照着三年前的教程写的配置"行为不同。凡是影响正确性的指令(proxy_http_version、Connection、keepalive 系列),都显式写出来,不依赖默认值。

第四句:保护性配置比性能配置更重要。 限流、超时、max_fails、proxy_cache_use_stale——这些在正常情况下看起来"没用",但在异常时决定了系统是"降级"还是"雪崩"。性能调优让系统跑得快,保护性配置让系统不崩。

三个值得长期维护的机制:

  1. 配置变更的回归清单:每次改配置,按清单验证(静态资源、API、WebSocket、上传、HTTPS、限流)。清单化能避免"改 A 坏 B"。
  2. 日志字段的稳定契约:$request_id、rt、urt、ustat 这几个字段要长期保留,所有日志分析脚本都依赖它们。改 log_format 等于让所有监控失效。
  3. 版本升级的默认值对照:每次升级前读 release notes 的 “Change” 段落,把默认值变化记进文档。这是唯一能避免"升级后行为变了但没人知道"的方法。

Nginx 的配置项有几百个,但真正影响生产稳定性的不超过二十个。上面这些就是那二十个。剩下的,遇到具体问题再查文档,比一开始全部配上要可靠得多。


阅读原文:点击这里​


该文章在 2026/11/9 11:41:59 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-1  粤公网安备44030602007207号