Nginx 学习指南

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、按响应内容判断),有两个选择:

  1. 用 Nginx Plus(商业版)
  2. 用第三方模块 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 动态处理:

nginx 复制代码
map $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 更现代的做法

生产环境优先考虑:

  1. 云厂商的 SLB / ALB(阿里云 SLB、腾讯云 CLB、AWS ALB)------ 它们本身就是多可用区高可用的,你只需要挂多台 Nginx 到后端。这是最省事且最靠谱的方案。
  2. 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 为什么能支持高并发?

三个层次:

  1. 多进程 + 单线程:一个 master 管多个 worker,worker 之间无共享内存,不需要锁
  2. IO 多路复用(epoll):一个 worker 用 epoll 同时监听上万个连接,只有活跃连接才被处理。epoll 是事件驱动回调,复杂度 O(1),不像 select/poll 需要 O(n) 遍历
  3. 异步非阻塞:请求分阶段处理,等待上游响应时不阻塞,去处理其它请求

对比 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?

三层配合:

  1. Nginx:proxy_set_header X-Real-IP $remote_addr; 和 X-Forwarded-For
  2. 经过 CDN 时用 realip 模块从可信代理网段里剥离出真实 IP
  3. 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)。

真正的门槛在三个地方:

  1. location 匹配优先级 ------ 理解「精确 > ^~ > 正则有序 > 最长前缀」这个链条,90% 的路由问题都能自己解出来
  2. proxy_pass 的 URI 替换规则 ------ 记住「把 proxy_pass 值当替换模板」,带不带斜杠的区别就清楚了
  3. 错误码定位能力 ------ 413、499、502、504 这四个码,能不看文档说出根因,才算真的会

最后一点建议:别把 Nginx 当黑盒。 它就在你服务的入口,出现问题时第一反应应该是「去 error.log 看一眼」,而不是「这个我搞不定,找运维」。能看到日志、能定位问题,这本身就是后端工程师的核心竞争力。

上一篇:《Redis 学习指南:从会用 set/get 到聊透原理》

下一篇预告:《消息队列该怎么选:Kafka 与 RocketMQ 对比实战》


本篇完 · 有任何问题欢迎在评论区讨论

相关推荐
HAHAXX81 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa
零基础1232 小时前
LLM Agent 驱动的物模型构建:从设备手册到边缘接入的自动化实践
运维·人工智能·经验分享·python·自动化
程序猿老A3 小时前
从入门型到企业型:云服务器开放共享型到独享型规格升级
运维·服务器
Wang's Blog3 小时前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
Ruiery4 小时前
Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC
linux·运维·服务器
吴声子夜歌4 小时前
Nginx应用与运维——Nginx日志管理
java·运维·nginx
打工仔折腾 AI4 小时前
用UU远程把家里电脑变成AI Agent常驻服务器:CLI、端口映射与代理实测
运维·服务器·人工智能·后端·python·电脑·ai agent 实战
guo_wen_qiang5 小时前
云服务器nacos搭建-集群&持久化
java·运维·服务器
牢姐与蒯5 小时前
一点小碎片——每个线程独有的东西——上下文+栈
linux·运维·服务器
Elastic 中国社区官方博客5 小时前
错误最多的服务运行正常:使用 ES|QL 从日志进行根因分析
大数据·运维·数据库·sql·elasticsearch·搜索引擎·全文检索