Nginx 学习指南:反向代理、负载均衡与生产调优
面向人群:Java 后端初学者 / 准备校招实习的同学
目标:看完这篇,你能独立配出一套能上生产的 Nginx(反向代理 + 负载均衡 + HTTPS + 限流),并且能定位线上最常见的那几个错误码。
一、先搞清楚:后端为什么必须会 Nginx
很多后端同学的认知是「Nginx 是运维的事,我只管写接口」。真实情况是:只要你负责的服务要上线,你和 Nginx 之间至少隔着这五件事。
| 场景 | 没配好会怎样 | 用到的 Nginx 能力 |
|---|---|---|
| 多实例部署 | 前端不知道该访问哪个端口,实例挂了请求就打空 | upstream 负载均衡 + 健康检查 |
| HTTPS 接入 | 在 Tomcat 里配证书,每台机器配一遍,改证书要全量重启 | 统一 SSL 卸载 |
| 前端静态资源 | JS/CSS 交给 Tomcat 处理,QPS 上不去 | 动静分离 + gzip + 缓存头 |
| 接口被刷 | 单个 IP 一秒几千次请求,服务雪崩 | limit_req 限流 |
| 跨域 / 跨域头 | 前端联调天天报 CORS | 统一加响应头 |
| 上传大文件 | 上传 10MB 的图片直接报 413 | client_max_body_size |
| 长连接 / WebSocket | 消息推送连上就断 | Upgrade 头转发 |
一句话定位:Nginx 是应用前面的那道「流量总闸门」,所有请求都要过它,所以横切关注点(HTTPS、限流、日志、压缩、跨域、灰度)放在这一层最省事。
Nginx 在架构里的位置
客户端
│
▼
CDN / DNS
│
▼
┌─────────────────────────────┐
│ LVS / 云负载均衡(四层) │ ← 扛超大流量,一般云厂商托管
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Nginx(七层) │ ← 你要掌握的:SSL 卸载、路由、限流
│ · 反向代理 / 负载均衡 │
│ · 静态资源 / 动静分离 │
│ · HTTPS / HTTP2 │
└─────────────────────────────┘
│ │
▼ ▼
Spring Boot Spring Boot
:8080 :8081
│ │
└────────┬───────┘
▼
MySQL / Redis / MQ
四层 vs 七层: LVS 工作在传输层(只看 IP + 端口),性能极高但不懂 HTTP;Nginx 工作在应用层(能看 URL、Header、Cookie),可以做路由和改写,代价是要解析 HTTP。绝大多数业务场景用 Nginx 就够了,十万级并发以下不用碰 LVS。
二、快速起步
2.1 安装
bash
# Docker 最快
docker run -d --name nginx-dev \
-p 80:80 \
-v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro \
-v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \
-v /opt/nginx/logs:/var/log/nginx \
-v /opt/nginx/html:/usr/share/nginx/html \
nginx:1.27-alpine
# CentOS
yum install -y nginx
# Ubuntu
apt install -y nginx
2.2 三条必记的命令
bash
nginx -t # 检查配置语法(改配置前必做,能救命)
nginx -s reload # 热加载,不断连接(生产环境只用它)
nginx -s stop # 立即停止(粗暴)
nginx -s quit # 优雅停止,处理完当前请求再退出
nginx -V # 查看编译参数、模块和版本
⚠️
nginx -t一定要养成习惯。 生产环境改配置直接 reload,如果语法错了,reload 会失败并保留旧配置继续运行 ------ 这本身是安全机制,但如果你没-t就以为改成功了,排查方向会跑偏。
2.3 配置文件的层级
Nginx 配置是树形结构,理解层级就理解了一半:
main(全局块)
├── events # 连接模型:worker 连接数、IO 多路复用
└── http # HTTP 协议相关,所有 server 的公共配置
├── upstream backend {...} # 定义后端服务器组
├── server { # 一个虚拟主机
│ ├── listen 80;
│ ├── server_name a.com;
│ └── location /api/ { # 按 URI 匹配,最常用
│ └── proxy_pass http://backend;
│ }
└── server { ... }
配置加载顺序与拆分: 主配置 nginx.conf 里一般有一行:
nginx
include /etc/nginx/conf.d/*.conf;
最佳实践:一个站点一个 .conf 文件 ,不要把所有 server 都堆进 nginx.conf。文件名建议带序号(00-default.conf、10-api.conf),因为 include 是按字母序加载的,序号能控制默认站点的优先级。
2.4 变量:Nginx 的「内置 API」
写复杂配置绕不开变量,常用的就这些:
| 变量 | 含义 |
|---|---|
$host |
请求头里的 Host(不含端口) |
$http_host |
请求头里的 Host(含端口) |
$remote_addr |
客户端 IP(经过代理后就是上一层代理的 IP) |
$proxy_add_x_forwarded_for |
追加格式的代理链,形如 client, proxy1, proxy2 |
$request_uri |
完整原始 URI,含查询串,/a/b?x=1 |
$uri |
规范化后的 URI,不含查询串 |
$args / $query_string |
查询串部分 |
$request_method |
GET / POST / ... |
$scheme |
http 或 https |
$status |
响应状态码 |
$upstream_addr |
实际转发到的后端地址,排查问题必备 |
$upstream_response_time |
后端处理耗时,排查性能必备 |
$request_time |
整请求耗时,含接收客户端数据的时间 |
三、location 匹配规则(最容易踩坑的一节)
这是 Nginx 配置里出错率最高的部分,也是面试官最爱问的。
3.1 语法
nginx
location [修饰符] 匹配模式 {
...
}
| 修饰符 | 含义 | 优先级 |
|---|---|---|
= |
精确匹配,完全一致才命中 | 最高,命中即停止 |
^~ |
前缀匹配,命中后不再检查正则 | 次之 |
~ |
正则匹配,区分大小写 | 按定义顺序,先到先得 |
~* |
正则匹配,不区分大小写 | 按定义顺序,先到先得 |
| 无 | 普通前缀匹配 | 最低,取最长的那个 |
3.2 完整匹配流程(背下来)
1. 先找 = 精确匹配,命中 → 立即使用,停止搜索
↓ 未命中
2. 遍历所有「普通前缀匹配」(含 ^~),记住最长的那一个
↓
3. 如果最长那个前缀带 ^~ → 立即使用,跳过正则
↓ 否则
4. 按配置文件中的书写顺序,逐个尝试正则(~ 和 ~*)
第一个命中的正则 → 立即使用,停止搜索
↓ 都不匹配
5. 使用第 2 步记住的最长前缀匹配
一句话口诀:精确 > 前缀(^~) > 正则(按顺序) > 最长前缀。
3.3 一个综合例子
nginx
server {
listen 80;
server_name example.com;
location = / {
# 只匹配 http://example.com/ 这一个 URL
return 200 "首页\n";
}
location ^~ /static/ {
# /static/ 开头的全部走这里,不会被下面的正则抢走
root /data/www;
expires 30d;
}
location ~ \.(gif|jpg|png|jpeg|webp)$ {
# 正则:图片后缀。注意 /static/logo.png 不会走到这被 ^~ 拦住了
root /data/img;
expires 7d;
}
location ~* \.(js|css)$ {
# 不区分大小写
root /data/www;
}
location /api/ {
# 普通前缀,兜底
proxy_pass http://backend;
}
location / {
# 最终兜底
try_files $uri $uri/ /index.html;
}
}
常见误解纠正:
- ❌「前缀匹配按配置顺序」------ 不对,普通前缀是按长度,越长越优先,和配置顺序无关。
- ❌「正则写在前面就一定能优先」------ 只有在没有
^~拦住的情况下才是。 - ✅
/能匹配所有请求,它是长度最短的前缀,所以永远是兜底。
3.4 root 与 alias 的区别(经典坑)
nginx
# root:最终路径 = root 值 + 完整 URI
location /static/ {
root /data/www;
}
# 请求 /static/a.js → /data/www/static/a.js
# alias:最终路径 = alias 值 + 去掉 location 前缀后的部分
location /static/ {
alias /data/www/;
}
# 请求 /static/a.js → /data/www/a.js
规则:
- 用
root,URI 会带着 location 前缀拼上去 - 用
alias,URI 会剥掉 location 前缀再拼 alias后面的路径必须以/结尾,否则拼接会出错alias只能用在location块里,root可以放在server/http/location
90% 的静态资源 404 都是这两个指令搞混了。判断方法很简单:看磁盘上实际文件路径里有没有 location 的那个目录名。
四、反向代理:Java 后端的核心用法
4.1 最小可用配置
nginx
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
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;
}
}
4.2 那几个 proxy_set_header 为什么要写
这是「配了能跑」和「配对了」的分水岭。
| Header | 不写的后果 |
|---|---|
Host $host |
后端收到的 Host 是 backend(upstream 名),导致后端生成的重定向 URL、拼接的绝对链接全错 |
X-Real-IP $remote_addr |
后端 request.getRemoteAddr() 拿到的是 Nginx 的 IP,所有基于 IP 的逻辑(限流、风控、日志、地域判断)失效 |
X-Forwarded-For $proxy_add_x_forwarded_for |
多级代理下拿不到真实客户端 IP 链 |
X-Forwarded-Proto $scheme |
后端不知道用户走的是 http 还是 https,生成的链接协议可能不对 |
Spring Boot 侧配套配置(否则拿到的还是 Nginx 的 IP):
yaml
server:
forward-headers-strategy: native # Spring Boot 2.6+ 推荐
# 老版本(2.2 之前)用:
# tomcat:
# remote-ip-header: x-forwarded-for
# protocol-header: x-forwarded-proto
配好之后,代码里这么取真实 IP:
java
public static String getClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(xff)) {
// 格式:client, proxy1, proxy2,第一个才是真实客户端
return xff.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
return StringUtils.hasText(realIp) ? realIp : request.getRemoteAddr();
}
⚠️ 安全提醒 :
X-Forwarded-For是可以被客户端伪造的。如果 Nginx 是你的唯一入口 ,直接用$proxy_add_x_forwarded_for会把伪造值追加进去。更安全的做法是在最外层 Nginx 上强制覆盖 :
proxy_set_header X-Forwarded-For $remote_addr;(用真实连接 IP 覆盖,丢弃客户端传来的值)。只有在 Nginx 内部还有二次代理时才用$proxy_add_x_forwarded_for。
4.3 proxy_pass 结尾的斜杠(必考题)
这是最容易让人困惑的语法细节,一张表说清:
假设请求 URI 是 /api/user/list,location 匹配 /api/:
| 配置 | 转发给后端的 URI | 说明 |
|---|---|---|
proxy_pass http://backend; |
/api/user/list |
不带 /:原样透传完整 URI |
proxy_pass http://backend/; |
/user/list |
带 / :把 location 匹配部分替换成 / |
proxy_pass http://backend/app/; |
/app/user/list |
匹配部分换成 /app/ |
记忆方法:把 proxy_pass 的值看成一个「替换模板」,location 匹配到的那一段被替换成模板里的 URI 部分。
nginx
# 场景 1:后端接口带 /api 前缀,原样转发
location /api/ {
proxy_pass http://backend; # /api/user/list → /api/user/list ✓
}
# 场景 2:后端接口没有 /api 前缀,需要剥掉
location /api/ {
proxy_pass http://backend/; # /api/user/list → /user/list ✓
}
如果 location 用的是正则 ,或者
proxy_pass里带变量,规则会更复杂(正则模式下不能带 URI 部分)。初学阶段避开正则 + proxy_pass 的组合。
4.4 超时相关参数(504 的根源)
nginx
location / {
proxy_pass http://backend;
proxy_connect_timeout 5s; # 与后端建立连接的超时,默认 60s,内网 5s 足够
proxy_send_timeout 60s; # 向后端发送请求的超时(两次写之间)
proxy_read_timeout 60s; # 等待后端响应的超时 ← 504 主要看这个
}
504 Gateway Timeout 的排查链条:
浏览器 504
→ 看 Nginx error.log 里有没有 "upstream timed out"
→ 有:后端处理超过 proxy_read_timeout
→ 要么优化后端接口(正解)
→ 要么针对慢接口单独加大 timeout(临时方案)
→ 没有:可能是上游 LB / CDN 的超时更短
按接口差异化配置(报表导出这类慢接口单独放宽):
nginx
location /api/report/export {
proxy_pass http://backend;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
4.5 其它高频代理参数
nginx
location / {
proxy_pass http://backend;
# 大文件上传,默认只有 1MB,超过就 413
client_max_body_size 100m;
# 禁用缓冲:适用于 SSE、流式响应;常规接口别开,会影响性能
# proxy_buffering off;
# 后端出错时,换个 upstream 服务器重试
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
# HTTP/1.1 + keepalive,复用后端连接,显著降低延迟
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# upstream 里也要配,否则 keepalive 不生效
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
keepalive 32; # 每个 worker 保持的空闲长连接数
}
为什么
proxy_set_header Connection "";? Nginx 默认用 HTTP/1.0 转发,且不传Connection头。设成空字符串是为了清掉客户端可能带来的close,配合proxy_http_version 1.1才能真正复用连接。忘了配 upstream 的keepalive是最常见的疏漏 ------ 只写proxy_http_version 1.1没用。
五、负载均衡:upstream 详解
5.1 五种调度策略
nginx
# 1. 轮询(默认):请求按顺序分发,适合后端配置一致
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
# 2. 加权轮询:weight 越大分到越多,适合机器配置不一致
upstream backend {
server 10.0.0.1:8080 weight=3; # 约 75%
server 10.0.0.2:8080 weight=1; # 约 25%
}
# 3. ip_hash:按客户端 IP 的哈希固定到某台机器
upstream backend {
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
# 4. least_conn:优先发给当前连接数最少的,适合长连接/耗时不均的请求
upstream backend {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
# 5. hash 自定义键(一致性哈希):按 URI 固定到后端,利于缓存命中
upstream backend {
hash $request_uri consistent;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
策略选型:
| 策略 | 适用场景 | 注意 |
|---|---|---|
| 轮询 | 后端无状态、配置一致 | 默认即可 |
| weight | 机器性能不均 | 权重差异别设太大(如 100:1),会导致热点 |
ip_hash |
需要会话保持、且不想引入 Redis Session | 客户端走移动网络 IP 会变,导致会话漂移;NAT 出口(公司、校园网同 IP)会导致严重负载不均;摘节点要用 down 而不是直接删,否则哈希空间重算 |
least_conn |
请求耗时差异大(如文件上传 + 普通查询混合) | 比轮询更均衡 |
hash ... consistent |
需要「同一类请求落到同一台」提升缓存命中 | consistent 用 Ketama 一致性哈希,节点增减时只迁移少量 key |
重要架构建议 :Java 后端不要依赖
ip_hash做会话保持。正确做法是把 Session 抽到 Redis(Spring Session),让后端彻底无状态。这样任意实例挂掉都不影响用户,扩缩容也自由。ip_hash 只作为兜底手段。
5.2 健康检查与故障摘除
nginx
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.3:8080 backup; # 备用机,主的全挂了才上
server 10.0.0.4:8080 down; # 手动标记下线,用于灰度摘除
}
max_fails=3:30 秒内失败 3 次就判定该节点不可用fail_timeout=30s:既是统计窗口,也是被摘除后的「冷静期」,30 秒后重新尝试backup:热备,正常情况下不参与调度down:永久下线(一般配合变量做动态控制)
⚠️ 开源版 Nginx 的被动健康检查有局限 :它只在真实请求失败时 才发现节点故障,也就是说必须有用户请求打过去撞上错误才会摘除。
如果需要主动健康检查 (定时探测
/actuator/health、按响应内容判断),有两个选择:
- 用 Nginx Plus(商业版)
- 用第三方模块
nginx_upstream_check_module,或者直接用 OpenResty / APISIX / Kong
Spring Boot 健康检查端点配合:
yaml
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: never # 对外只返回 UP/DOWN,不暴露细节
5.3 平滑发布:怎么优雅摘除一个节点
nginx
# 方案:改 weight=0 + reload(开源版可用)
upstream backend {
server 10.0.0.1:8080 weight=0; # 不再接收新请求,已有连接继续处理完
server 10.0.0.2:8080;
}
Nginx 的 reload 是平滑的:master 进程不变,重新加载配置后启动新 worker,老 worker 处理完手头连接才退出。所以线上改配置不会断连。
六、动静分离与静态资源
6.1 静态资源直接由 Nginx 返回
nginx
server {
listen 80;
server_name www.example.com;
root /data/frontend/dist;
index index.html;
# 前端 SPA 路由,刷新子路径不 404
location / {
try_files $uri $uri/ /index.html;
}
# 带 hash 的构建产物,长期强缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 接口走后端
location /api/ {
proxy_pass http://backend/;
}
}
try_files $uri $uri/ /index.html; 是 Vue/React SPA 的标配。 它依次尝试:文件 → 目录 → 兜底返回 index.html。没有这行,用户在 /user/profile 刷新页面就会 404,因为磁盘上根本没有这个路径。
6.2 缓存头策略
| 资源 | 策略 | 配置 |
|---|---|---|
| HTML(入口) | 不缓存或协商缓存 | add_header Cache-Control "no-cache" |
| JS/CSS(带 hash) | 一年强缓存 | expires 1y; add_header Cache-Control "public, immutable" |
| 图片 | 中等时长 | expires 30d |
为什么
immutable很重要 :浏览器遇到immutable后,即使在用户强制刷新时也不会对这类资源发起校验请求,能显著减少请求数。前提是你的构建产物文件名带内容 hash(app.a3f9c2.js)。
6.3 gzip 压缩(收益最明显的一项)
nginx
gzip on;
gzip_vary on;
gzip_min_length 1k; # 小于 1KB 不压,压了反而变大还浪费 CPU
gzip_comp_level 5; # 1~9,建议 4~6,再高压缩率收益极小但 CPU 翻倍
gzip_buffers 16 8k;
gzip_proxied any;
gzip_http_version 1.1;
# 注意:只压缩文本类,图片/音视频本身已压缩,再压是白费 CPU
gzip_types
text/plain
text/css
text/xml
application/javascript
application/json
application/xml
application/xml+rss
application/x-javascript
image/svg+xml;
# 禁用老版本 IE6 的 gzip
gzip_disable "MSIE [1-6]\.";
验证有没有生效:
bash
curl -H "Accept-Encoding: gzip" -I http://example.com/app.js
# 看响应头里有没有 Content-Encoding: gzip
一个 800KB 的 JS bundle 开启 gzip 后大约 200~250KB,通常是投入产出比最高的一步优化 。
更进一步的 Brotli(
br)压缩率比 gzip 高 15~20%,但需要额外编译模块,且只在 HTTPS 下被浏览器支持(HTTP 下 Chrome 不发br的 Accept-Encoding)。
6.4 sendfile 与零拷贝
nginx
sendfile on; # 静态文件走 sendfile,数据不经过用户态,直接从内核缓冲区到网卡
tcp_nopush on; # 配合 sendfile,攒够一个 MSS 再发,减少小包
tcp_nodelay on; # 针对 keepalive 连接,有小包就立刻发,降低延迟
tcp_nopush 和 tcp_nodelay 看起来矛盾,实际上同时开启是对的 :nopush 只在 sendfile 传输完整文件时生效(攒包),nodelay 只在 keepalive 连接的后续小包上生效(立即发)。这两个指令作用于不同场景,Nginx 官方也是这么建议的。
七、HTTPS 配置
7.1 完整配置
nginx
server {
listen 80;
server_name example.com www.example.com;
# HTTP 全量跳转 HTTPS,注意 301 会被浏览器永久缓存,调试期用 302
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on; # 较新版本写法;老版本是 listen 443 ssl http2
server_name example.com www.example.com;
ssl_certificate /etc/nginx/cert/example.com.pem;
ssl_certificate_key /etc/nginx/cert/example.com.key;
# 协议与套件:只留现代安全套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 会话复用,减少握手开销
ssl_session_cache shared:SSL:10m; # 约能存 4 万个会话
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling:省掉客户端去 CA 查证书状态的一次请求
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 114.114.114.114 valid=300s;
# HSTS:告诉浏览器以后只走 HTTPS(确认全站 HTTPS 后再开)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
7.2 为什么叫「SSL 卸载」
HTTPS 加解密是 CPU 密集型操作。放在 Nginx 这一层做,好处是:
- 后端 Tomcat 只处理明文 HTTP,省掉 TLS 握手和加解密开销
- 证书只需要在 Nginx 上维护(续期时不用重启 Java 服务)
- Nginx 内部转发是内网明文,性能更好
代价是内网段是明文传输,如果内网不可信(比如合规要求),需要在内部也开 TLS,或者保证网络隔离。
7.3 免费证书自动续期
bash
# 用 acme.sh
curl https://get.acme.sh | sh
acme.sh --issue -d example.com --webroot /data/frontend/dist
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/cert/example.com.key \
--fullchain-file /etc/nginx/cert/example.com.pem \
--reloadcmd "nginx -s reload"
# acme.sh 会自动加 crontab,每 60 天续期
Let's Encrypt 证书有效期是 90 天 ,所以自动续期是必须项,不是可选项。见过太多线上事故是「证书过期,全站 HTTPS 不可访问」。
加个监控:用
openssl s_client或者脚本定期查证书剩余天数,低于 15 天告警。
7.4 HTTP/2
HTTP/2 的多路复用(一个连接并发多个请求)能显著改善高延迟网络下的加载速度。
前提:必须开启 HTTPS。 所有主流浏览器只支持 h2c 之外的 h2(即基于 TLS 的 HTTP/2),明文 HTTP/2 浏览器不支持。
验证:
bash
curl -I --http2 https://example.com
# 看有没有 HTTP/2 200
八、限流与访问控制
8.1 请求速率限流(防刷)
nginx
# 在 http 块里定义限流区
# $binary_remote_addr 比 $remote_addr 省内存(4 字节 vs 15 字节)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
# burst=20:允许突发 20 个请求排队;nodelay:排队的不延迟,直接放行
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429; # 默认返回 503,改成 429 更语义化
proxy_pass http://backend;
}
}
burst 和 nodelay 的组合效果:
| 配置 | 行为 |
|---|---|
burst=20 |
超出的请求排队,按 rate 匀速放行,多的直接拒。响应会变慢但不会拒太多 |
burst=20 nodelay |
突发 20 个立即放行,之后按速率计算,超了拒。用户体验好,但瞬时压力大 |
| 不带 burst | 严格按 rate,超过立刻拒绝 |
针对登录接口单独限流(防止暴力破解):
nginx
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/m;
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://backend;
}
8.2 连接数限流
nginx
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
limit_conn conn_limit 20; # 每个 IP 最多 20 个并发连接
}
}
limit_req限的是速率 (每秒多少次),limit_conn限的是并发数(同时多少个)。防刷用前者,防慢连接攻击用后者。
8.3 IP 黑白名单
nginx
location /admin/ {
allow 192.168.1.0/24; # 内网网段
allow 10.0.0.5;
deny all; # 其它全拒,返回 403
}
经过 CDN / SLB 之后,
$remote_addr是 CDN 的 IP,白名单会失效。 需要用ngx_http_realip_module:
nginx
set_real_ip_from 10.0.0.0/8; # 可信代理网段
real_ip_header X-Forwarded-For; # 从哪个头取真实 IP
real_ip_recursive on; # 递归剥离可信代理,取到真正的客户端 IP
8.4 防盗链
nginx
location ~* \.(jpg|png|gif|webp)$ {
valid_referers none blocked example.com *.example.com;
if ($invalid_referer) {
return 403;
# 也可以返回一个「防盗链」提示图
# rewrite ^/ /static/forbidden.png;
}
}
valid_referers 参数含义:none 允许无 Referer(直接访问)、blocked 允许防火墙伪装过的 Referer。
8.5 Nginx 里 if 的陷阱
Nginx 官方有一句著名的话:"if is evil"。
原因是 if 在 location 块里会创建一个隐式的嵌套 location,导致 proxy_pass、try_files 等指令行为异常。
nginx
# ❌ 不推荐
location / {
if ($request_method = POST) {
proxy_pass http://a; # 这里的行为可能不符合预期
}
}
# ✅ 推荐:用 map 或者独立的 location
map $request_method $backend {
POST http://write_backend;
default http://read_backend;
}
location / {
proxy_pass $backend;
}
在 location 里,if 只安全地支持 return 和 rewrite ... last 这两种操作。 其它用法都要格外小心。
九、性能优化
9.1 进程与连接模型
nginx
# main 块
worker_processes auto; # 通常设为 CPU 核数,或 auto 自动检测
worker_rlimit_nofile 65535; # 提高单进程最大文件描述符数,别忘了系统的 ulimit
events {
use epoll; # Linux 下最高效的 IO 多路复用
worker_connections 65535; # 单个 worker 最大连接数
multi_accept on; # 一次性 accept 所有新连接
accept_mutex on; # 避免惊群,让 worker 轮流 accept
}
理论最大并发数 = worker_processes × worker_connections / 2(反向代理场景下一个客户端连接要占用一个到后端的连接,所以要除以 2;纯静态服务除以 1 再留点余量)。
worker_rlimit_nofile 改了还不够,系统层面也要配合:
bash
# /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
9.2 连接保持
nginx
http {
keepalive_timeout 65; # 客户端长连接保持时间
keepalive_requests 1000; # 单个长连接最多处理多少请求后关闭
# 与后端之间的长连接(前面讲过,别漏)
upstream backend {
server 127.0.0.1:8080;
keepalive 32;
}
}
keepalive_requests 适当调大能减少 TCP 握手次数,但太小会导致频繁建连,太大会让连接长时间占用(不利于负载再均衡)。1000~10000 都是合理范围。
9.3 日志优化
nginx
http {
log_format main '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'rt=$request_time urt=$upstream_response_time '
'upstream=$upstream_addr';
access_log /var/log/nginx/access.log main buffer=32k flush=5s;
# 静态资源不计日志,IO 压力大时收益明显
location /static/ {
access_log off;
}
}
强烈建议把 $request_time、$upstream_response_time、$upstream_addr 加进日志格式。 出问题时:
rt明显大于urt→ 时间花在接收客户端请求(客户端网络慢或上传大文件)rt约等于urt→ 瓶颈在后端服务upstream显示-→ 请求根本没转发到后端(被 Nginx 自己处理或拒了)
9.4 日志切割(必须做)
Nginx 自己不会切割日志,日志文件会无限增长。用 logrotate:
bash
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
关键是
kill -USR1------ 这个信号让 Nginx 重新打开日志文件 ,而不是重启。如果直接rm日志文件不通知 Nginx,进程会继续往已删除的 inode 写,磁盘空间不释放(经典的「删了文件但磁盘还是满」)。
十、常见错误码排查对照表
这是线上最实用的一张表,建议收藏。
| 状态码 | 含义 | 常见根因 | 排查方向 |
|---|---|---|---|
| 400 | Bad Request | 请求头过大(Cookie 巨多) | 调 large_client_header_buffers 4 16k |
| 403 | Forbidden | 文件权限 / deny 规则 / 目录无 index |
看 error.log 的 Permission denied;检查 autoindex |
| 404 | Not Found | root vs alias 混淆;SPA 没配 try_files |
看 error.log 里实际打开的文件路径 |
| 413 | Request Entity Too Large | 上传超过 client_max_body_size(默认 1m) |
调大 client_max_body_size 100m(别忘了后端 Tomcat 也有 max-file-size) |
| 499 | Client Closed Request | 客户端主动断开(Nginx 自定义码,非 HTTP 标准) | 后端太慢用户跑了,或者客户端超时设置比 Nginx 短 |
| 500 | Internal Server Error | 一般是后端抛异常 | 直接看后端日志 |
| 502 | Bad Gateway | 后端没启动 / 端口不对 / upstream 全挂 | telnet 后端端口;看 error.log 的 connect() failed (111: Connection refused) |
| 504 | Gateway Timeout | 后端处理超过 proxy_read_timeout |
优化接口,或针对慢接口单独放宽容忍 |
| 499 + 大量 502 | --- | 后端频繁重启 / GC 停顿 | 看 JVM 和后端监控 |
排查命令三板斧:
bash
# 1. 实时看错误日志
tail -f /var/log/nginx/error.log
# 2. 统计错误码分布
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# 3. 找出最慢的 20 个请求
awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head -20
499 是面试常问的一个点。 它是 Nginx 自己定义的状态码,不在 HTTP 规范里,含义是「客户端在 Nginx 返回响应之前就关闭了连接」。常见于:APP 端超时设得比服务端短、用户等不及刷新页面、爬虫抓取后立即断开。
大量 499 通常说明接口太慢,而不是 Nginx 有问题。
十一、WebSocket 与 SSE 配置
11.1 WebSocket
nginx
upstream ws_backend {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name ws.example.com;
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
# 核心:转发 Upgrade 协议升级头
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 长连接空闲超时,必须远大于客户端心跳间隔,否则会被断开
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
原理: WebSocket 通过 HTTP 的 Upgrade: websocket 头完成协议切换。Nginx 默认会转发请求但不转发 Upgrade 头,所以必须手动配上,否则握手失败(表现为连接立刻断开)。
如果想让
Connection头只在有 Upgrade 时才设为upgrade,可以用 map 动态处理:
nginxmap $http_upgrade $connection_upgrade { default upgrade; '' close; } proxy_set_header Connection $connection_upgrade;
11.2 SSE(Server-Sent Events)
nginx
location /api/sse {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 关键:关掉缓冲,否则消息会被攒着不发
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 3600s;
}
SSE 是流式响应,proxy_buffering 会攒够 proxy_buffer_size 才往客户端发,必须关掉。这个坑非常常见:本地能收到消息,上了 Nginx 就一直等。
十二、高可用:Nginx 自己挂了怎么办
Nginx 是单点,它挂了整个站点就不可用了。常见方案:
12.1 Keepalived + 双机热备(VIP 漂移)
虚拟 IP: 192.168.1.100
│
┌─────────┴─────────┐
▼ ▼
Nginx Master Nginx Backup
(MASTER, 优先级100) (BACKUP, 优先级90)
│ │
└────────┬──────────┘
▼
后端服务集群
两台 Nginx 通过 VRRP 协议互相心跳,主节点挂了 VIP 自动漂移到备机,切换时间 1~3 秒。
conf
# /etc/keepalived/keepalived.conf(主节点)
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -20 # 检测失败则优先级降 20,触发切换
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.100/24
}
track_script {
chk_nginx
}
}
bash
#!/bin/bash
# /etc/keepalived/check_nginx.sh
if ! pidof nginx > /dev/null; then
systemctl start nginx
sleep 2
if ! pidof nginx > /dev/null; then
exit 1 # 起不来就让 keepalived 降权切换
fi
fi
exit 0
12.2 更现代的做法
生产环境优先考虑:
- 云厂商的 SLB / ALB(阿里云 SLB、腾讯云 CLB、AWS ALB)------ 它们本身就是多可用区高可用的,你只需要挂多台 Nginx 到后端。这是最省事且最靠谱的方案。
- Kubernetes Ingress(Nginx Ingress Controller / APISIX Ingress)------ 容器化部署下由 K8s 保证多副本和自愈。
除非是自建机房或者特殊合规要求,否则不建议自己搭 Keepalived。运维成本和维护难度远高于收益,云 LB 一年几百块就解决了。
十三、Nginx 与现代网关怎么分工
学到这里你可能会问:有了 Spring Cloud Gateway / Kong / APISIX,还需要 Nginx 吗?
典型的分层:
客户端
↓
云 Load Balancer(四层,扛流量、跨可用区)
↓
Nginx(七层边缘)
· SSL 卸载、静态资源、gzip、限流、防刷、日志
↓
API 网关(Spring Cloud Gateway / APISIX / Kong)
· 鉴权、路由到微服务、熔断、灰度、插件化限流
↓
业务微服务
边界怎么划:
| 能力 | 放 Nginx | 放网关 |
|---|---|---|
| SSL 卸载 | ✅ 更合适 | 也行,但增加 Java 侧负担 |
| 静态资源 / gzip | ✅ 只能是它 | ❌ |
| 基础限流(按 IP) | ✅ 性能好,挡在最外层 | 也可以 |
| 鉴权(JWT 校验) | 勉强(需 Lua) | ✅ 天然合适 |
| 服务发现 / 动态路由 | ❌ 需 reload | ✅ 天然合适 |
| 熔断降级 | ❌ | ✅ |
| 灰度发布 | 可以(按 Cookie 分流) | ✅ 更灵活 |
结论:
- 中小项目 / 单体 + 多实例部署:Nginx 单挑就够,别引入网关增加复杂度
- 微服务项目:Nginx 做边缘(SSL + 静态 + 防刷),网关做业务路由,两者并存
- 简历项目:能说清楚这个分层,比堆组件名字更有说服力
十四、OpenResty:当需要写逻辑时
Nginx 配置是声明式的,遇到「根据请求头查 Redis 判断灰度」这类逻辑就力不从心。OpenResty 把 LuaJIT 嵌进 Nginx,让你能在 Nginx 里写脚本。
nginx
location /api/ {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local token = ngx.var.http_authorization
if not token then
ngx.exit(401)
end
-- 在 Nginx 层完成鉴权,没通过的请求根本到不了后端
}
proxy_pass http://backend;
}
典型用途: WAF、灰度分流、AB 测试、在网关层做鉴权、动态路由、边缘计算。
什么时候用: 只有当「Nginx 配置表达不了 + 性能要求高」时才上 OpenResty。大部分业务用 Spring Boot 写个鉴权服务反而更可维护。不要为了炫技引入 Lua。
十五、实践练习清单
Level 1 --- 基础(必做)
- Docker 起 Nginx,配一个静态站点,验证
root和alias的路径差异 - 起两个 Spring Boot 实例(8080/8081),用 upstream 做轮询负载均衡,在日志里确认两个实例都被打到
- 故意写错配置,跑
nginx -t看报错格式;再试一次nginx -s reload观察它如何保留旧配置 - 配置
location,验证=、^~、~、无修饰符四种匹配的实际优先级(每个配置打一个return 200 "x"就能测)
Level 2 --- 进阶(强烈建议)
- 起一个 Vue 前端项目,配
try_files解决刷新 404,配置/assets/强缓存和 gzip,用浏览器 Network 面板验证 - 用 acme.sh 申请免费证书,配 HTTPS + HTTP 自动跳转 + HSTS,用 SSL Labs 测一下评级
- 配
limit_req限流,用ab或wrk压测观察 429 返回:
bash
# ab 压测:并发 50,共 1000 个请求
ab -n 1000 -c 50 http://localhost/api/test
- 配 WebSocket 代理,写个简单的聊天 demo,验证握手能成功且长时间不掉线
- 上传一个 10MB 文件,先复现 413,再配
client_max_body_size解决
Level 3 --- 综合(写进简历)
- 完整搭建「Nginx + 2×Spring Boot + Redis 共享 Session」的多实例架构,手动 kill 一个实例,验证用户不掉线
- 给日志加上
$request_time和$upstream_response_time,写一个脚本分析慢接口 Top20 - 配置 logrotate 并验证日志正常切割(用
logrotate -d调试模式先试) - 用 Nginx +
map实现简单的灰度:按 Cookie 里的uid尾号分流到新旧两个版本
十六、面试高频题速查
Q:Nginx 为什么能支持高并发?
三个层次:
- 多进程 + 单线程:一个 master 管多个 worker,worker 之间无共享内存,不需要锁
- IO 多路复用(epoll):一个 worker 用 epoll 同时监听上万个连接,只有活跃连接才被处理。epoll 是事件驱动回调,复杂度 O(1),不像 select/poll 需要 O(n) 遍历
- 异步非阻塞:请求分阶段处理,等待上游响应时不阻塞,去处理其它请求
对比 Apache 的多线程/多进程模型:每连接占一个线程,内存开销大、上下文切换多,并发上万就吃力。
Q:Nginx 怎么处理一个请求?
接收请求 → 解析请求行和请求头
→ 匹配 server(按 listen 的 IP:port,再按 server_name)
→ 匹配 location(按第三节讲的优先级)
→ 执行该 location 的指令(rewrite → 访问控制 → 内容生成/代理)
→ 过滤并发送响应(gzip、header 处理)
→ 记录日志
Q:502 和 504 的区别?
- 502 Bad Gateway:Nginx 连不上后端,或者后端返回了无法解析的响应。常见原因是后端进程挂了、端口写错、upstream 节点全部被摘除。
- 504 Gateway Timeout :连上了,但后端在
proxy_read_timeout时间内没返回。是超时,不是连不上。
Q:怎么获取客户端真实 IP?
三层配合:
- Nginx:
proxy_set_header X-Real-IP $remote_addr;和X-Forwarded-For - 经过 CDN 时用
realip模块从可信代理网段里剥离出真实 IP - Spring Boot 侧:
server.forward-headers-strategy: native,或者自己写工具类取X-Forwarded-For的第一段
Q:root 和 alias 的区别?
root 把完整 URI(含 location 前缀)拼到路径后面;alias 剥掉 location 前缀再拼。alias 必须以 / 结尾,且只能用在 location 里。
Q:Nginx 怎么实现热部署 / 平滑升级?
- 改配置 :
nginx -s reload,master 不变,新 worker 用新配置,老 worker 处理完现有连接后退出 - 升级二进制 :给老 master 发
USR2信号启动新版本 master,新老并存;给老 master 发WINCH让其 worker 优雅退出;确认无误后QUIT老 master。全程不断连接。
Q:worker_processes 设多少合适?
一般设为 CPU 核数 或者 auto。Nginx 是事件驱动、非阻塞的,不像 CPU 密集型服务需要更多进程。设太多反而增加上下文切换和内存开销。
例外:如果有大量阻塞式操作(比如磁盘 IO 密集的静态文件服务,或者 OpenResty 里写了阻塞调用),可以适当多设一些。
Q:正向代理和反向代理的区别?
- 正向代理 :代理客户端,服务端不知道真实客户端是谁(VPN、公司内网代理)
- 反向代理 :代理服务端,客户端不知道真实服务器是谁(Nginx 代理 Tomcat)
核心区别是代理的对象不同,也决定了「谁不知道谁」。
十七、下一步学什么
Nginx 基础 ← 你在这里
↓
Lua / OpenResty(需要写逻辑时)
↓
API 网关(Spring Cloud Gateway / APISIX / Kong)------ 对比理解职责边界
↓
Kubernetes Ingress ------ 容器化下的流量入口
↓
可观测性(Prometheus + Grafana 监控 Nginx、链路追踪)
推荐资料:
- 官方文档 :
https://nginx.org/en/docs/------ 指令说明最权威,查任何指令都先看这个 - 《深入理解 Nginx:模块开发与架构解析》(陶辉)------ 讲原理讲得最透,想理解事件模型看这本
- Nginx 官方博客的 "Inside NGINX" 系列 ------ 讲进程模型和请求处理流程,图解很清晰
- Mozilla SSL Configuration Generator ------ 生成安全 TLS 配置的在线工具,别自己瞎配套件
小结
Nginx 的学习曲线有个特点:入门很简单(十几行配置就能跑),但细节特别多(一个斜杠之差就是 404)。
真正的门槛在三个地方:
- location 匹配优先级 ------ 理解「精确 > ^~ > 正则有序 > 最长前缀」这个链条,90% 的路由问题都能自己解出来
- proxy_pass 的 URI 替换规则 ------ 记住「把 proxy_pass 值当替换模板」,带不带斜杠的区别就清楚了
- 错误码定位能力 ------ 413、499、502、504 这四个码,能不看文档说出根因,才算真的会
最后一点建议:别把 Nginx 当黑盒。 它就在你服务的入口,出现问题时第一反应应该是「去 error.log 看一眼」,而不是「这个我搞不定,找运维」。能看到日志、能定位问题,这本身就是后端工程师的核心竞争力。
上一篇:《Redis 学习指南:从会用 set/get 到聊透原理》
下一篇预告:《消息队列该怎么选:Kafka 与 RocketMQ 对比实战》
本篇完 · 有任何问题欢迎在评论区讨论