Nginx 学习笔记

从问题出发,讲清 Nginx"为什么需要、怎么工作、怎么配置、怎么调优、有什么坑"。不写面面俱到的手册,只讲原理和实战中真正重要的东西。

📌 学习路线建议

这篇笔记内容较多,不要试图一次全部消化。建议分四步走:

阶段 读哪些章节 目标
第一步:建立直觉 第 1~3 节 理解"为什么需要 Nginx"、事件驱动架构、配置文件层级
第二步:掌握核心 第 4~7 节 搞懂 location 匹配规则、反向代理、负载均衡、HTTPS
第三步:生产加固 第 8~11 节 缓存、压缩、限流安全、WebSocket 代理
第四步:实战调优 第 12~15 节 性能调优、常见架构模式、排错速查、最佳实践

每步读完后再进入下一步。第三步建议本地起一个 Nginx,把配置改一改、reload 一下——看十遍不如跑一遍。


第一部分 · 原理篇

这部分讲"为什么"和"怎么想"。读完这部分你能搞懂 Nginx 的全貌:为什么需要它、它内部怎么工作、配置文件怎么组织。


1 · 为什么需要 Nginx

1.1 问题:让应用直接面对用户有什么不好

假设你写了一个 Node.js / Java / Python Web 服务,跑在 3000 端口。直接让用户访问行不行?行,但有很多问题:

  • 端口问题——用户不会输入 :3000,他们只认 80/443
  • 安全——后端应用直接暴露公网,攻击面大
  • 静态文件——让应用处理图片、CSS 太浪费,应用擅长的是业务逻辑
  • HTTPS——每个应用都配证书太麻烦
  • 多站点——一台机器想跑多个网站,应用各自占端口冲突
  • 高可用——一台后端挂了,整个网站就挂了

1.2 解法:前面加一层 Nginx

在用户和后端应用之间放一个 Nginx:

  • Nginx 监听 80/443,用户只跟它打交道——统一入口
  • 静态文件 Nginx 直接返回,动态请求转发给后端——动静分离
  • HTTPS 证书在 Nginx 统一配置——SSL 终结
  • 一个 Nginx 用 server_name 区分多个站点——虚拟主机
  • 多台后端,Nginx 分发请求——负载均衡
TIP

📌 核心思想:把"通用的网络层杂活"(HTTPS、缓存、限流、负载均衡、静态文件)从应用里剥离,交给专门的 Nginx 做。应用只管业务逻辑。这是关注点分离。

1.3 Nginx 的六大核心价值

价值 没用 Nginx 用了 Nginx
统一入口 用户要记端口号,应用直接暴露 Nginx 监听 80/443,隐藏后端
动静分离 应用处理所有请求,包括静态文件 Nginx 直接返回静态文件,动态请求才转发
负载均衡 一台后端扛不住就没办法 Nginx 分发到多台后端,自动故障转移
SSL 终结 每个应用都配证书 Nginx 统一加解密,后端走明文 HTTP
虚拟主机 一台机器一个网站 一个 Nginx 承载多个域名
安全防护 应用直接面对攻击 Nginx 做限流、IP 黑白名单、隐藏后端信息

2 · Nginx 的架构:为什么它这么快

2.1 事件驱动模型

Nginx 之所以快,核心在于它不像传统服务器(如 Apache)那样"一个连接一个线程"。Nginx 用的是事件驱动 + 非阻塞 I/O:

  • 传统模型(Apache prefork):每个连接占一个进程/线程。10000 个连接 = 10000 个线程,内存爆炸,上下文切换开销巨大
  • Nginx 模型:一个 worker 进程用 epoll 同时管理数千个连接。连接没有数据来时,worker 去处理别的连接,不阻塞等待
TIP

📌 打个比方:

  • 传统模型像银行一个窗口一个柜员,客户排队等。客户填表时柜员干等着——浪费
  • Nginx 模型像一个柜员同时服务多个客户。A 客户填表时,柜员转头处理 B 客户的请求。A 填完了再回来——这个柜员(worker)永远不闲着 :::

2.2 Master-Worker 进程模型

图 1 \xb7 Nginx Master-Worker 架构

  • Master:负责读取配置、启动/管理 worker 进程、reload 配置时不中断服务。Master 不直接处理请求
  • Worker:实际处理请求的进程。通常设为 auto(等于 CPU 核心数),每个 worker 独立处理数千并发连接
  • Cache Manager/Loader:管理缓存索引的辅助进程

:::tip 📌 为什么 reload 不中断服务?Master 收到 reload 信号后:① 启动新 worker(用新配置)→ ② 给老 worker 发"优雅关闭"信号 → ③ 老 worker 处理完手上的请求后退出。整个过程中新连接由新 worker 处理,老连接由老 worker 处理完——用户完全无感。

2.3 连接处理流程

① Worker 通过 epoll 等到新连接事件 ② accept() 接受连接,放入事件循环 ③ 读取请求头(非阻塞,读到才继续) ④ 根据 server_name 选 server 块 ⑤ 根据 URI 选 location 块 ⑥ 执行 location 里的指令: - 静态文件 → 直接读文件返回 - 反向代理 → 转发给后端,等响应 - 重定向 → 返回 301/302 ⑦ 写响应给客户端(非阻塞写) ⑧ 连接关闭或保持 keep-alive

2.4 Nginx vs Apache:什么时候选谁

特性 Nginx Apache
并发模型 事件驱动,一个 worker 管数千连接 每连接一线程/进程(prefork)或事件(worker/event MPM)
内存占用 极低(几 MB / worker) 高(每连接独立内存)
静态文件 极快 快
反向代理 核心能力,功能丰富 需要 mod_proxy,不如 Nginx 灵活
动态内容 必须反向代理给后端 可直接嵌入 PHP/Python(mod_php)
.htaccess 不支持(配置只在主文件改) 支持,用户可自行配置
配置复杂度 简洁,声明式 较复杂,模块多
TIP

📌 一句话总结:Nginx 是"反向代理 + 静态文件服务器"的王者,Apache 是"全能 Web 服务器"的老牌。现代架构几乎都是 Nginx 做前端代理 + 后端应用跑业务。两者不是对立的,可以共存。


3 · 配置文件结构

3.1 三层嵌套

Nginx 配置是层层嵌套的块(block):

# 全局块(main)—— 影响 Nginx 整体
worker_processes auto;        # worker 数量
error_log /var/log/nginx/error.log;

events {                      # events 块——影响连接处理
    worker_connections 1024;  # 每个 worker 最大连接数
}

http {                        # http 块——所有 HTTP 配置的总容器
    # http 级别的全局设置
    sendfile on;
    gzip on;

    upstream backend {        # upstream 块——定义后端服务器组
        server 10.0.0.1:3000;
        server 10.0.0.2:3000;
    }

    server {                  # server 块——一个虚拟主机(一个网站)
        listen 80;
        server_name example.com;

        location / {          # location 块——URL 路径匹配规则
            root /var/www/html;
            index index.html;
        }

        location /api/ {
            proxy_pass http://backend;
        }
    }
}

层级关系:

main(全局) └── events └── http ├── upstream(后端服务器组) ├── server(虚拟主机) │ └── location(URL 路径规则) │ └── 各指令 └── server ...
TIP

📌 像三层抽屉:http 是整个柜子的通用规则,server 是一个抽屉(一个网站),location 是抽屉里的分隔格(不同路径分开处理)。请求来了,先找哪个 server(按域名+端口),再找哪个 location(按路径)。

📌 指令继承:子块自动继承父块的指令。在 http 里设了 gzip on,所有 server 和 location 都生效。在某个 location 里单独设 gzip off,只覆盖那一个。

3.2 配置文件位置

Ubuntu/Debian:

/etc/nginx/nginx.conf            # 主配置(全局 + http 块)
/etc/nginx/sites-available/      # 站点配置仓库(写了但未必启用)
/etc/nginx/sites-enabled/        # 软链接,指向已启用的站点
/etc/nginx/snippets/             # 可复用的配置片段
/etc/nginx/mime.types            # 文件扩展名 → MIME 类型映射
/var/www/html/                   # 默认网站根目录
/var/log/nginx/access.log        # 访问日志
/var/log/nginx/error.log         # 错误日志

CentOS/RHEL:

/etc/nginx/nginx.conf            # 主配置
/etc/nginx/conf.d/               # 站点配置(直接放这里就启用)
/usr/share/nginx/html/           # 默认网站根目录
TIP

📌 Ubuntu 的 available/enabled 设计:sites-available 是"所有写好的站点配置仓库",sites-enabled 放软链接,只链接当前要启用的。想临时停用一个站点,删软链接即可,配置文件还留着。

# 启用一个站点 = 建软链接
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
# 停用 = 删软链接(配置还在 available)
sudo rm /etc/nginx/sites-enabled/mysite

CentOS 没有这个设计,直接放 conf.d/ 就启用,删文件就停用。两种方式各有优劣,Ubuntu 的方式更适合多站点管理。

3.3 主配置文件 nginx.conf 详解

# /etc/nginx/nginx.conf 典型内容

user www-data;                    # worker 进程的运行用户
worker_processes auto;            # worker 数量,auto = CPU 核心数
pid /run/nginx.pid;               # PID 文件位置
include /etc/nginx/modules-enabled/*.conf;  # 加载动态模块

events {
    worker_connections 768;       # 每个 worker 最大连接数
    # multi_accept on;            # 一次接受多个连接(高并发建议开)
}

http {
    # 基础设置
    sendfile on;                  # 零拷贝发送文件(性能关键)
    tcp_nopush on;                # 数据包攒够再发(配合 sendfile)
    tcp_nodelay on;               # 小数据立即发(配合 keep-alive)
    keepalive_timeout 65;         # keep-alive 超时秒数
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;     # MIME 类型映射
    default_type application/octet-stream;

    # 日志格式
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Gzip 压缩
    gzip on;
    gzip_disable "msie6";

    # 加载站点配置
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
TIP

📌 sendfile on 为什么重要?传统读文件再发送要经过 4 次内核↔用户空间拷贝。sendfile 让内核直接把文件数据从磁盘缓存送到网卡,跳过用户空间——零拷贝,性能提升显著。这是 Nginx 静态文件飞快的核心原因之一。


第二部分 · 核心配置


4 · location 匹配规则(最容易搞混的部分)

4.1 location 的四种修饰符

location = /exact { }       # 精确匹配
location ^~ /prefix { }     # 前缀匹配(不再做正则检查)
location ~ /regex { }       # 正则匹配(区分大小写)
location ~* /regex { }      # 正则匹配(不区分大小写)
location /normal { }        # 普通前缀匹配(优先级最低)
修饰符 含义 优先级
= 精确匹配,URI 完全相等 最高
^~ 前缀匹配,匹配后不再检查正则 次高
~ / ~* 正则匹配,按配置文件顺序,先匹配到的生效 中
无 普通前缀匹配,最长匹配生效 最低

4.2 匹配流程

请求 URI: /api/users/123 ① 先查所有 = 精确匹配 → 有则直接用,结束 ② 再查所有 ^~ 前缀匹配 → 最长匹配的,直接用,不再查正则 ③ 再查所有 ~ / ~* 正则匹配 → 按配置顺序,第一个匹配的用 ④ 最后查普通前缀匹配 → 最长匹配的用 ⑤ 全都没匹配 → 返回 404 或用 default location
TIP

📌 最容易踩的坑:你以为 location /api/ 会匹配 /api/users,但如果有个 location ~ ^/api/ 写在前面,正则会优先匹配。记住优先级:精确 > ^~前缀 > 正则 > 普通前缀。

📌 实际建议:

  • 静态文件用 ^~(避免被正则抢走)
  • API 转发用普通前缀或 ^~
  • 需要灵活匹配时才用正则(如 ~* \.(jpg|png|css|js)$) :::

4.3 实战示例

server {
    listen 80;
    server_name example.com;

    # ① 精确匹配首页(最高优先级,命中后直接返回)
    location = / {
        root /var/www/site;
        index index.html;
    }

    # ② 前缀匹配静态文件(不再检查正则)
    location ^~ /static/ {
        root /var/www/site;
        expires 30d;              # 静态资源缓存 30 天
    }

    # ③ 正则匹配图片文件
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        root /var/www/site;
        expires 7d;
    }

    # ④ API 转发
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
    }

    # ⑤ 默认匹配(兜底)
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

4.4 root vs alias(最容易搞混的指令)

# root:把 location 路径拼接在 root 后面
location /static/ {
    root /var/www/site;     # 请求 /static/a.css → /var/www/site/static/a.css
}

# alias:把 location 路径替换为 alias
location /static/ {
    alias /var/www/site/;   # 请求 /static/a.css → /var/www/site/a.css
}

:::tip 📌 区别:root 是拼接(location 路径保留),alias 是替换(location 路径丢弃)。

  • root 适合 location 路径和文件系统路径一致的情况
  • alias 适合 location 路径和文件系统路径不一致的情况
  • alias 目录路径末尾必须加 /,root 不需要

📌 踩坑警告:alias 配正则 location 时要小心路径替换,容易 404。不确定就用 root。


5 · 反向代理

5.1 基本反向代理

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;     # 转发给本机 3000 端口
    }
}

5.2 传递客户端真实信息

location / {
    proxy_pass http://127.0.0.1:3000;

    # 传递原始请求信息给后端
    proxy_set_header Host $host;                    # 原始域名
    proxy_set_header X-Real-IP $remote_addr;        # 真实客户端 IP
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # IP 链
    proxy_set_header X-Forwarded-Proto $scheme;     # 原始协议(http/https)
}
TIP

📌 为什么要 proxy_set_header?请求经 Nginx 转发后,后端看到的"客户端"是 Nginx(127.0.0.1),看不到真实用户。这几行把原始信息塞进请求头传给后端,否则:

  • 后端日志里全是 Nginx 的 IP,排查问题困难
  • 后端做 IP 限流、地域判断全失效
  • 后端不知道用户用的是 HTTP 还是 HTTPS,生成 URL 可能协议错误

📌 X-Forwarded-For 是什么?它记录请求经过的 IP 链:客户端IP, 代理1IP, 代理2IP。$proxy_add_x_forwarded_for 会自动在已有值后面追加 Nginx 的上游 IP。后端取第一个值就是真实客户端 IP。

5.3 proxy_pass 路径规则(最容易踩的坑)

# 情况 1:proxy_pass 不带 URI(不带路径)
location /api/ {
    proxy_pass http://backend;    # → 请求 /api/users 原样转发为 /api/users
}

# 情况 2:proxy_pass 带 URI(带路径或带 /)
location /api/ {
    proxy_pass http://backend/;   # → 请求 /api/users 转发为 /users(去掉 /api 前缀)
}

# 情况 3:带具体路径
location /api/ {
    proxy_pass http://backend/v2/;  # → 请求 /api/users 转发为 /v2/users
}

# 情况 4:正则 location,proxy_pass 不能带 URI
location ~ ^/api/ {
    proxy_pass http://backend;    # 正则 location 里 proxy_pass 不能带路径!
}
TIP

📌 一句话记忆:proxy_pass 带 / = 替换前缀(像 alias),不带 / = 保留完整路径。不确定就别带 URI,让路径原样传递最安全。

5.4 代理超时设置

location /api/ {
    proxy_pass http://backend;

    proxy_connect_timeout 5s;     # 连接后端超时(默认 60s)
    proxy_send_timeout 30s;       # 发送请求给后端超时
    proxy_read_timeout 60s;       # 等后端响应超时(后端慢的话要调大)
}
TIP

📌 什么时候要调 proxy_read_timeout?后端有耗时操作(如大文件生成、AI 推理、报表导出)时,默认 60s 可能不够,后端还没返回 Nginx 就超断了,用户看到 504 Gateway Timeout。调大到 300s 甚至更长。但别盲目调大——如果是后端卡死而非正常处理,调大只是延迟报错。

5.5 实战:完整的反向代理配置

server {
    listen 80;
    server_name app.example.com;

    # 静态文件直接返回
    location /static/ {
        root /var/www/app;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # API 转发给后端
    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
        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_connect_timeout 5s;
        proxy_read_timeout 120s;

        # 缓冲设置
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }

    # 其他所有请求转发给前端 SPA
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

6 · 负载均衡

6.1 基本配置

upstream backend {
    server 10.0.0.1:3000;       # 后端服务器 1
    server 10.0.0.2:3000;       # 后端服务器 2
    server 10.0.0.3:3000;       # 后端服务器 3
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;   # 转发给 upstream 组
    }
}

图 2 \xb7 Nginx 负载均衡架构

6.2 六种负载均衡策略

# ① 轮询(默认)—— 依次轮流分发
upstream backend {
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ② 加权轮询 —— 按权重分发(机器配置不同时用)
upstream backend {
    server 10.0.0.1:3000 weight=3;   # 3/5 的请求
    server 10.0.0.2:3000 weight=2;   # 2/5 的请求
}

# ③ least_conn —— 发给当前连接数最少的(更均衡)
upstream backend {
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ④ ip_hash —— 同一 IP 固定发到同一台(会话保持)
upstream backend {
    ip_hash;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ⑤ hash —— 按指定 key 一致性哈希(如按 URI 分发)
upstream backend {
    hash $request_uri;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ⑥ random —— 随机分发(Nginx 1.15.1+)
upstream backend {
    random;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}
策略 适用场景 优缺点
轮询 后端机器配置相同 简单,但不考虑实际负载
加权轮询 后端机器配置不同 按能力分配,但也不考虑实时负载
least_conn 请求处理时间差异大 更均衡,但要追踪连接数有开销
ip_hash 需要会话保持(无 Session 共享时) 简单,但 IP 变了或机器增减会失效
hash 需要按特定 key 固定分发 灵活,一致性哈希减少增减节点的影响
random 极简场景 最轻量,但可能不均衡
TIP

📌 ip_hash 的陷阱:

  • 用户走 NAT/代理出口,大量用户同一个 IP,全压到一台后端
  • IPv6 地址变化导致会话丢失
  • 更好的方案:用 Redis/数据库共享 Session,不用 ip_hash 粘连

📌 生产建议:后端机器配置相同用 least_conn,配置不同用加权轮询,有 Session 共享方案就别用 ip_hash。

6.3 健康检查与故障转移

upstream backend {
    server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:3000 backup;           # 备用,其他全挂了才用
}
  • max_fails=3:30 秒内失败 3 次,标记为不可用
  • fail_timeout=30s:失败后 30 秒内不再发给它
  • backup:备用服务器,只有其他全挂了才会启用
  • down:永久标记为不可用(维护时用)
TIP

📌 Nginx 开源版的健康检查是被动的——只有在实际转发请求失败时才标记不可用。没有主动探测。Nginx Plus(商业版)才有主动健康检查。

开源版主动健康检查方案:用 nginx_upstream_check_module 模块,或用外部脚本 + proxy_next_upstream 配合。

📌 proxy_next_upstream:请求发给一台后端失败后,是否自动重试下一台:

proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;   # 最多重试 2 台

6.4 负载均衡完整示例

upstream api_backend {
    least_conn;
    server 10.0.0.1:3000 weight=2 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:3000 weight=2 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:3000 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:3000 backup;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://api_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_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 2;

        proxy_connect_timeout 3s;
        proxy_read_timeout 30s;
    }
}

7 · HTTPS 与 SSL

7.1 基本 HTTPS 配置

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/cert.pem;    # 证书(含中间证书链)
    ssl_certificate_key /etc/nginx/ssl/key.pem;     # 私钥
    ssl_protocols       TLSv1.2 TLSv1.3;            # 只允许 TLS 1.2+
    ssl_ciphers         HIGH:!aNULL:!MD5;           # 加密套件
    ssl_prefer_server_ciphers on;                   # 服务端优先选择加密套件

    location / {
        root /var/www/site;
        index index.html;
    }
}

# HTTP 自动跳转 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
TIP

📌 什么是"SSL 终结"?HTTPS 加解密很耗 CPU。让 Nginx 统一处理加解密(在 Nginx 这里"终结"加密),它和后端之间走普通 HTTP(内网安全)。后端应用完全不用管证书和加密,省心又省 CPU。

📌 证书文件里有什么?cert.pem 应该包含你的证书 + 中间证书链(按顺序拼接)。如果只放了你的证书没放中间证书,部分浏览器会报"证书不受信任"。Let's Encrypt 的 fullchain.pem 已经帮你拼好了。

7.2 安全加固的 HTTPS 配置

server {
    listen 443 ssl http2;                    # 启用 HTTP/2(多路复用,更快)
    server_name example.com;

    # 证书
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 只允许 TLS 1.2 和 1.3
    ssl_protocols TLSv1.2 TLSv1.3;

    # 推荐的加密套件
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;           # TLS 1.3 时由客户端选

    # 会话缓存(减少 TLS 握手开销)
    ssl_session_cache shared:SSL:10m;        # 10MB 缓存约 4 万个会话
    ssl_session_timeout 1d;
    ssl_session_tickets off;                 # 关闭 session ticket(安全考虑)

    # OCSP 装订(验证证书未被吊销)
    ssl_stapling on;
    ssl_stapling_verify on;

    # HSTS:强制浏览器以后都用 HTTPS
    add_header Strict-Transport-Security "max-age=31536000" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

7.3 免费 HTTPS 证书(Let's Encrypt + Certbot)

# 安装 certbot 的 nginx 插件
sudo apt install -y certbot python3-certbot-nginx

# 自动申请证书并改好 nginx 配置
# 要求:域名已解析到本机、80 端口能被外网访问
sudo certbot --nginx -d example.com -d www.example.com

# 测试自动续期(证书有效期 90 天)
sudo certbot renew --dry-run
TIP

📌 certbot 怎么证明"域名是你的"?它用 ACME 协议做验证:certbot 在你服务器上放一个特定文件,Let's Encrypt 通过 http://你的域名/.well-known/... 来访问——能访问到,就证明你控制这个域名。所以申请前域名必须先解析到本机、80 端口要能被外网访问。

📌 为什么有效期只有 90 天?短有效期是安全设计:万一私钥泄露,危害窗口短。certbot 装好后会自动加一个 systemd timer 定期续期,全程无人值守。certbot --nginx 还会自动帮你改好 nginx 配置(加 ssl_certificate、配 80→443 跳转)。

7.4 多域名 HTTPS(SNI)

一个 Nginx 承载多个域名,每个域名有自己的证书——靠 SNI(Server Name Indication)实现:

server {
    listen 443 ssl;
    server_name site-a.com;
    ssl_certificate     /etc/nginx/ssl/site-a.com.pem;
    ssl_certificate_key /etc/nginx/ssl/site-a.com.key;
    location / { root /var/www/site-a; }
}

server {
    listen 443 ssl;
    server_name site-b.com;
    ssl_certificate     /etc/nginx/ssl/site-b.com.pem;
    ssl_certificate_key /etc/nginx/ssl/site-b.com.key;
    location / { root /var/www/site-b; }
}
TIP

📌 SNI 原理:TLS 握手时客户端在 ClientHello 里带上域名,Nginx 据此选择对应的证书。现代浏览器都支持 SNI。一个 IP 就能承载多个 HTTPS 站点。


第三部分 · 生产加固


8 · 缓存

8.1 浏览器缓存(静态文件)

location /static/ {
    root /var/www/site;

    # 不同文件类型不同缓存时间
    expires 30d;                          # 默认 30 天
    add_header Cache-Control "public, immutable";  # immutable:告诉浏览器不用验证
}

# 针对特定文件类型
location ~* \.(jpg|jpeg|png|gif|ico)$ {
    expires 30d;
    add_header Cache-Control "public";
}

location ~* \.(css|js)$ {
    expires 1y;                           # 带文件指纹的静态资源可以缓存一年
    add_header Cache-Control "public, immutable";
}

location ~* \.(html)$ {
    expires -1;                           # HTML 不缓存,每次都验证
    add_header Cache-Control "no-cache";
}
TIP

📌 immutable 是什么?告诉浏览器这个文件永远不会变,连验证请求(304)都不用发。配合文件名加 hash(如 app.a1b2c3.js)使用——文件内容变了就换 hash,URL 变了浏览器自然重新下载。这是前端构建工具(Webpack/Vite)的标准做法。

8.2 Nginx 代理缓存(缓存后端响应)

http {
    # 定义缓存区域
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
                     max_size=1g inactive=60m use_temp_path=off;

    server {
        location /api/ {
            proxy_pass http://backend;
            proxy_cache api_cache;                    # 使用上面定义的缓存
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_valid 200 10m;                # 200 响应缓存 10 分钟
            proxy_cache_valid 404 1m;                 # 404 缓存 1 分钟
            proxy_cache_use_stale error timeout updating;  # 后端挂了用旧缓存
            add_header X-Cache-Status $upstream_cache_status;  # 告诉客户端是否命中
        }
    }
}

$upstream_cache_status 的值:

值 含义
MISS 未命中,请求发给了后端
HIT 命中缓存,直接返回
EXPIRED 缓存已过期,发给了后端
STALE 后端不可用,返回了旧缓存
UPDATING 正在更新缓存,返回了旧缓存
TIP

📌 什么时候用代理缓存?

  • 后端接口返回的数据不频繁变化(如商品列表、文章列表)
  • 后端扛不住读压力
  • 不适合:用户个性化数据(如购物车、个人中心)——会缓存到别人的数据

📌 proxy_cache_use_stale 是生产利器:后端挂了时,Nginx 返回过期但还能用的旧缓存,用户感觉不到后端故障。这是缓存作为"降级手段"的典型用法。


9 · Gzip 压缩

9.1 基本配置

http {
    gzip on;
    gzip_min_length 1k;          # 小于 1KB 不压缩(压缩收益小,反而浪费 CPU)
    gzip_comp_level 6;           # 压缩级别 1-9,6 是性价比最高的
    gzip_types text/plain
               text/css
               text/xml
               text/javascript
               application/json
               application/javascript
               application/xml
               application/rss+xml
               application/atom+xml
               image/svg+xml;    # 对哪些 MIME 类型压缩
    gzip_vary on;                # 加 Vary: Accept-Encoding 头
    gzip_disable "MSIE [1-6]\."; # 老 IE 不支持 gzip
}
TIP

📌 gzip_comp_level 选几?

  • 1:压缩最快,压缩率最低
  • 6:推荐,压缩率和速度的平衡点
  • 9:压缩率最高,但 CPU 开销大,对高并发不划算

📌 哪些文件不要压缩?已经压缩过的格式(jpg、png、mp4、zip、gzip)——再压缩几乎不会变小,反而浪费 CPU。只压缩文本类文件。

9.2 Brotli 压缩(更先进的替代品)

# 需要 ngx_brotli 模块
brotli on;
brotli_comp_level 4;
brotli_types text/plain text/css application/json application/javascript;

Brotli 比 Gzip 压缩率高 15-25%,但只对 HTTPS 生效(浏览器只在 HTTPS 下接受 Brotli)。现代浏览器都支持。


10 · 限流与安全

10.1 限流:限制请求速率

http {
    # 定义限流区域:按客户端 IP 限流,10MB 内存约存 16 万个 IP
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            # burst=20:允许突发 20 个请求排队
            # nodelay:突发请求不延迟,立即处理(超出才拒绝)

            proxy_pass http://backend;
        }
    }
}
TIP

📌 rate=10r/s 和 burst=20 怎么理解?

  • rate=10r/s:平均每秒最多 10 个请求
  • burst=20:允许瞬间来 30 个请求(10 + 20 burst),超出的直接返回 503
  • nodelay:burst 里的请求立即处理不排队(不加 nodelay 的话,突发请求会被延迟处理)

📌 限流返回什么状态码?默认 503 Service Temporarily Unavailable。可以自定义:

limit_req_status 429;    # 改成 429 Too Many Requests 更语义化

10.2 限制并发连接数

http {
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        location /api/ {
            limit_conn conn_limit 10;    # 每个 IP 最多 10 个并发连接
            proxy_pass http://backend;
        }
    }
}

10.3 IP 黑白名单

# 白名单(只允许特定 IP 访问)
location /admin/ {
    allow 192.168.1.0/24;     # 允许内网
    allow 10.0.0.0/8;
    deny all;                 # 拒绝其他所有人
    proxy_pass http://backend;
}

# 黑名单(拒绝特定 IP)
location / {
    deny 123.45.67.89;        # 拒绝某个 IP
    deny 123.45.0.0/16;       # 拒绝某个网段
    allow all;
    proxy_pass http://backend;
}

10.4 隐藏 Nginx 版本号

http {
    server_tokens off;        # 不在响应头和错误页显示 Nginx 版本号
}

10.5 常见安全头

server {
    # 防止 MIME 嗅探
    add_header X-Content-Type-Options "nosniff" always;

    # 防止点击劫持
    add_header X-Frame-Options "SAMEORIGIN" always;

    # XSS 保护
    add_header X-XSS-Protection "1; mode=block" always;

    # HSTS(强制 HTTPS)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # CSP(内容安全策略)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
}

11 · WebSocket 代理

11.1 问题:WebSocket 不是普通 HTTP

WebSocket 连接建立时发一个 HTTP Upgrade 请求,之后变成持久的双向连接。Nginx 默认会在一段时间没有数据后断开连接。

11.2 配置

server {
    listen 80;
    server_name ws.example.com;

    location /ws/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;                           # WebSocket 需要 HTTP/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_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # WebSocket 连接是长连接,超时要调大
        proxy_read_timeout 86400s;    # 24 小时,防止空闲断开
    }
}
TIP

📌 Connection "upgrade" 为什么是固定字符串?因为 $http_connection 的值通常是 keep-alive,不会自动变成 upgrade。必须手动设为 "upgrade" 才能触发协议升级。

📌 proxy_read_timeout 86400s 为什么重要?WebSocket 连接建立后可能长时间没有数据传输(如聊天室没人说话)。默认 60s 超时会让 Nginx 断开连接,用户频繁掉线重连。调到 24 小时基本解决问题。如果真有长时间空闲的场景,应用层应该发心跳包保活。

11.3 同时代理 HTTP 和 WebSocket

server {
    listen 80;
    server_name app.example.com;

    # 用 map 自动判断是否是 WebSocket 升级请求
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;    # 自动适配
        proxy_set_header Host $host;
        proxy_read_timeout 86400s;
    }
}
TIP

📌 map 的作用:当 $http_upgrade 有值(是 WebSocket 请求)时,$connection_upgrade = upgrade;没有值(普通 HTTP)时 = close。这样一个 location 同时处理 HTTP 和 WebSocket。


第四部分 · 实战与调优


12 · 性能调优

12.1 worker 进程调优

# 全局块
worker_processes auto;          # auto = CPU 核心数,最常用
# 或手动指定
worker_processes 4;             # 4 核机器就设 4

worker_rlimit_nofile 65535;     # 每个 worker 最大文件描述符数

events {
    worker_connections 10240;   # 每个 worker 最大连接数(默认 512/1024)
    multi_accept on;            # 一次接受多个新连接(高并发建议开)
    use epoll;                  # Linux 用 epoll(Nginx 会自动选,一般不用手动设)
}
TIP

📌 worker_connections 设多少?每个连接占约 2KB 内存。10240 个连接 × 4 个 worker = 40960 并发,内存约 80MB——对现代服务器微不足道。但要注意系统级 ulimit -n 也要调大(否则 Nginx 受限于系统 fd 限制)。

📌 最大并发 = worker_processes × worker_connections。4 worker × 10240 = 40960 并发连接。但反向代理模式下每个客户端连接还要对应一个后端连接,所以实际并发约为一半。

12.2 内核参数配合

# /etc/sysctl.conf 或 /etc/sysctl.d/99-nginx.conf

# 文件描述符
fs.file-max = 1000000

# TCP 连接队列
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# TIME_WAIT 快速回收
net.ipv4.tcp_tw_reuse = 1

# TCP keepalive
net.ipv4.tcp_keepalive_time = 600

# 生效
sudo sysctl -p
# 提高 Nginx 用户的文件描述符限制
# /etc/security/limits.conf
nginx soft nofile 65535
nginx hard nofile 65535

# 或在 systemd service 里设
# /etc/systemd/system/nginx.service
[Service]
LimitNOFILE=65535

12.3 关键性能指令

http {
    sendfile on;              # 零拷贝发送文件(静态文件性能关键)
    tcp_nopush on;            # 数据包攒够再发(配合 sendfile 提升网络效率)
    tcp_nodelay on;           # 小数据立即发(配合 keep-alive 降低延迟)

    keepalive_timeout 65;     # keep-alive 超时
    keepalive_requests 1000;  # 一个 keep-alive 连接最多处理多少请求

    # 后端长连接(减少与后端建连的开销)
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    # 缓冲区
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 64k;

    # 上游长连接池(Nginx 1.7.11+,减少与后端 TCP 握手)
    upstream backend {
        server 127.0.0.1:3000;
        keepalive 32;         # 保持 32 个到后端的长连接
    }
}
TIP

📌 keepalive upstream 的坑:要同时设 proxy_http_version 1.1 和 proxy_set_header Connection "",否则 Nginx 到后端还是短连接。这三件套缺一不可。

📌 tcp_nopush 和 tcp_nodelay 看似矛盾?不矛盾:

  • tcp_nopush:大文件传输时数据包攒够再发,减少网络包数量
  • tcp_nodelay:小数据(如 HTTP 头)立即发送,降低延迟
  • Nginx 在不同阶段自动选择策略,两个都开就行 :::

13 · 常见架构模式

13.1 SPA(单页应用)部署

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

    # 静态资源(带 hash 文件名,长期缓存)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA 路由兜底:所有非文件请求都返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API 转发
    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

:::tip 📌 try_files $uri $uri/ /index.html 怎么工作?

  1. 先找 $uri 对应的文件(如 /assets/app.js)——找到直接返回
  2. 再找 $uri/ 对应的目录——找到返回目录下 index
  3. 都找不到就返回 /index.html——让前端路由接管

这是 SPA 的标准配置。用户访问 /users/123,文件系统里没有这个文件,但 try_files 兜底返回 index.html,前端 JS 路由解析 /users/123 渲染对应页面。

13.2 API 网关模式

upstream user_service {
    server 10.0.0.1:3001;
}
upstream order_service {
    server 10.0.0.2:3002;
}
upstream payment_service {
    server 10.0.0.3:3003;
}

server {
    listen 80;
    server_name api.example.com;

    # 统一限流
    limit_req zone=api_limit burst=20 nodelay;

    # 按路径分发到不同微服务
    location /api/users/ {
        proxy_pass http://user_service/;
    }

    location /api/orders/ {
        proxy_pass http://order_service/;
    }

    location /api/payments/ {
        proxy_pass http://payment_service/;
    }

    # 公共代理头
    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;
}

13.3 灰度发布(按比例分流)

upstream stable {
    server 10.0.0.1:3000;
}
upstream canary {
    server 10.0.0.2:3000;
}

# 用 split_clients 按比例分流
split_clients "${remote_addr}${http_user_agent}" $upstream_group {
    10%  canary;     # 10% 流量去灰度
    *    stable;     # 90% 流量去稳定版
}

server {
    listen 80;
    location / {
        proxy_pass http://$upstream_group;
        proxy_set_header Host $host;
    }
}

13.4 前后端分离 + 多站点

# 前端站点
server {
    listen 80;
    server_name www.example.com;
    root /var/www/frontend;
    location / {
        try_files $uri $uri/ /index.html;
    }
    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
    }
}

# 后台管理
server {
    listen 80;
    server_name admin.example.com;
    root /var/www/admin;
    auth_basic "Admin Area";              # HTTP 基础认证
    auth_basic_user_file /etc/nginx/.htpasswd;
    location / {
        try_files $uri $uri/ /index.html;
    }
    location /api/ {
        proxy_pass http://127.0.0.1:3000/admin/;
    }
}

# API 文档
server {
    listen 80;
    server_name docs.example.com;
    location / {
        proxy_pass http://127.0.0.1:8080;  # Swagger/API docs 服务
    }
}

14 · 排错速查

14.1 排查 Nginx 问题的顺序

① nginx -t 配置语法对不对 ② systemctl status nginx 服务起没起 ③ ss -tlnp | grep nginx 端口监听没 ④ tail error.log 具体报什么错 ⑤ curl -I localhost 本机能不能访问 ⑥ curl -I -H "Host:xxx" localhost 带域名能不能访问 ⑦ ufw status / iptables -L 防火墙放行没

14.2 常见错误与解决

症状 可能原因 解决
nginx: [emerg] bind() to 0.0.0.0:80 failed 端口被占用 ss -tlnp | grep :80 找到占用进程,停掉或改端口
502 Bad Gateway 后端没启动或 Nginx 连不上后端 检查后端进程、端口、防火墙
504 Gateway Timeout 后端响应太慢 调大 proxy_read_timeout,或优化后端
403 Forbidden 文件权限不对或 index 文件不存在 检查目录权限、index 指令
404 Not Found root/alias 路径不对 检查文件路径,区分 root 和 alias
413 Request Entity Too Large 上传文件超过限制 client_max_body_size 50m;
too many open files 文件描述符不够 调大 worker_rlimit_nofile 和系统 ulimit -n
配置 reload 后不生效 可能是语法错误没 reload 成功 先 nginx -t 检查,再看 error.log
WebSocket 频繁断开 proxy_read_timeout 太短 调大到 86400s,或应用层加心跳
后端日志里 IP 都是 127.0.0.1 没传 X-Real-IP / X-Forwarded-For 加上 proxy_set_header 传递真实 IP
静态文件 404 但文件存在 root vs alias 用错 仔细检查路径拼接逻辑

14.3 日志分析

# 自定义日志格式(包含更多有用信息)
log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                'rt=$request_time uct=$upstream_connect_time '
                'urt=$upstream_response_time';

access_log /var/log/nginx/access.log main;
# 常用日志分析命令

# 统计各状态码数量
awk '{print $9}' access.log | sort | uniq -c | sort -rn

# 找出最慢的请求(按响应时间排序)
awk '{print $NF, $7}' access.log | sort -rn | head -20

# 统计各 IP 访问次数(找异常流量)
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20

# 统计各 URL 访问次数
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20

# 实时监控 502 错误
tail -f access.log | grep ' 502 '

14.4 核心操作命令速查

sudo nginx -t                    # 检查配置语法(改完必做!)
sudo systemctl reload nginx      # 平滑重载配置(不断连接)
sudo systemctl restart nginx     # 重启(会断连接)
sudo nginx -s reload             # 等价于 reload
sudo nginx -s stop               # 停止
sudo nginx -T                    # 打印当前生效的完整配置(排查 include 问题)
tail -f /var/log/nginx/access.log  # 看访问日志
tail -f /var/log/nginx/error.log   # 看错误日志(排查必看)
TIP

📌 为什么改配置后先 nginx -t 再 reload?这是血泪经验:配置写错时直接 reload,Nginx 可能加载失败导致整个网站挂掉。nginx -t 先做语法检查,通过了再 reload 才安全。

📌 nginx -T 是排查神器:它会打印所有 include 进来的配置文件内容,拼成完整的生效配置。当你不确定某个配置到底在哪个文件里、是否生效时,nginx -T 一目了然。


15 · 最佳实践清单

15.1 配置组织

  • 主配置 nginx.conf 只放全局设置,站点配置放 sites-available/ 或 conf.d/
  • 用 sites-enabled/ 软链接控制启用/停用(Ubuntu 方式)
  • 公共配置抽成 snippet 用 include 引入(如 SSL 参数、代理头)
  • 每次改完先 nginx -t 再 reload

15.2 性能

  • worker_processes auto + worker_connections 调大
  • sendfile on + tcp_nopush on + tcp_nodelay on
  • 开启 Gzip,级别用 6
  • 静态文件设 expires 长期缓存
  • 后端长连接 keepalive + proxy_http_version 1.1 + Connection ""
  • 系统级 ulimit -n 和 sysctl 参数配合

15.3 安全

  • server_tokens off 隐藏版本号
  • 只允许 TLS 1.2+,禁用旧协议
  • 配置 HSTS、X-Frame-Options、X-Content-Type-Options 等安全头
  • API 接口加 limit_req 限流
  • 管理后台加 IP 白名单或基础认证
  • client_max_body_size 限制上传大小
  • 定期续期 HTTPS 证书(certbot 自动续期)

15.4 可靠性

  • proxy_next_upstream 配置故障自动重试
  • proxy_cache_use_stale 后端故障时返回旧缓存降级
  • backup 服务器做兜底
  • 监控 Nginx 健康状态(nginx -t、systemctl status、日志)
  • 日志轮转配置好(logrotate)

15.5 常用变量速查

变量 含义
$host 请求的 Host 头(域名)
$remote_addr 客户端 IP
$request_uri 完整请求 URI(含参数)
$uri 请求 URI(不含参数)
$args 查询参数
$scheme 协议(http 或 https)
$request_method 请求方法(GET/POST 等)
$request_time 总请求处理时间
$upstream_response_time 后端响应时间
$upstream_addr 后端服务器地址
$upstream_cache_status 缓存命中状态
$http_user_agent 客户端 User-Agent
$http_upgrade WebSocket 升级头
$proxy_add_x_forwarded_for 追加后的 X-Forwarded-For

16 · 总结

16.1 Nginx 的核心定位

Nginx 不是一个"Web 服务器"那么简单。它在现代架构中扮演的是网络入口层的角色:

  • 对用户:统一入口、HTTPS 终结、静态文件服务
  • 对后端:负载均衡、健康检查、故障转移
  • 对运维:限流防护、缓存降级、日志监控

16.2 学习建议

  1. 先跑通最小配置——装一个 Nginx,配一个静态站点 + 一个反向代理,curl 验证。感受一下请求是怎么流转的
  2. 再理解 location 匹配——这是 Nginx 配置的核心。写几个 location,用不同 URL 访问,看哪个生效
  3. 然后搞懂 HTTPS——用 certbot 申请免费证书,配好 HTTPS。理解 SSL 终结的含义
  4. 最后学负载均衡和缓存——这些是生产环境的进阶能力。理解了这些,你就能用 Nginx 撑起一个中小规模的生产系统

现实节奏:Nginx 的基础配置一两天就能上手,但 location 匹配规则、proxy_pass 路径替换、性能调优这些细节,需要在实战中反复踩坑才能真正掌握。遇到问题先 nginx -t、再看 error.log——这两步解决 90% 的问题。

本页目录