Nginx 生产实践:反向代理、缓存、限流与 TLS
本文目录
Nginx 在多数项目里承担的是"流量第一道闸门"的角色:所有请求先进 Nginx,再由它决定转发给谁、缓存什么、拒绝什么。这篇文章按生产环境实际会用到的顺序讲:事件驱动模型与容量估算、反向代理与路径规则、负载均衡与健康检查、限流、缓存与压缩、TLS,最后是超时矩阵和 502/504/499 三类高频故障的定位方法。所有配置都可以在 nginx.org 的官方文档里逐条对照验证。
事件驱动模型与连接数估算
Nginx 采用多进程 + 事件驱动(epoll/kqueue)的架构:一个 master 进程负责读取配置、管理 worker,不处理请求;每个 worker 进程单线程事件循环,同时监听数千个连接。没有每连接一个线程的内存开销,这是它能在小机器上扛高并发的根本原因。
worker 数量的官方建议是等于 CPU 核数:
# 自动按 CPU 核数启动 worker
worker_processes auto;
# worker_connections 含客户端连接和到上游的连接两个方向
events {
worker_connections 10240;
use epoll; # Linux;FreeBSD/macOS 对应 kqueue
}
容量估算要记住一条:一个客户端连接如果是被代理的,会占用 2 个 worker 连接(client → nginx 一个,nginx → upstream 一个)。所以理论上限是:
| 配置 | 计算 | 说明 |
|---|---|---|
| worker_processes 4,worker_connections 10240 | 4 × 10240 = 40960 连接 | 单机总连接槽位 |
| 其中可服务的客户端并发 | 40960 / 2 ≈ 20480 | 代理场景,每请求占 2 槽位 |
| 长连接复用到上游 | 接近 40960 | keepalive 到 upstream 可省掉后一半 |
配套动作不能漏:
# 连接数估算过万时,内核 fd 上限与 nginx 的 worker_rlimit_nofile 要同步调大
ulimit -n
还有 ulimit 只对当前 shell 生效,生产环境要改 systemd 单元或 /etc/security/limits.conf,这是最常见的"测试机好好的、上线就 24: Too many open files"的原因。
反向代理:proxy_pass 路径规则与请求头透传
反向代理的核心是 proxy_pass,它最容易踩坑的点是尾斜杠的语义:
# 规则一:proxy_pass 带路径(含尾斜杠),location 匹配的部分会被替换
location /api/ {
proxy_pass http://127.0.0.1:3000/v1/;
# 请求 /api/users → 上游收到 /v1/users
}
# 规则二:proxy_pass 只有 host:port,原始 URI 原样透传
location /api/ {
proxy_pass http://127.0.0.1:3000;
# 请求 /api/users → 上游收到 /api/users
}
尾斜杠差一个字符,上游收到的路径完全不同。改配置时必须用 curl 验证真实转发路径,不要靠猜。
请求头透传是第二个必须固化的配置。默认情况下 Nginx 透传大部分 header,但后端真正需要的几个客户端信息需要显式声明:
location /api/ {
proxy_pass http://127.0.0.1:3000;
# X-Real-IP 是直连地址;XFF 由 nginx 追加 $remote_addr 形成链路
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 保留原始 Host 与协议,后端生成绝对链接、判断 https 时依赖它们
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# HTTP/1.1 + keepalive:免每请求一次 TCP 握手
proxy_http_version 1.1;
proxy_set_header Connection "";
}
注意边界:X-Forwarded-For 是可伪造的,客户端可以直接带上这个 header。$proxy_add_x_forwarded_for 会把 $remote_addr 追加在后面,所以后端取第一个 IP 之前必须确认入口 Nginx 会覆盖该 header,或者只信任链路里可信代理追加的最后一跳。直接信任客户端送来的 XFF 是伪造 IP 绕过封禁的经典漏洞。
负载均衡:四种策略与选择
upstream 定义一组后端,策略决定流量怎么分:
upstream app_backend {
# 会话粘性用 ip_hash;请求耗时差异大用 least_conn(都写在 server 之前)
server app1.example.com:3000 weight=3; # weight 按权重分流:异构机器、灰度
server app2.example.com:3000 weight=1;
keepalive 32; # 到上游的长连接池,大幅减少握手开销
}
| 策略 | 分配逻辑 | 适用场景 | 代价 |
|---|---|---|---|
| 轮询(默认) | 逐个分 | 后端无状态、机器同配 | 无 |
| weight | 按权重比例 | 异构机器、灰度分流 | 无 |
| ip_hash | 对 IP 哈希取模 | 本地会话/缓存依赖来源 | 扩缩容时映射重排,IP 造假可被打穿 |
| least_conn | 活跃连接最少者 | 请求耗时方差大 | 需维护连接计数,略耗 CPU |
无状态服务一律用轮询或 least_conn;ip_hash 只在明确需要会话粘性且接受其故障转移代价时才用。灰度分流用 weight 是低成本做法,但更精确的按比例分流应该在网关层做。
upstream 健康检查与失败转移
开源版 Nginx 的被动健康检查:请求失败时标记节点,连续失败达到 max_fails(在 fail_timeout 窗口内)就摘除一段时间。
upstream app_backend {
server app1.example.com:3000 max_fails=3 fail_timeout=30s;
server app2.example.com:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3000 backup; # 备用节点,全部主节点失败才启用
}
判定为失败的默认条件是连接失败或超时,5xx 是否算失败由 proxy_next_upstream 决定:
# 什么情况下换下一个节点重试(默认只重试幂等方法,这是安全兜底)
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3; # 最多换 3 个节点
proxy_next_upstream_timeout 10s; # 换节点重试的总时间上限
边界必须写清楚:proxy_next_upstream 加上 non_idempotent 会把 POST 也重试,除非后端做了幂等,否则会产生重复下单。默认只重试幂等方法(GET/HEAD/PUT/DELETE/OPTIONS)是安全兜底,不要随手加参数。
被动检查的盲区是"没流量就发现不了挂掉的节点"。要主动探测需要在商用版(Nginx Plus)或用Consul/网关侧的主动健康检查摘节点,开源版常见做法是把健康检查放到部署系统:发布完自动 curl /healthz,失败就从负载均衡注册里摘除。
限流:limit_req 与 limit_conn
Nginx 内置基于漏桶的请求限流。rate 是稳定速率,burst 是允许的突发容量,nodelay 决定突发请求是排队还是立刻拒绝:
# req 控速率,conn 控并发,两个 zone 都按客户端 IP
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
location /api/ {
limit_req zone=req_per_ip burst=200 nodelay; # 稳态 100 r/s + 200 突发
limit_conn conn_per_ip 50;
limit_req_status 429; # 统一返回 429,便于前端区分限流与其他错误
limit_conn_status 429;
}
}
关键参数的工程含义:
| 参数 | 作用 | 实践建议 |
|---|---|---|
rate | 漏桶长期速率 | 按正常峰值 × 1.5 定,先观察后收紧 |
burst | 瞬时可容纳的突发 | 必须配 nodelay,否则突发会排队拖慢整体延迟 |
zone 10m | 状态内存,约存 16 万个 IP | $binary_remote_addr 用 4 字节,比字符串 key 省 10 倍 |
limit_conn | 并发连接上限 | 防慢连接攻击(slowloris)比 rate 更直接 |
限流的 key 选择比参数更重要:按 $binary_remote_addr 会误伤 NAT 出口后的全体用户(公司网络、校园网共享一个出口 IP);按 $http_x_user_id(登录态)更精确但要防止用户伪造头。生产中常见组合是"IP 维度做粗防线 + 用户维度做细防线"两层 zone。
压缩与缓存:gzip、brotli、Cache-Control、proxy_cache
压缩减少的是传输体积,缓存减少的是请求量,两者解决不同问题。
gzip on;
gzip_comp_level 5; # 等级 4-5 是体积与 CPU 的平衡点
gzip_min_length 1k; # 小于 1k 压缩收益低于开销
gzip_types text/css application/javascript application/json text/plain;
# brotli 压缩率更高,需安装 ngx_brotli 模块,客户端支持时优先
brotli on;
brotli_comp_level 5;
brotli_types text/css application/javascript application/json;
带 hash 的静态资源(如 app.a3f9c2.js)可以直接长缓存:
location /assets/ {
root /var/www/example.com;
expires 1y; # 内容寻址的文件名,改内容必改名,缓存一年也安全
add_header Cache-Control "public, immutable";
}
对上游 API 响应做代理缓存要谨慎——只有明确的幂等 GET 才值得缓存:
# 目录层级/内存区/磁盘上限/失效时间一行声明
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app_cache:20m max_size=2g inactive=30m use_temp_path=off;
location /api/public/ {
proxy_pass http://app_backend;
proxy_cache app_cache;
proxy_cache_valid 200 5m; # 200 响应缓存 5 分钟
proxy_cache_key $scheme$request_method$host$request_uri;
proxy_cache_use_stale error timeout updating; # 上游挂了先回旧数据
add_header X-Cache-Status $upstream_cache_status; # HIT/MISS/STALE,排查必备
}
X-Cache-Status 这个 header 强烈建议保留在测试环境,命中与否一眼可见。个人化接口(含登录态)绝对不要进 proxy_cache,除非 key 里包含用户标识且过期时间足够短。
TLS:证书、协议、套件、HSTS 与 OCSP 装订
生产 TLS 配置的要点是:新协议、少量强套件、开启会话复用与装订。
server {
listen 443 ssl;
http2 on;
server_name example.com;
# 证书链必须完整(服务器证书 + 中间证书拼在一个文件)
ssl_certificate /etc/nginx/ssl/example.com.fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# TLS 1.2 起步;1.3 握手更快且去掉了大量不安全套件
ssl_protocols TLSv1.2 TLSv1.3;
# 1.2 只留 ECDHE(前向保密)+ AEAD 加密;1.3 由协议内置,不在此列
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# 会话复用:减少重复完整握手
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP 装订:由服务器代查证书状态,客户端不用再去 CA 查
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/ca.crt;
# HSTS:强制浏览器后续只用 https 访问;先确认全站 https 再开
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
验证手段(不是靠感觉):
# 检查协议与套件协商结果
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>/dev/null | rg "Protocol|Cipher"
# 检查 OCSP 装订是否真的在响应里
openssl s_client -connect example.com:443 -status </dev/null 2>/dev/null | rg "OCSP"
注意 includeSubDomains 的杀伤范围:所有子域名必须已经 https 化,否则会被浏览器直接拒绝访问。新域名先只发 max-age=300 观察,再逐步放大到一年。
超时矩阵与 413:body 上限
超时要成体系地配,且与上游服务的超时形成层级关系(Nginx 略小于上游自身超时,让 Nginx 先返回错误而不是干等):
| 指令 | 含义 | 典型值 |
|---|---|---|
proxy_connect_timeout | 与上游建连超时 | 3s–5s(内网应该秒连) |
proxy_send_timeout | 两次向上游写之间的间隔超时 | 30s–60s |
proxy_read_timeout | 两次从上游读之间的间隔超时 | 30s–60s(SSE/慢接口单独放大) |
send_timeout | 向客户端两次写之间的间隔 | 60s |
client_body_timeout | 读请求体的间隔 | 60s |
location /api/ {
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 10m;
}
location /upload/ {
client_max_body_size 100m; # 不设就是默认 1m,大文件直接 413
proxy_read_timeout 300s;
}
client_max_body_size 默认只有 1m,这是上传接口"本地好好的、上了线就 413"的头号原因。另外 414(URI 过长)同理来自 large_client_header_buffers 的默认值,做长查询串接口时要留意。
location 匹配优先级
Nginx 的 location 匹配不是"从上到下",而是有固定优先级,写错顺序不会救你:
| 优先级 | 匹配符 | 行为 |
|---|---|---|
| 1 | = 精确匹配 | 命中即停止 |
| 2 | ^~ 前缀匹配 | 命中即停止(不再查正则) |
| 3 | ~ / ~* 正则 | 按配置文件出现顺序,第一个命中生效 |
| 4 | 普通前缀 | 最长前缀胜出 |
server {
location = /healthz { return 200 "ok"; } # 精确匹配,探测走这里
location ^~ /assets/ { expires 1y; } # 前缀命中后不再查正则
location ~ \.php$ { return 404; } # 正则按出现顺序生效
location /api/ { proxy_pass http://app_backend; } # 兜底前缀
}
经典事故:一个 location ~ \.(js|css)$ 正则把静态资源的缓存 header 全部抢走,而配置文件里明明先写了 /assets/ 前缀——因为前缀永远排在正则后面。出问题时用 curl -v 看真实响应 header 归属哪个 location。
常见故障:502、504、499
| 状态码 | 直接含义 | 高频根因 | 定位动作 |
|---|---|---|---|
| 502 | 上游返回无效响应或连接被拒 | 应用崩溃/端口没监听/上游主动断开 | 到机器上 curl 127.0.0.1:3000/healthz,查应用日志与 OOM 记录 |
| 504 | 上游超时(proxy_read_timeout 到点) | 慢查询、死锁、下游依赖卡死 | 对齐 Nginx 与应用超时,查慢日志 |
| 499 | 客户端在响应前断开 | 用户刷新/关闭页面,或前置超时比 Nginx 短 | 少量 499 正常;大量 499 伴随 504 时说明客户端等不及了,是慢接口的间接证据 |
# 从 Nginx 机器直接打上游,区分"Nginx 问题"还是"应用问题"
curl -sS -o /dev/null -w "%{http_code} %{time_total}s\n" http://127.0.0.1:3000/healthz
# error 日志会记录 upstream 超时与连接失败的具体原因
tail -100 /var/log/nginx/error.log
# 确认上游端口确实在监听、监听在预期地址
ss -lntp | rg 3000
502 里有一类容易被忽略:upstream sent too big header——上游响应头(常是过大的 Cookie 或网关注入的调试头)超出 proxy_buffer_size,解决办法是适度调大 proxy_buffer_size 与 proxy_buffers,而不是重启应用。
注意事项与边界
- 改配置永远
nginx -t先验证,再nginx -s reload;reload 是平滑的,但配了新 upstream 的 keepalive 池时旧连接会按旧配置走完。 limit_req的rate单位支持r/s与r/m,慢速限流(如登录接口 1r/s + burst=5)用r/m表达更直观。- proxy_cache 缓存的内容含
Set-Cookie时默认不会缓存,但更安全的做法是个人化响应压根不进缓存 location。 - HSTS 一旦下发到浏览器,回退非常困难(要等 max-age 过期),子域名未全量 https 前不要加
includeSubDomains。 X-Forwarded-For信任链问题:只有在"本 Nginx 是唯一入口"时后端才可以取第一段,多层代理要逐层配置可信段。- brotli 需要自行编译模块或用带模块的发行版,动态
.so与 Nginx 版本要配套,升级 Nginx 时容易漏。 - 本文示例域名统一为 example.com、IP 为 127.0.0.1,直接复制到生产前替换成你的真实拓扑。
总结
Nginx 生产配置的核心是三层结构:容量层(worker/连接数/fd 与内核参数对齐)、路由层(proxy_pass 尾斜杠、location 优先级、header 透传可验证)、保护层(限流双维度、超时矩阵成体系、健康检查与失败转移)。把每一条配置都配上一个 curl 验证命令,比记住任何"最佳实践模板"都可靠——模板会过时,验证方法不会。
参考
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。