POST · DevOps · 文章

nginx 从入门到进阶

2026/11/13 · by ZHANG XING YUN · 阅读 2 次 · 7522 字 · 预计阅读 19 分钟

nginx 从入门到进阶

本文面向已经会部署前端项目、但没系统学过 nginx 的开发者。内容按"先能用,再理解,最后调优"的顺序组织,覆盖静态站点、反向代理、负载均衡、HTTPS、缓存、限流与排错。

文中所有配置都放在配套示例工程里,可以直接启动验证(见附录 A)。标注为"实测"的结果,均来自该示例的运行输出,命令附在对应小节,读者可以自行复现。

  • 适用对象:需要部署前端产物、反向代理后端接口、处理 HTTPS 的开发者
  • 运行环境:Docker(示例使用 nginx 1.27-alpine、Node 20);Linux 包管理安装方式在第二节说明
  • 前置知识:HTTP 基础(请求方法、状态码、请求头/响应头)

一、nginx 是什么

1.1 定位

nginx 是一个 HTTP 服务器和反向代理服务器。请求到达业务代码之前,通常先经过它:

浏览器 ──► nginx ──► 静态文件(直接返回)
                └──► 后端服务(反向代理后返回结果)

它承担的是"接入层"职责:静态资源托管、请求转发、TLS 卸载、压缩、缓存、限流、负载均衡。业务逻辑仍由后端服务处理。

1.2 进程模型

         ┌──────────────┐
         │   master     │  读取配置、管理 worker、处理信号
         └──────┬───────┘
     ┌──────────┼──────────┐
┌────▼────┐ ┌───▼─────┐ ┌──▼──────┐
│ worker1 │ │ worker2 │ │ workerN │  实际处理请求,数量通常等于 CPU 核数
└─────────┘ └─────────┘ └─────────┘

两个直接结论:

  • reload 不中断服务:master 加载新配置后启动新 worker,旧 worker 处理完手头请求再退出,所以线上修改配置不需要停服。
  • 高并发来自事件驱动:每个 worker 使用 epoll/kqueue 处理连接,单 worker 可维持数千连接,不需要一个请求一个线程。

1.3 反向代理与正向代理

由谁部署 代理对象 典型场景
正向代理 客户端 客户端 企业出口代理
反向代理 服务端 服务器 nginx 将 /api 转发给后端

nginx 做的是反向代理:客户端以为在和 nginx 通信,实际响应由后端产生。

1.4 能力边界

适合交给 nginx 不适合交给 nginx
静态资源托管 复杂业务逻辑(鉴权、数据拼装)
反向代理、隐藏后端端口 作为应用服务器运行 Node/PHP
负载均衡 大量条件分支的 rewrite 规则
TLS 终止、HTTP/2 需要读写数据库的操作
压缩、缓存、限流 实时计算、长事务

常见的过度设计是把业务判断写进 nginx(大量 if 和 rewrite),后期难以维护。能在应用层解决的问题尽量留在应用层。


二、安装与运行

2.1 Linux(以 Ubuntu 为例)

sudo apt update
sudo apt install -y nginx
nginx -v
systemctl status nginx

目录约定:

/etc/nginx/
├─ nginx.conf            # 主配置
├─ conf.d/               # 站点配置(通常被 include)
├─ sites-available/      # Debian/Ubuntu 的站点目录
├─ sites-enabled/        # 软链接到这里才生效
└─ mime.types            # 扩展名与 Content-Type 的映射
/var/www/html/           # 默认站点根目录
/var/log/nginx/          # access.log、error.log

建议一个站点一个配置文件,放在 conf.d/ 下。sites-available / sites-enabled 是 Debian 系的约定,用或不用都可以。

2.2 Docker

本地或容器化部署时,直接用官方镜像更省事,也便于和线上保持一致:

docker run --rm -p 8080:80 nginx:1.27-alpine

浏览器访问 http://localhost:8080,看到默认欢迎页即安装成功。完整的多服务示例见附录 A。

2.3 常用命令

sudo nginx -t                 # 校验配置语法(修改后必做)
sudo nginx -T                 # 打印 include 展开后的最终配置
sudo nginx -s reload          # 热加载
sudo nginx -s reopen          # 重新打开日志文件(配合日志切割)
sudo systemctl reload nginx   # systemd 管理下的等价写法

nginx -t 只检查语法和文件是否存在,不校验路径语义和正则逻辑。路径写错、正则写错它都会通过,这两类问题通常要到请求发生时才暴露。

nginx -T 用于确认"实际生效的配置",排查"改了没生效"时比逐个文件翻更有效。


三、配置文件的结构

3.1 上下文(context)

nginx 配置是树形结构,指令只能写在允许的上下文里:

main                      ← 最外层:worker_processes、user、pid
├─ events { }             ← 连接相关:worker_connections
└─ http {                 ← HTTP 相关:log_format、gzip、upstream
   ├─ server { }          ← 一个虚拟主机(域名 + 端口)
   │  └─ location { }     ← 一个 URL 前缀或正则
   └─ server { }
   }

例如把 gzip on; 写进 server 块,nginx -t 会报 "gzip" directive is not allowed here,这就是上下文限制。

3.2 最小可运行配置

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

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile      on;

    server {
        listen 80;
        server_name _;              # _ 表示匹配任意域名
        root  /usr/share/nginx/html;
        index index.html;

        location / {
            try_files $uri $uri/ =404;
        }
    }
}

3.3 阅读一份配置的方法

拿到任意 nginx 配置,先回答四个问题:

  1. 监听哪个端口、哪个域名?—— listen、server_name
  2. 站点根目录在哪?—— root 还是 alias
  3. 请求由哪个 location 处理?—— 匹配规则见第四节
  4. 处理方式是返回文件还是转发?—— try_files 或 proxy_pass

四、location 匹配规则

4.1 五种写法

写法 含义 示例
location = /a 精确匹配,完全相等才命中 只匹配 /a
location ^~ /static/ 前缀匹配,命中后不再检查正则 /static/x.js
location ~ \.php$ 正则匹配,区分大小写 按书写顺序,先命中先生效
location ~* \.(jpg|png)$ 正则匹配,忽略大小写 /a.JPG 也命中
location /api/ 普通前缀匹配,最长前缀优先 /api/、/api/x

4.2 优先级

1. = 精确匹配            ← 命中即结束
2. ^~ 前缀匹配           ← 命中即结束,不再检查正则
3. ~ / ~* 正则           ← 按书写顺序,第一个命中的生效
4. 普通前缀匹配          ← 选择最长的一个
5. location /            ← 兜底

记忆方式:精确 > ^~ > 正则 > 最长前缀 > 兜底。

容易出错的一点:正则优先于普通前缀。因此 location /api/ 会被 location ~ \.json$ 抢占 /api/user.json。接口路径建议写成 location ^~ /api/,明确跳过正则检查。

4.3 验证命中结果

在示例工程中分别请求不同类型的路径,观察状态码即可判断命中了哪个 location:

curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/assets/app.js      # 200,命中静态资源正则
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/api/info           # 200,命中 /api/ 前缀
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/some/spa/route     # 200,命中 location /
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/assets/missing.js  # 404,命中静态资源正则并 =404

最后一条尤其需要注意:静态资源 location 里如果写成 try_files $uri /index.html,缺失文件会返回 HTML 且状态码为 200,浏览器把 HTML 当 JS 执行后报语法错误。静态资源应使用 =404。

4.4 root 与 alias

# root:把 location 前缀拼到路径后面
location /img/ {
    root /data;          # 请求 /img/a.png → /data/img/a.png
}

# alias:替换掉 location 前缀
location /img/ {
    alias /data/static/; # 请求 /img/a.png → /data/static/a.png
}

两个注意点:

  1. alias 结尾必须带 /,且要与 location 的前缀对齐,否则路径拼接会出错。
  2. alias 用在正则 location 中时必须使用捕获组:alias /data/$1;。

实际项目中优先使用 root,可减少一类路径问题。

4.5 try_files

location / {
    try_files $uri $uri/ /index.html;
}

含义是按顺序尝试:先找同名文件,再找同名目录,都不存在则内部转发到 /index.html。内部转发不会改变浏览器地址栏,这是单页应用刷新不 404 的实现方式。

实测(示例工程):

curl -s -o /dev/null -w "status=%{http_code} type=%{content_type}\n" https://localhost:8443/some/spa/route
# status=200 type=text/html

五、部署静态站点与单页应用

5.1 文件布局

/data/www/my-app/
├─ index.html
└─ assets/
   ├─ index-Df76S7DV.js      # 文件名带内容 hash
   └─ index-Df76S7DV.css

5.2 配置模板

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

    root  /data/www/my-app;
    index index.html;

    # 1) 带 hash 的静态资源:长缓存
    location ~* \.(?:js|css|woff2?|png|jpe?g|gif|svg|ico|webp)$ {
        add_header Cache-Control "public, max-age=31536000, immutable" always;
        access_log off;
        try_files $uri =404;
    }

    # 2) index.html 不缓存,保证版本可更新
    location = /index.html {
        add_header Cache-Control "no-cache" always;
    }

    # 3) 单页应用路由回退
    location / {
        try_files $uri $uri/ /index.html;
    }
}

index.html 单独配置的原因:页面里引用的是带 hash 的 JS/CSS,只要 HTML 是最新的,资源自然是最新的。如果 HTML 被长缓存,用户会持续加载旧资源,表现为"发布了新版本但用户仍看到旧页面"。

5.3 验证响应头

curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js
HTTP/1.1 200 OK
ETag: W/"6af70172-77c"
Cache-Control: public, max-age=2592000, immutable
Content-Encoding: gzip

两个常见配置问题:

  • expires 与 add_header Cache-Control 同时使用时,响应里会出现两个 Cache-Control(expires 自身也会生成一个 max-age)。二者选其一即可。
  • location 中出现 add_header 后,不再继承 server 级的 add_header。安全响应头要么写在每个 location 里,要么统一加 always 参数,不能假设"server 级配置了就在所有 location 生效"。

5.4 gzip

gzip              on;
gzip_vary         on;
gzip_comp_level   5;
gzip_min_length   1024;
gzip_proxied      any;
gzip_types        text/plain text/css application/json
                  application/javascript application/xml image/svg+xml;

默认 gzip_types 只包含 text/html,JS、CSS、JSON 需要显式添加,否则会出现"配置了 gzip 但没有效果"。

验证要看响应头:

curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js
# Content-Encoding: gzip

brotli 压缩率更高,但官方镜像不包含 brotli 模块,需要自行编译或通过 CDN 提供。gzip 已能满足大多数场景。

5.5 子路径部署

站点部署在 https://example.com/admin/ 时:

server {
    listen 80;
    server_name example.com;
    root /data/www;

    location /admin/ {
        alias /data/www/admin/;
        index index.html;
        try_files $uri $uri/ /admin/index.html;
    }
}

前端构建也要同步设置公共路径(Vite 的 base、webpack 的 publicPath)。两侧路径不一致会导致白屏和大量 404。


六、反向代理

6.1 proxy_pass 结尾斜杠的区别

# A:不带斜杠,原样转发 URI
location /api/ {
    proxy_pass http://127.0.0.1:3000;
}
# 请求 /api/user → 后端收到 /api/user

# B:带斜杠,替换掉 location 前缀
location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}
# 请求 /api/user → 后端收到 /user

规则:带 / 表示截掉 location 前缀再转发。配置前先确认后端期望的路径形式。

6.2 必要的请求头

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;

缺少这些头的后果:

  • Host 会是内部地址(如 api1:3000),后端生成的回调地址、重定向地址不正确。
  • 后端拿到的客户端 IP 全是 nginx 的地址,访问日志、限流、风控失效。
  • 后端无法判断原始协议是 HTTP 还是 HTTPS,可能出现重定向循环。

示例后端会把收到的头回显出来,便于确认:

"upstreamHeaders": {
    "host": "localhost",
    "x-real-ip": "172.20.0.1",
    "x-forwarded-for": "172.20.0.1",
    "x-forwarded-proto": "https"
}

6.3 三个超时

proxy_connect_timeout 3s;    # 与上游建立连接的超时
proxy_send_timeout    30s;   # 发送请求的超时
proxy_read_timeout    30s;   # 等待上游响应的超时(最常调整)

后端全部不可用时,访问日志中的上游耗时字段可以说明失败过程:

"GET /api/info HTTP/1.1" 504 160 ... rt=6.009 uct=-, - uht=-, - urt=3.003, 3.006

urt=3.003, 3.006 表示两台上游各自耗时 3 秒(连接超时),总耗时 6 秒后返回 504。这也解释了"后端挂掉后接口要等若干秒才报错"的现象:等待时间由 proxy_connect_timeout × 上游数量 决定。

6.4 故障转移

proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;

实测(示例工程):停止 api-1 后连续请求,全部由 api-2 返回 200:

第1次 -> api-2
第2次 -> api-2
第3次 -> api-2
第4次 -> api-2

api-1 恢复并超过 fail_timeout 后,请求重新在两台之间分配:

第1次 -> api-2
第2次 -> api-1
第3次 -> api-2
第4次 -> api-1

需要注意语义:proxy_next_upstream 是"换一台上游重试",只对幂等请求安全。如果 POST 请求已被后端处理但响应超时,重试会造成重复提交。生产环境建议 POST 仅允许 error(连接失败)重试,不允许 timeout 重试。

6.5 WebSocket

location /ws/ {
    proxy_pass http://app_cluster;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 60s;
}

三个条件缺一不可:HTTP/1.1、Upgrade 头、Connection: upgrade。缺任一条会连接失败或立即断开。proxy_read_timeout 需要根据心跳间隔适当放大。

6.6 上传大小限制

client_max_body_size 20m;    # 默认 1m

默认只有 1MB,超出返回 413。这是"本地能上传、线上上传失败"的常见原因。


七、负载均衡

7.1 upstream 与调度算法

upstream app_cluster {
    least_conn;
    server api1:3000 max_fails=2 fail_timeout=5s;
    server api2:3000 max_fails=2 fail_timeout=5s;
    keepalive 16;
}
算法 写法 适用场景
轮询(默认) 不写 后端性能一致
加权 server a weight=3; 机器配置不同
最少连接 least_conn; 请求耗时差异大
IP 哈希 ip_hash; 需要会话绑定
一致性哈希 hash $request_uri consistent; 提升缓存命中率

7.2 健康检查

开源版 nginx 没有主动健康检查,只有被动摘除:

  • max_fails=2:连续失败 2 次后标记为不可用。
  • fail_timeout=5s:5 秒后重新尝试。

因此"后端挂掉后需要等待一段时间才摘除"是正常行为。主动健康检查需要 Nginx Plus(商业版)或第三方模块(需重新编译)。

7.3 轮询验证

连续 6 次请求,结果在两台后端间交替:

第1次 -> api-1
第2次 -> api-2
第3次 -> api-1
第4次 -> api-2
第5次 -> api-1
第6次 -> api-2

7.4 会话保持

ip_hash 按客户端 IP 取模分配,但存在明显限制:

  • 同一出口 IP 的用户会集中到一台后端。
  • 移动网络切换导致 IP 变化时,会话丢失。
  • 负载容易不均。

更稳妥的做法是让后端无状态(session 存 Redis),或使用 cookie 粘滞方案(需要第三方模块或在应用层实现)。ip_hash 适合作为临时方案。

7.5 keepalive 长连接

upstream app_cluster {
    server api1:3000;
    keepalive 16;
}

location /api/ {
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_pass http://app_cluster;
}

三行必须同时存在。只配置 keepalive 16 而缺少后两行时,长连接不会生效。


八、HTTPS 与 HTTP/2

8.1 证书来源

来源 成本 适用场景
Let's Encrypt(certbot) 免费,90 天自动续期 个人项目、小团队
云厂商证书 免费 DV 或付费 已备案的国内站点
自签证书 免费 本地开发与测试

示例工程使用自签证书(conf/certs/dev.crt、dev.key),仅用于本地启动 HTTPS 环境,不能用于线上。

8.2 配置

server {
    listen 80;
    server_name example.com;

    # Let's Encrypt 续期验证路径
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    http2 on;                      # nginx ≥ 1.25.1
    server_name example.com;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    # 站点配置
}

旧写法 listen 443 ssl http2; 在 1.25.1 之后会提示 deprecated,应改为独立的 http2 on;。

8.3 验证 HTTP/2

部分 curl 构建未包含 HTTP/2 支持(curl: option --http2: the installed libcurl version does not support this),可以改用 Node 的 http2 模块:

node -e "
const http2 = require('http2');
const c = http2.connect('https://localhost:8443', { rejectUnauthorized: false });
c.on('connect', () => { console.log('ALPN =', c.alpnProtocol); c.close(); });
"
# ALPN = h2

访问日志中会记录为 HTTP/2.0:

172.20.0.1 - - [...] "GET / HTTP/2.0" 200 1075 ...

8.4 HSTS

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

只有在确认全站 HTTPS 可用后再启用。启用后浏览器会在 max-age 期间强制使用 HTTPS,配置失误时恢复成本较高。

8.5 证书续期

sudo certbot certonly --webroot -w /var/www/certbot -d example.com
sudo nginx -s reload

certbot 的定时任务会自动续期,但续期后需要 reload,否则 nginx 仍在使用旧证书,表现为"证书已续期但仍然过期"。


九、缓存

9.1 浏览器缓存

# 带 hash 的静态资源
add_header Cache-Control "public, max-age=31536000, immutable" always;

# HTML 入口
add_header Cache-Control "no-cache" always;

no-cache 的含义是"使用前回源确认",不是"不缓存"。真正不缓存是 no-store。

常见资源配置:

资源 策略 原因
index.html no-cache 它更新后整个应用才会更新
带 hash 的 JS/CSS max-age=31536000, immutable 内容变化会改变文件名
不带 hash 的图片 max-age=86400 折中
接口 GET 不缓存或很短 保证数据新鲜度

9.2 代理缓存

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

location /api/ {
    proxy_cache            api_cache;
    proxy_cache_valid      200 302 60s;
    proxy_cache_valid      404      10s;
    proxy_cache_key        $scheme$request_method$host$request_uri;
    proxy_cache_use_stale  error timeout updating;
    add_header             X-Cache-Status $upstream_cache_status;
}

X-Cache-Status 会返回 HIT、MISS、EXPIRED、UPDATING,没有这个响应头很难判断缓存是否生效。

带用户身份的接口不能缓存,否则会出现越权读取他人数据的问题。建议 /api/ 默认不缓存,只对明确无状态的接口单独开启。

9.3 三层缓存的关系

浏览器缓存(Cache-Control) → nginx 代理缓存(proxy_cache) → 后端与数据库

排查缓存问题时,先确定当前命中哪一层。有时修改 Cache-Control 无效,是因为 CDN 在更外层缓存。


十、限流与基础安全

10.1 limit_req

# http 块:定义限流规则
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;

# location 块:应用规则
location /api/ {
    limit_req zone=api_limit burst=10 nodelay;
    limit_req_status 429;
}
  • rate=5r/s:平均每 0.2 秒放行一个请求。
  • burst=10:允许瞬时超出的排队数量。
  • nodelay:突发请求立即处理,不排队等待(不加会显著增加延迟)。
  • limit_req_status 429:默认拒绝返回 503,改为 429 语义更准确。

实测(示例工程,30 次连续请求):

200 -> 17 次
503 -> 13 次

结果说明限流不是"超过 5 个就全部拒绝",而是按速率补充加突发额度。

10.2 limit_conn

limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

location /download/ {
    limit_conn conn_limit 2;    # 同一 IP 最多 2 个并发连接
}

10.3 基础加固

server_tokens off;                                    # 不暴露版本号

location ~ /\.(?!well-known) {                        # 禁止访问 .git、.env 等
    deny all;
    return 404;
}

if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
    return 405;
}

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;

if ($request_method ...) 这种简单判断是安全的;复杂条件判断见第十一节。


十一、变量、map、rewrite 与 if

11.1 常用内置变量

变量 含义
$host / $http_host 请求域名,$host 不含端口,更安全
$request_uri 原始完整 URI(含参数)
$uri 当前 URI(经过 rewrite 后可能变化)
$args / $query_string 查询串
$remote_addr 客户端 IP(有代理时是代理 IP)
$http_x_forwarded_for 代理链上的客户端 IP 列表
$scheme http 或 https
$request_method 请求方法
$status 响应状态码
$request_time 请求总耗时
$upstream_response_time 上游响应耗时
$upstream_addr 实际处理请求的上游地址
$upstream_cache_status 缓存命中状态

11.2 map

# 按 UA 分流
map $http_user_agent $site_root {
    default            /data/www/pc;
    ~*Mobile           /data/www/mobile;
}

# 按请求头开关功能
map $http_x_debug $enable_debug {
    default 0;
    "1"     1;
}

server {
    root $site_root;
}

map 在 http 块中定义一次即可处处使用,不产生 if 的副作用,是条件分支的首选方式。

11.3 rewrite 与 return

# 301 永久跳转
rewrite ^/old/(.*)$ /new/$1 permanent;

# 整站跳转优先使用 return
return 301 https://new.example.com$request_uri;

区别:

  • rewrite 会重新执行 location 匹配,条件复杂时可能造成循环。
  • return 直接返回,不重新匹配。

能用 return 就不使用 rewrite。

11.4 if 的使用限制

nginx 官方文档标注 "if is evil",原因是:

  • if 在 location 中会创建隐式嵌套 location,try_files 可能失效。
  • if 块中的 add_header 不一定生效,继承规则被破坏。

建议 if 只用于 return 或 rewrite,其余条件分支使用 map 或拆分成独立 location。

11.5 sub_filter

location / {
    sub_filter '</head>' '<script src="/runtime-config.js"></script></head>';
    sub_filter_once on;
    sub_filter_types text/html;
}

常见用途是同一份前端产物注入不同环境的运行时配置。注意 sub_filter 处理的是未压缩内容,与 gzip 同时使用时需确认生效顺序。

11.6 动态 upstream

resolver 127.0.0.11 valid=10s;      # Docker 内置 DNS;线上可改为内网 DNS

location ~ ^/tenant/(?<tenant>[a-z0-9-]+)/ {
    set $backend "http://${tenant}.internal:3000";
    proxy_pass $backend;
}

使用变量后 upstream 的 keepalive 失效,且必须配置 resolver,否则启动时会报 no resolver defined。


十二、stream:四层代理

HTTP 层无法转发 MySQL、Redis、MQ 等 TCP 协议,stream 块负责四层转发:

stream {
    upstream mysql_backend {
        server 10.0.0.11:3306;
        server 10.0.0.12:3306;
    }

    server {
        listen 3306;
        proxy_pass mysql_backend;
        proxy_connect_timeout 3s;
        proxy_timeout 300s;
    }
}

注意事项:

  • stream 与 http 平级,不能写在 http 内部。
  • 超时使用 proxy_timeout,四层没有 proxy_read_timeout。
  • 官方 Docker 镜像包含 stream 模块;Debian 的 nginx-light 不包含,需安装 nginx-full。

十三、性能相关配置

worker_processes auto;              # 通常等于 CPU 核数
worker_connections 1024;            # 每个 worker 的最大连接数
worker_rlimit_nofile 65535;         # 每个 worker 可打开的文件句柄数

sendfile        on;                 # 零拷贝发送静态文件
tcp_nopush      on;                 # 与 sendfile 配合,攒满数据包再发送
tcp_nodelay     on;                 # keepalive 连接下禁用 Nagle
keepalive_timeout 65;
client_header_timeout 15s;
client_body_timeout   15s;

几点说明:

  1. worker_connections 是每个 worker 的上限,理论并发约为 worker_processes × worker_connections。作为反向代理时,一个客户端请求会占用两个连接(客户端与上游),实际并发要减半。
  2. tcp_nopush 作用于 sendfile 的响应;tcp_nodelay 作用于 keepalive 连接,二者并不冲突。
  3. proxy_buffering on 是默认值,会先将上游响应缓冲再发送,可释放后端连接。流式接口(SSE、大文件下载)需关闭,否则会出现数据不输出的情况。
  4. 压测时不要只看 QPS,还要看 $request_time 分布、$upstream_response_time,以及 error.log 中是否有 worker_connections are not enough。

压测示例:

wrk -t4 -c200 -d30s --latency https://example.com/

日志切割

nginx 不会自动切割日志,通常借助 logrotate:

sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.1
sudo nginx -s reopen

容器环境中可以直接通过 docker compose logs 查看,或把日志目录挂载出来自行管理。


十四、排错

14.1 排查顺序

  1. nginx -t 检查语法与文件是否存在。
  2. curl -I 查看状态码与响应头,先区分是 nginx 的问题还是后端的问题。
  3. 查看 error.log 最后若干行。
  4. 查看 access.log 中的 rt、urt、upstream_addr。
  5. 确认配置确实生效:是否 reload、是否修改了被 include 的文件(可用 nginx -T 确认)。

14.2 常见状态码

状态码 常见原因 处理方向
400 请求头或请求行不合法 检查 client_header_* 配置与客户端行为
403 目录权限不足、缺少 index、deny all 检查文件权限与 autoindex
404 路径错误、root/alias 用错、try_files 未配置 对照请求 URI 与磁盘路径
413 上传超过 client_max_body_size 调大该值
499 客户端主动断开(nginx 特有状态码),通常因后端响应过慢 优化后端耗时
502 上游连接失败或返回非法响应 检查后端进程、端口、容器网络
504 上游响应超时 查看 urt,调整 proxy_read_timeout 或优化后端

14.3 502 与 504 的区别

  • 502 Bad Gateway:连接建立后,上游返回的内容不合法(进程崩溃、端口未监听、返回非 HTTP 内容)。
  • 504 Gateway Timeout:连接已建立并发出请求,但等待响应超时。

在同一示例中停止全部后端,返回的是 504,日志为:

"GET /api/info HTTP/1.1" 504 160 ... rt=6.009 uct=- uht=- urt=3.003, 3.006

原因是容器停止后连接表现为超时而非拒绝。因此"后端不可用"既可能是 502 也可能是 504,需要结合 error.log 与 urt 判断,不能只凭状态码下结论。

14.4 配置未生效的常见原因

  1. 没有执行 nginx -s reload。
  2. 修改的文件没有被 include。
  3. 浏览器或 CDN 缓存了旧响应。
  4. 上游存在多个实例,只重启了其中一个。

nginx -T 可以输出 include 展开后的完整配置,用于确认最终生效内容。


十五、上线检查清单

  • nginx -t 通过,nginx -T 中能看到预期的 server 与 location
  • index.html 为 no-cache,静态资源为长缓存
  • 单页应用深层路由刷新正常(try_files 生效)
  • gzip 生效(响应头包含 Content-Encoding: gzip)
  • HTTPS 证书链完整,curl -I https://... 无证书错误
  • HTTP 到 HTTPS 的 301 跳转正常
  • client_max_body_size 满足业务上传需求
  • 反向代理已透传 Host 与 X-Forwarded-*
  • 上游配置了 max_fails / fail_timeout,POST 请求未开启 timeout 重试
  • 访问日志包含 $request_time 与 $upstream_response_time,且已配置切割
  • server_tokens off
  • 修改前已备份旧配置

十六、FAQ

Q1:root 和 alias 怎么选?

优先 root。仅当 URL 前缀与磁盘路径不一致时使用 alias,并保证结尾斜杠对齐。

Q2:为什么 location ~ \.js$ 会抢走 /api/xxx.js?

正则优先于普通前缀。将该接口 location 改为 location ^~ /api/ 即可避免。

Q3:proxy_pass 带不带斜杠?

带斜杠会截掉 location 前缀;不带则原样转发。取决于后端期望的路径。

Q4:后端拿到的客户端 IP 为什么是内网地址?

未配置 X-Real-IP / X-Forwarded-For,后端只能看到 nginx 或容器网关的地址。

Q5:if 能不能用?

只用于 return / rewrite 是安全的;用于修改 root、proxy_pass 等会出问题,应改用 map。

Q6:reload 会断开连接吗?

不会。旧 worker 处理完当前请求后退出,属于平滑重启。但配置有语法错误时 reload 会失败并保持旧配置,因此应先执行 nginx -t。

Q7:index.html 修改后用户仍是旧页面?

HTML 被缓存。检查是否在 server 级写了 expires,导致 HTML 也被长缓存。

Q8:静态资源报 404,但文件确实存在?

多为 root 与 alias 混用,或 location 前缀被拼进了路径。对照请求 URI 与磁盘路径即可。

Q9:limit_req 配置后没有效果?

检查 limit_req_zone 是否定义(在 http 块)以及 location 是否引用;同时确认 rate 单位写法为 5r/s。

Q10:容器内 nginx 转发 localhost:3000 为什么不通?

容器内的 localhost 指向容器自身。应使用 host.docker.internal(Windows/macOS)或 compose 服务名(如 api1:3000)。


十七、版本与镜像选型

版本 变化点 影响
1.25.1+ listen ... http2 改为 http2 on; 旧写法提示 deprecated
1.25+ http2 不再强制与 ssl 绑定 支持明文 HTTP/2
1.19+ 支持 ssl_early_data 可实现 0-RTT,需防重放
1.9.5+ stream 模块内置 四层代理无需额外编译

镜像选择:

  • nginx:stable-alpine:体积小,常用于生产;注意 alpine 使用 musl,部分第三方模块不兼容。
  • nginx:stable:Debian 基础,兼容性好,体积较大。
  • 避免使用 nginx:latest,版本会漂移。

附录 A:示例工程

目录结构:

nginx/
├─ docker-compose.yml        # nginx + 两个 Node 后端(api1 / api2)
├─ conf/
│  ├─ nginx.conf             # 本文示例的合集
│  └─ certs/dev.crt|dev.key  # 自签证书,仅本地使用
├─ html/                     # 前端示例页面
├─ backend/server.js         # 零依赖 Node 后端,回显请求头与实例名
└─ logs/                     # 挂载的访问日志

启动:

cd nginx
docker compose up -d
docker compose exec nginx nginx -t
# 浏览器访问 https://localhost:8443(自签证书,需手动信任)
# Windows 也可执行:start https://localhost:8443

复现文中结论的命令(Git Bash / WSL / Linux 可直接使用):

# 1) HTTP 跳转
curl -sI http://localhost:8080/

# 2) 单页应用回退
curl -k -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/some/spa/route

# 3) gzip 与缓存头
curl -k -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://localhost:8443/assets/app.js

# 4) 负载均衡(多次请求观察 instance 字段)
for i in $(seq 1 6); do curl -k -s https://localhost:8443/api/info | grep instance; done

# 5) 限流
for i in $(seq 1 30); do curl -k -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/api/info; done | sort | uniq -c

# 6) 故障转移
docker stop nginx-demo-api1
curl -k -s https://localhost:8443/api/info | grep instance

# 7) 状态页
curl -k -s https://localhost:8443/nginx-status

# 8) 日志
tail -f logs/access.log

停止并清理:

docker compose down

附录 B:完整配置模板

以下配置对应 conf/nginx.conf 的核心部分,可直接作为新项目的起点。

worker_processes auto;                  # 跟随 CPU 核数
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 1024;            # 单 worker 最大连接数
}

http {
    include /etc/nginx/mime.types;      # 缺少它,JS/CSS 会被当作 text/plain
    default_type application/octet-stream;

    log_format main '$remote_addr [$time_local] "$request" $status '
                    '$body_bytes_sent rt=$request_time urt=$upstream_response_time';

    gzip on;
    gzip_types text/plain text/css application/javascript application/json;

    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;

    upstream app_cluster {
        least_conn;
        server api1:3000 max_fails=2 fail_timeout=5s;
        server api2:3000 max_fails=2 fail_timeout=5s;
        keepalive 16;
    }

    server {
        listen 80;
        return 301 https://$host$request_uri;   # 全站 HTTPS
    }

    server {
        listen 443 ssl;
        http2 on;
        server_name localhost;

        ssl_certificate     /etc/nginx/certs/dev.crt;
        ssl_certificate_key /etc/nginx/certs/dev.key;

        root  /usr/share/nginx/html;
        index index.html;

        # 静态资源:长缓存;缺文件直接 404,不回退到 index.html
        location ~* \.(?:js|css|png|jpe?g|svg|ico|woff2?)$ {
            add_header Cache-Control "public, max-age=31536000, immutable" always;
            try_files $uri =404;
        }

        # 入口 HTML:每次回源确认
        location = /index.html {
            add_header Cache-Control "no-cache" always;
        }

        # 单页应用路由
        location / {
            try_files $uri $uri/ /index.html;
        }

        # 接口反向代理
        location ^~ /api/ {
            limit_req zone=api_limit burst=10 nodelay;

            proxy_pass http://app_cluster/;     # 结尾 / 会截掉 /api/
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            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_connect_timeout 3s;
            proxy_read_timeout    30s;
            proxy_next_upstream error timeout http_502 http_503;
        }
    }
}

这份配置覆盖静态站点、单页应用回退、接口代理、负载均衡、HTTPS、压缩、限流与缓存,可作为实际项目的初始模板,再按需增删。

← 返回帖子 更多DevOps内容 →