Nginx 使用方式与典型场景
使用方式与典型场景
场景一:反向代理 + HTTPS + gzip + 静态资源缓存
这是最标准的「Node/Java/Go 应用 + Nginx 前置」部署。要点是:HTTPS 只在 Nginx 终止,静态资源加长缓存,后端接口不缓存,前端转发真实客户端信息。
# /etc/nginx/conf.d/app.conf
# conf.d 下的文件会被主配置 include 进 http 块,
# 所以 proxy_cache_path 和 upstream 可以直接写在文件顶层。
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app_cache:20m
max_size=2g inactive=60m use_temp_path=off;
upstream app_backend {
least_conn;
server 10.0.0.11:3000 max_fails=3 fail_timeout=15s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=15s;
keepalive 32;
}
# HTTP 一律跳 HTTPS
server {
listen 80;
listen [::]:80;
server_name www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /etc/nginx/certs/www.example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/certs/www.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
root /srv/www/app;
index index.html;
# gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss image/svg+xml;
# 带指纹的静态资源:强缓存一年,前端换文件名即失效
location ~* \.(?:js|css|woff2|png|jpg|jpeg|gif|svg|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 不走缓存、不做长缓存的接口
location /api/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
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_set_header Connection "";
proxy_read_timeout 60s;
}
# 读多写少、可缓存的公开接口
location /api/public/ {
proxy_pass http://app_backend;
proxy_cache app_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 5m;
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;
add_header X-Cache-Status $upstream_cache_status;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
几个关键点解释一下:
proxy_cache_path里的keys_zone是共享内存名和大小,max_size是磁盘上限,inactive是多久没被访问就清理。响应头X-Cache-Status会显示 HIT / MISS / EXPIRED / STALE,压测时非常有用。proxy_cache_use_stale让后端挂掉时还能继续吐旧缓存,是「降级可用」的关键一行。add_header X-Cache-Status这类自定义头建议放在 location 内且注意:同一层级出现多个 add_header 时,子层的会覆盖父层,不是叠加。这是个高频困惑点。expires 1y配合文件名带 hash 的构建产物(如 Vite / webpack 产出的app.a1b2c3.js)才有意义。文件名不变就别加 immutable。
适合:后端是应用服务、静态资源来自构建产物、拓扑固定的传统部署。不适合:纯静态托管(直接用对象存储 + CDN 更省事)、需要按请求内容动态路由的复杂网关场景(那是 Traefik / Envoy 的地盘)。
场景二:SPA 的 history fallback
React Router、Vue Router 用 history 模式时,用户直接访问或刷新 /courses/123,浏览器会真的向服务器请求这个路径,而服务器磁盘上并没有这个文件,于是 404。解决办法是 try_files:真实文件优先,找不到就交给 index.html,由前端路由接管。
server {
listen 443 ssl;
server_name spa.example.com;
ssl_certificate /etc/nginx/certs/spa.fullchain.pem;
ssl_certificate_key /etc/nginx/certs/spa.key;
ssl_protocols TLSv1.2 TLSv1.3;
root /srv/www/spa;
index index.html;
# 有真实文件就发文件,没有就回 index.html(前端路由接管)
location / {
try_files $uri $uri/ /index.html;
}
# 入口 HTML 绝不缓存,否则发版后用户拿到旧页面引用已删除的 chunk
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires -1;
}
# 带指纹的产物长缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 反向代理到后端
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}
}
那个 location = /index.html 的精确匹配块不是可有可无的装饰。SPA 发版后旧 chunk 文件被删,如果 index.html 被浏览器缓存了,用户会一直加载失败的 chunk,且刷新也没用。这是 SPA 部署最高频的线上事故之一。
场景三:WebSocket 代理 + 限流
聊天、实时通知、协作编辑这类长连接服务,除了协议升级头,还必须解决两件事:一是长连接不能因为空闲被 Nginx 断开,二是要限制单 IP 的并发连接数,防止有人开一堆连接把端口占满。
# http 块级:定义 WebSocket 升级头的映射,避免为普通请求误发 Connection: upgrade
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# http 块级:限流区定义
limit_req_zone $binary_remote_addr zone=api_req:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=ws_conn:10m;
upstream ws_backend {
ip_hash; # 会话粘性,长连接尽量落在同一实例
server 10.0.0.21:9000;
server 10.0.0.22:9000;
}
server {
listen 443 ssl;
server_name ws.example.com;
ssl_certificate /etc/nginx/certs/ws.fullchain.pem;
ssl_certificate_key /etc/nginx/certs/ws.key;
ssl_protocols TLSv1.2 TLSv1.3;
# WebSocket 通道
location /ws {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s; # 让空闲长连接不被掐断
proxy_send_timeout 3600s;
proxy_buffering off; # 实时消息不走缓冲,降低延迟
limit_conn ws_conn 10; # 单 IP 最多 10 条并发长连接
}
# 普通 HTTP 接口限流
location /api/ {
limit_req zone=api_req burst=40 nodelay;
limit_req_status 429; # 默认是 503,改成 429 语义更准
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
关于限流参数,几个容易误解的点:
rate=20r/s里的$binary_remote_addr是二进制形式的客户端 IP,比$remote_addr省内存(IPv4 只占 4 字节),官方推荐用这个做 key。burst是允许额外排队的请求数。nodelay表示 burst 范围内的请求立刻放行而不是慢慢排队,体感更自然;不加 nodelay 则严格按速率放行,会有可感知的延迟。- 如果把限流放在负载均衡器后面,
$binary_remote_addr拿到的是负载均衡器的 IP,所有用户会共享一个限流桶。这时得换成基于$http_x_forwarded_for的 key,但要确保只有可信代理能设置这个头。 limit_conn超限返回 503,limit_req超限默认也返回 503,都可以用limit_conn_status/limit_req_status改。
适合:有实时通信需求、需要保护后端不被刷接口的场景。不适合:分布式部署下需要全局一致限流的场景——Nginx 的限流区是单机内存,多台机器各限各的,总量是台数倍。要全局精确限流得靠 Redis 计数或 API 网关。

