Nginx笔记

角色 主机名 IP 地址 主要作用
代理 / 负载均衡 nginx1 192.168.17.10 安装 Nginx、PHP、memcached,作为统一入口
后端服务器 1 nginx2 192.168.17.20 提供 Web、MariaDB、DNS 服务
后端服务器 2 nginx3 192.168.17.30 提供 Web、MariaDB、DNS 服务

一、先弄懂 Web 服务:一次访问到底发生了什么

1.1 从浏览器到 Nginx 的完整链路

用户访问 http://www.qw.com/index.html,大致会经历下面这些步骤:

  1. 浏览器解析 URL :拆出协议 http、域名 www.qw.com、端口 80、路径 /index.html。
  2. DNS 解析 :把域名换成服务器 IP(实验里通常改 hosts 文件)。
  3. 建立 TCP 连接:三次握手;如果是 HTTPS,还要多做一次 TLS 握手。
  4. 发送 HTTP 请求报文:请求行、请求头、请求体,经客户端网卡发出。
  5. 服务端网卡收包:数据通过 DMA 写入内存,触发中断,交给内核网络协议栈处理。
  6. 内核 → 用户空间 :内核把数据从内核空间的 Socket 接收缓冲区拷贝到 Nginx 进程的用户空间内存,Nginx 这时才"看见"请求。
  7. Nginx 解析请求:分析请求方法、URI、Host、请求头,决定是返回静态文件,还是转发给后端。
  8. 读取文件 :如果是静态资源,Nginx 根据 root / alias 把 URI 映射成磁盘路径,通过系统调用请内核读文件;内核先查页缓存(Page Cache),没有再读磁盘。
  9. 构建响应:生成状态行、响应头、响应体。
  10. 内核 → 网卡:响应数据写入 Socket 发送缓冲区,由内核协议栈封装,最后经网卡发给客户端。

请求先到网卡 → 内核空间 → 拷贝到用户空间 → Web 服务分析请求 → 读硬盘(又要经过内核)→ 构建响应 → 再交给内核 → 网卡 → 客户端。

用一张图表示:

text 复制代码
                         ┌──────────────────────── 服务器 ────────────────────────┐
                         │                                                        │
  浏览器                 │   ┌─────────┐   ①收包    ┌──────────────────────────┐  │
    │   HTTP 请求        │   │  网卡   │ ─────────▶ │ 内核空间                 │  │
    └───────────────────▶│   └─────────┘            │  TCP/IP 协议栈           │  │
                         │                          │  Socket 接收缓冲区       │  │
                         │                          └───────────┬──────────────┘  │
                         │                                      │ ② read() 拷贝   │
                         │                                      ▼                 │
                         │                          ┌──────────────────────────┐  │
                         │                          │ 用户空间:Nginx 进程     │  │
                         │                          │  解析请求 / 匹配 location│  │
                         │                          └───────────┬──────────────┘  │
                         │                                      │ ③ open/read     │
                         │                                      ▼                 │
                         │   ┌─────────┐  ④DMA   ┌──────────────────────────┐    │
                         │   │  磁盘   │ ◀──────▶│ Page Cache(内核页缓存) │    │
                         │   └─────────┘         └───────────┬──────────────┘    │
                         │                                      │ ⑤ 拷贝/零拷贝   │
                         │                                      ▼                 │
                         │                          ┌──────────────────────────┐  │
                         │                          │ Nginx 构建响应报文       │  │
                         │                          └───────────┬──────────────┘  │
                         │                                      │ ⑥ write()       │
                         │                                      ▼                 │
                         │   ┌─────────┐         ┌──────────────────────────┐     │
                         │   │  网卡   │ ◀────── │ 内核 Socket 发送缓冲区   │     │
                         │   └────┬────┘         └──────────────────────────┘     │
                         └────────┼───────────────────────────────────────────────┘
                                  │
                                  ▼
                              浏览器渲染
  • 用户空间 / 内核空间 :操作系统把内存分成两块。普通程序(Nginx、PHP)运行在用户空间,不能直接操作网卡、磁盘等硬件,只能通过系统调用 (read、write、sendfile 等)请内核代劳。
  • 数据拷贝是有成本的 :每进出一次内核,就多一次内存拷贝和上下文切换。后面讲的 零拷贝(Zero-Copy),本质上就是想办法减少这些拷贝。

这也是 Nginx 比传统"一个连接一个进程"的服务器更省资源的原因之一:它用事件驱动 + 非阻塞 I/O + 零拷贝,把等待和拷贝都降到了很低。

1.2 静态页面与动态页面

Web 页面按"内容是不是现算出来的"分为两类:

类型 典型文件 谁来处理 特点
静态页面 .html、.css、.js、图片、视频 Nginx / Apache 直接读文件返回 内容固定,速度快,不消耗解释器
动态页面 .php、.py、.jsp 等 中间件(php-fpm、uWSGI、Tomcat)执行程序后生成 内容随数据库、用户、时间变化

关键点:Web 服务器本身并不负责"执行程序"。Nginx 不会解析 PHP 代码,它只会把请求转交给会解析 PHP 的中间件,拿到结果后再返回给客户端。

中间件是什么

在 Web 架构里,"中间件"通常指位于 Web 服务器和后端数据库/应用之间的服务,例如:

  • PHP → php-fpm(FastCGI 进程管理器)
  • Python → uWSGI、Gunicorn
  • Perl → 相应的 FastCGI/PSGI 服务
  • Java → Tomcat、Jetty

一次动态请求的链路:

text 复制代码
客户端
  │  HTTP
  ▼
Nginx ──静态资源──▶ 直接读磁盘返回
  │
  │  FastCGI / HTTP
  ▼
php-fpm / uWSGI(中间件)
  │
  ▼
PHP / Python 程序 ──▶ MySQL / Redis 等
  │
  └── 生成 HTML ──▶ Nginx ──▶ 客户端
LNMP 与 LAMP
  • LAMP = Linux + Apache + MySQL/MariaDB + PHP/Perl/Python
  • LNMP = Linux + Nginx + MySQL/MariaDB + PHP/Perl/Python

两者都是经典的 Web 架构。区别主要在前端 Web 服务器:Apache 处理动态内容更"原生",Nginx 处理高并发静态请求和反向代理更擅长。现在常见的组合是 Nginx + php-fpm:Nginx 扛住连接和静态资源,PHP 业务交给 php-fpm。


二、Linux 权限原理

2.1 一行 ls -l 里藏着什么

bash 复制代码
[root@nginx1 ~]# ls -l /var/www/qw/html/index.html
-rw-r--r-- 1 root root 18 Sep 16 03:16 /var/www/qw/html/index.html

拆开看:

text 复制代码
-rw-r--r--   1   root   root   18   Sep 16 03:16   index.html
│└┬┘└┬┘└┬┘   │    │      │     │         │            │
│ │  │  │    │    │      │     │         │            └─ 文件名
│ │  │  │    │    │      │     │         └─ 修改时间
│ │  │  │    │    │      │     └─ 文件大小(字节)
│ │  │  │    │    │      └─ 所属组
│ │  │  │    │    └─ 所有者
│ │  │  │    └─ 硬链接数
│ │  │  └─ 其他人(other)权限:r--
│ │  └─ 所属组(group)权限:r--
│ └─ 所有者(owner)权限:rw-
└─ 文件类型:- 普通文件,d 目录,l 软链接

权限只有三种:

权限 字母 数字 对文件 对目录
读 r 4 读取文件内容 列出目录里的文件名(ls)
写 w 2 修改文件内容 在目录里创建、删除、重命名文件
执行 x 1 把文件当程序运行 进入目录 (cd)、访问目录下的文件

所以 rw-r--r-- 换成数字就是 644,rwxr-xr-x 就是 755。

2.2 Nginx 为什么会出现 403

Nginx 的 worker 进程通常以 nginx 用户运行(编译时 --user=nginx --group=nginx),它要读取 /var/www/qw/html/index.html,必须满足:

  1. 路径上每一级目录 都对 nginx 用户有 x 权限(能进入);
  2. 文件本身对 nginx 用户有 r 权限;
  3. 如果访问的是目录,还需要目录有 x 权限来找 index.html。

快速检查整条路径的权限,推荐用 namei:

bash 复制代码
[root@nginx1 ~]# namei -l /var/www/qw/html/index.html
f: /var/www/qw/html/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-xr-x root root qw
drwxr-xr-x root root html
-rw-r--r-- root root index.html

只要某一级目录是 drwx------,即使文件是 777,Nginx 也进不去,照样 403。

正确的授权方式:

bash 复制代码
# 把站点目录交给 nginx 用户(或让它属于 nginx 组)
[root@nginx1 ~]# chown -R nginx:nginx /var/www/qw/html

# 目录 755,文件 644:所有者可写,其他人只读
[root@nginx1 ~]# find /var/www/qw/html -type d -exec chmod 755 {} \;
[root@nginx1 ~]# find /var/www/qw/html -type f -exec chmod 644 {} \;

如果权限确认没问题仍然 403,检查 SELinux:

bash 复制代码
[root@nginx1 ~]# getenforce                 # 查看 SELinux 状态
[root@nginx1 ~]# ls -Z /var/www/qw/html/    # 查看安全上下文
# 实验环境可以临时关闭;生产环境应设置正确的上下文
[root@nginx1 ~]# semanage fcontext -a -t httpd_sys_content_t "/var/www/qw/html(/.*)?"
[root@nginx1 ~]# restorecon -Rv /var/www/qw/html

三、Web 服务器与 Apache 的三种并发模型

Apache 是资历最老的 Web 服务器之一。理解它的三种工作模型,能帮助你明白 Nginx 为什么要设计成后来的样子。

3.1 prefork 模型:一个进程扛一个连接

预派生(prefork):主进程启动时先"派生"出一批子进程等着接客,每个子进程同一时间只处理一个连接。

text 复制代码
             ┌──────────────┐
             │  主进程      │
             └──────┬───────┘
        ┌───────────┼───────────┐
        ▼           ▼           ▼
   ┌────────┐  ┌────────┐  ┌────────┐
   │ 子进程 │  │ 子进程 │  │ 子进程 │   ← 每个子进程同一时刻只服务一个请求
   └────────┘  └────────┘  └────────┘

特点:

  • 每个子进程是单线程的,一个进程对应一个用户请求;
  • 相对占用内存,但非常稳定,可以设置最大/最小进程数(StartServers、MinSpareServers、MaxRequestWorkers);
  • 最古老的模型,适合访问量不大的场景;
  • 优点:稳定,隔离性好,某个进程崩了不影响其他进程;
  • 缺点:每个请求一个进程,资源开销大,高并发下性能差。

3.2 worker 模型:多进程 + 多线程

worker 模型是"进程池 + 线程池"的混合体:

  • 有一个控制进程,启动多个子进程;
  • 每个子进程里包含固定数量的线程(ThreadsPerChild);
  • 请求由线程处理,线程不够时再启动新子进程,然后在新进程里继续开线程。
text 复制代码
             ┌──────────────┐
             │  控制进程    │
             └──────┬───────┘
        ┌───────────┴───────────┐
        ▼                       ▼
  ┌──────────────┐        ┌──────────────┐
  │   子进程 1    │        │   子进程 2    │
  │ ┌──┐┌──┐┌──┐ │        │ ┌──┐┌──┐┌──┐ │
  │ │线││线││线│ │        │ │线││线││线│ │
  │ │程││程││程│ │        │ │程││程││程│ │
  │ └──┘└──┘└──┘ │        │ └──┘└──┘└──┘ │
  └──────────────┘        └──────────────┘
  • 优点:相比 prefork 占用更少内存,可以同时处理更多请求;
  • 缺点:使用 Keep-Alive 长连接时,某个线程会被一直占用,即使没有数据传输,也要等到超时才会释放。

3.3 event 模型:事件驱动,一个进程响应多个请求

event 模型基于事件驱动:每个进程可以同时响应多个请求,专门解决了 Keep-Alive 场景下线程被长期占用的问题。

  • 优点:单线程可以响应多请求,占用内存更少,高并发场景表现更好;有专门的线程管理 Keep-Alive 连接,有真实请求时才把连接交给服务线程,处理完再释放;
  • 缺点:部分第三方模块不是线程安全的,使用时需要谨慎(这也是 event 模型早期应用不如 prefork 广泛的原因)。

3.4 三种模型对比

模型 并发单位 内存开销 高并发表现 典型适用场景
prefork 进程 高 差 老应用、需要进程隔离
worker 进程 + 线程 中 较好 一般动态站点
event 事件 + 异步 低 好 高并发、长连接

而 Nginx 从一开始就是事件驱动 + 异步非阻塞:由一个 master 管理多个 worker,每个 worker 用 epoll 同时监听成千上万个连接。这就是下一节要讲的 I/O 模型。


四、Nginx 高性能的秘密:I/O 模型与进程架构

4.1 磁盘 I/O

磁盘 I/O 的过程可以概括为:

进程向内核发起系统调用,请求磁盘上的某个资源 → 内核通过驱动程序把目标文件加载到内核的内存空间(Page Cache)→ 再把数据从内核内存复制给进程内存。

text 复制代码
用户进程
   │  read() 系统调用
   ▼
内核 VFS ──▶ Page Cache(页缓存)
                │ 未命中
                ▼
            块设备层 ──▶ 磁盘驱动 ──▶ 磁盘
                ▲
                │ DMA(不占 CPU)
                └── 数据写入内存
   │
   ▼ 内核把数据拷贝到用户缓冲区
用户进程拿到数据

要点:

  • 如果数据已经在 Page Cache 里(比如刚读过),就不用再碰磁盘,速度极快;
  • 大文件读取需要等待,因为磁盘带宽和寻道时间是硬限制;
  • 一次 read 至少涉及两次数据搬运:磁盘 → 内核、内核 → 用户。

4.2 网络 I/O

网络通信中,数据从网卡进入内核协议栈,最终到达用户进程的 Socket 缓冲区,这一过程涉及的 I/O 就是网络 I/O。

text 复制代码
网卡收包 ──DMA──▶ 内核 Ring Buffer ──▶ TCP/IP 协议栈
                                            │
                                            ▼
                                   Socket 接收缓冲区
                                            │ read()
                                            ▼
                                       用户进程

对 Nginx 来说,它要同时处理大量连接,如果每个连接都用"阻塞等待"的方式,一个慢客户端就能把 worker 卡死。所以必须理解同步/异步、阻塞/非阻塞。

4.3 同步/异步、阻塞/非阻塞

这两组概念经常被混为一谈,其实关注点不同:

  • 同步 / 异步 :关注结果如何通知。调用方是否需要主动去问"好了没有"?
  • 阻塞 / 非阻塞 :关注等待期间能不能干别的事。调用方会不会被挂起?

用去银行办业务打比方:

概念 银行类比 技术含义
同步 自己一次次去问"办好了吗" 被调用者不主动通知结果,调用者主动查询
异步 办好后短信通知你 被调用者通过状态、回调、通知机制告知结果
阻塞 在柜台前一直站着等 调用返回前,调用者被挂起,什么都干不了
非阻塞 取号后去逛街,过会儿回来问 调用立即返回一个状态,调用者可以去做别的事

组合起来就是四种:

组合 行为 例子
同步阻塞 一直等,等到结果 默认的阻塞 read()
同步非阻塞 立即返回"还没好",反复轮询 设置 O_NONBLOCK 后轮询
I/O 多路复用 一个线程同时等很多个 fd,谁就绪处理谁 select / poll / epoll
异步非阻塞 发起后立刻返回,完成后内核通知 POSIX AIO、io_uring

Nginx 在 Linux 上用的是 epoll(I/O 多路复用)+ 非阻塞 Socket,所以一个 worker 能同时盯住上万个连接:哪个连接有数据,就处理哪个,不会傻等。

4.4 五种 I/O 模型与 epoll

模型 等待数据 数据拷贝 特点
阻塞 I/O 全程阻塞 阻塞 最简单,一连接一线程
非阻塞 I/O 轮询 阻塞 空转浪费 CPU
I/O 多路复用 select/poll/epoll 阻塞 阻塞 一个线程管多个 fd,Nginx 的选择
信号驱动 I/O 信号通知 阻塞 用得少
异步 I/O 不阻塞 不阻塞 真正的"内核全包",Linux 上逐步成熟

epoll 为什么快?

  • select / poll 每次调用都要把所有 fd 从用户空间拷到内核,再线性扫描一遍,fd 越多越慢;
  • epoll 用红黑树 管理关注的 fd,用就绪链表存放已经就绪的事件,内核只需返回就绪的那几个,不需要遍历全部。
text 复制代码
epoll 实例
  ├── 红黑树:保存所有被监听的 fd(增删改 O(log n))
  └── 就绪链表:内核把有事件的 fd 挂进来
             │
             ▼
      epoll_wait() 只返回就绪的 fd

epoll 还有两种触发模式:

  • 水平触发(LT,默认):只要缓冲区还有数据,就持续通知;
  • 边缘触发(ET):只在状态变化时通知一次,必须一次把数据读完,通常配合非阻塞 Socket。

Nginx 在 Linux 上默认使用 epoll;在 FreeBSD 用 kqueue,在 Windows 用 IOCP,对上层配置基本透明。

4.5 零拷贝:mmap、sendfile 与 DMA

回到 4.1 的读文件流程。传统方式返回一个文件,数据要被搬四次:

text 复制代码
磁盘 ──DMA──▶ 内核 Page Cache ──CPU拷贝──▶ 用户缓冲区 ──CPU拷贝──▶ Socket 缓冲区 ──DMA──▶ 网卡
                 ①                           ②                        ③                    ④

同时伴随 4 次用户态/内核态切换(read 进入/返回、write 进入/返回)。而 Nginx 只是"把文件发出去",根本不需要修改内容,用户空间这一趟完全是白跑的。

mmap + write

用 mmap 把内核的 Page Cache 映射到用户空间,省掉"内核 → 用户"的一次拷贝:

text 复制代码
磁盘 ──DMA──▶ Page Cache ══(映射,不拷贝)══ 用户空间
                    │
                    └──CPU拷贝──▶ Socket 缓冲区 ──DMA──▶ 网卡

拷贝次数从 4 次降到 3 次。适合需要读写文件内容的场景,但仍有系统调用和缺页开销。

sendfile + DMA Gather

sendfile 让内核直接把 Page Cache 里的数据送到 Socket 缓冲区,数据完全不经过用户空间:

text 复制代码
磁盘 ──DMA──▶ Page Cache ──DMA(带 scatter-gather)──▶ 网卡
  • CPU 拷贝次数:0;
  • 上下文切换:从 4 次降到 2 次;
  • 这就是 Nginx 返回静态大文件特别快的重要原因。

Nginx 里对应的配置:

nginx 复制代码
sendfile on;          # 开启 sendfile 零拷贝
tcp_nopush on;        # 配合 sendfile,把响应头和文件合并后再发,减少小包
tcp_nodelay on;       # 长连接下禁用 Nagle 算法,降低小数据延迟
关于"内存直通"

数据留在内核的 Page Cache 中,由 DMA 直接搬运到网卡,不再往用户空间绕一圈。它描述的是 sendfile/DMA 这套机制的效果,并不是一个独立的系统调用。

注意 :sendfile 的前提是"原样发送"。如果要压缩(gzip)或修改内容,数据还是得进用户空间,这时零拷贝就失效了。所以 Nginx 的 gzip on 和 sendfile on 在大文件场景下会有取舍。

4.6 Nginx 架构:一个 master,多个 worker

进程结构
text 复制代码
                    ┌─────────────────────┐
                    │  master 进程(root) │
                    │  读配置、绑端口      │
                    │  管理 worker、处理信号│
                    └──────┬──────┬───────┘
              fork         │      │        fork
              ┌────────────┘      └────────────┐
              ▼                                ▼
     ┌─────────────────┐              ┌─────────────────┐
     │ worker 进程 1    │              │ worker 进程 N    │
     │ (nginx 用户)   │              │ (nginx 用户)   │
     │ epoll 事件循环   │              │ epoll 事件循环   │
     └─────────────────┘              └─────────────────┘
              │
              ▼
     ┌─────────────────┐    ┌─────────────────┐
     │ cache manager   │    │ cache loader    │
     │ 管理缓存过期     │    │ 启动时加载缓存   │
     └─────────────────┘    └─────────────────┘
  • master:以 root 启动,读取并校验配置、绑定 80/443 等特权端口、fork worker、接收信号(reload、升级、退出)。它不直接处理请求。
  • worker :以 nginx 普通用户运行,真正处理连接。worker 数量一般等于 CPU 核数(worker_processes auto;)。
  • cache manager / cache loader:只在启用代理缓存时出现,负责清理过期缓存和启动时加载缓存索引。
进程间通信
  • 信号 :master 通过 HUP、USR1、USR2、WINCH、QUIT 等信号控制 worker;
  • 共享内存 :zone 定义的共享区域,用于限流计数、缓存键索引、SSL 会话缓存等;
  • Socket 通道:master 与 worker 之间传递控制消息。
启动与连接建立
  1. master 解析配置,创建监听 Socket(如 0.0.0.0:80);
  2. master fork 出多个 worker,worker 继承监听 Socket 的文件描述符;
  3. 每个 worker 在自己的 epoll 里注册监听 Socket;
  4. 新连接到来时,内核唤醒其中一个 worker 去 accept;
  5. worker 读取请求、匹配 location、处理并返回。

多个 worker 同时等同一个端口,可能出现"惊群"。Nginx 通过 accept_mutex(较老方案)或内核的 EPOLLEXCLUSIVE / SO_REUSEPORT 来缓解。

一个 HTTP 请求在 Nginx 内部的处理阶段

Nginx 把 HTTP 处理拆成 11 个阶段,理解它能解释很多"为什么配置不生效":

阶段 作用 常见指令
POST_READ 读取请求头后 realip
SERVER_REWRITE server 级重写 rewrite、return
FIND_CONFIG 查找 location ---
REWRITE location 级重写 rewrite、if、set、break
POST_REWRITE 重写后检查 ---
PREACCESS 访问前限制 limit_req、limit_conn
ACCESS 访问控制 allow/deny、auth_basic
POST_ACCESS 访问后处理 satisfy
PRECONTENT 内容前处理 try_files
CONTENT 生成内容 index、autoindex、proxy_pass、fastcgi_pass
LOG 记录日志 access_log

比如:allow/deny 在 ACCESS 阶段,try_files 在 PRECONTENT 阶段,proxy_pass 在 CONTENT 阶段。顺序错了,效果就不同。

模块分类
  • 核心模块:进程管理、配置解析、事件驱动;
  • 标准 HTTP 模块 :ngx_http_core_module、ngx_http_proxy_module、ngx_http_rewrite_module、ngx_http_gzip_module 等;
  • Stream 模块:四层 TCP/UDP 代理;
  • 第三方模块 :如 memc-nginx-module、srcache-nginx-module,需要编译时 --add-module 加入。
为什么 Nginx 能扛高并发
  1. 事件驱动 + 非阻塞 I/O:一个 worker 用 epoll 管大量连接,不需要一连接一线程;
  2. 多进程:充分利用多核,worker 之间相互独立;
  3. 零拷贝:静态文件发送少两次内存拷贝;
  4. 内存池 :减少频繁 malloc/free 的开销;
  5. 低内存占用:空闲连接几乎不占额外线程栈。

当然,Nginx 再强也受操作系统限制:文件描述符上限、worker_connections、带宽、后端处理速度,都会成为瓶颈。这些会在"高并发优化"一节具体调。

五、源码编译安装 Nginx

5.1 为什么选择源码编译

用 dnf install nginx 也能装,但源码编译有三个好处:

  1. 可以自定义安装路径 (本文统一装到 /usr/local/nginx);
  2. 可以按需开启模块(HTTPS、HTTP/2、四层代理、状态页、预压缩等);
  3. 可以后期加入第三方模块(后面的 memcache 页面缓存就必须重新编译)。

代价是依赖要自己装、升级要自己操作。生产环境如果需求简单,用系统包更省心;需要定制模块时再源码编译。

5.2 实验准备

先把三台机器的 IP、主机名、防火墙和 SELinux 处理好:

bash 复制代码
# 查看本机 IP,确认与实验规划一致
[root@nginx1 ~]# ip a

# 实验环境临时关闭防火墙(生产环境请改为放行 80/443 等端口)
[root@nginx1 ~]# systemctl disable --now firewalld

# 临时关闭 SELinux,避免实验中被权限策略拦截
[root@nginx1 ~]# setenforce 0
[root@nginx1 ~]# getenforce
Permissive

5.3 下载并解压源码

bash 复制代码
# 进入存放源码的目录(/usr/local/src 是存放第三方源码的惯例位置)
[root@nginx1 ~]# cd /usr/local/src/

# 下载 Nginx 1.28.3 源码包
[root@nginx1 src]# wget https://nginx.org/download/nginx-1.28.3.tar.gz

# 解压:z=gzip 解压,x=释放,f=指定文件
[root@nginx1 src]# tar zxf nginx-1.28.3.tar.gz

# 进入解压后的源码目录
[root@nginx1 src]# cd nginx-1.28.3/

tar zxf 三个参数的含义:z 表示用 gzip 解压,x 表示 extract(解包),f 表示后面跟的是文件名。如果想看解压过程,可以加 v(verbose)。

5.4 安装编译依赖

bash 复制代码
# C 语言编译器,编译 Nginx 源码必需
[root@nginx1 nginx-1.28.3]# dnf install gcc -y

# PCRE 开发库:rewrite、location 正则匹配依赖它
[root@nginx1 nginx-1.28.3]# dnf install pcre-devel.x86_64 -y

# OpenSSL 开发库:编译 HTTPS(ssl)模块依赖它
[root@nginx1 nginx-1.28.3]# dnf install openssl-devel.x86_64 -y

# zlib 开发库:gzip 压缩模块依赖它
[root@nginx1 nginx-1.28.3]# dnf install zlib-devel.x86_64 -y
依赖包 作用 不装的后果
gcc C 编译器 ./configure 或 make 直接失败
pcre-devel 正则表达式库 无法使用 rewrite、location 正则
openssl-devel TLS/SSL 库 无法编译 --with-http_ssl_module
zlib-devel 压缩库 无法编译 gzip 相关模块

5.5 配置编译参数

bash 复制代码
# 检测环境并生成 Makefile
# 反斜杠 \ 表示命令还没结束,下一行继续
[root@nginx1 nginx-1.28.3]# ./configure --prefix=/usr/local/nginx \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--with-pcre \
--with-stream \
--with-stream_ssl_module \
--with-stream_realip_module

每个参数的作用:

参数 作用 后面哪里会用到
--prefix=/usr/local/nginx 安装目录,决定 sbin/conf/logs/html 的位置 全文所有路径
--user=nginx worker 进程运行用户 创建 nginx 用户、排查权限
--group=nginx worker 进程运行组 同上
--with-http_ssl_module 支持 HTTPS "全站加密"一节
--with-http_v2_module 支持 HTTP/2 可选的性能优化
--with-http_realip_module 从 X-Forwarded-For 中取真实客户端 IP 反向代理、IP 访问控制
--with-http_stub_status_module 提供状态页 "Nginx 状态页"一节
--with-http_gzip_static_module 支持直接返回预压缩的 .gz 文件 "gzip 压缩"一节
--with-pcre 启用 PCRE 正则支持 rewrite、location
--with-stream 支持四层 TCP/UDP 代理 "四层负载均衡"一节
--with-stream_ssl_module 四层代理支持 SSL 数据库、DNS 的 TLS 代理
--with-stream_realip_module 四层代理获取真实 IP 四层日志与访问控制

执行后会打印一大段检测信息,结尾出现 Configuration summary 就表示配置成功:

常见疑问:现在还没有 nginx 用户,为什么配置能成功?

因为 --user=nginx 只是把用户名写进编译配置,真正的用户检查发生在启动 时。等会儿第一次执行 nginx 才会报 getpwnam("nginx") failed,这是第一次启动最常见的错误

5.6 编译与安装

bash 复制代码
# 编译:把源码编译成二进制,产物在 objs/nginx
[root@nginx1 nginx-1.28.3]# make

# 安装:把二进制、配置、默认页面复制到 --prefix 指定的目录
[root@nginx1 nginx-1.28.3]# make install

安装完成后,目录结构大致如下:

text 复制代码
/usr/local/nginx/
├── conf/                 # 配置文件目录
│   └── nginx.conf        # 主配置文件
├── html/                 # 默认站点目录
│   ├── index.html
│   └── 50x.html
├── logs/                 # 日志与 pid 文件(首次启动后生成)
├── sbin/
│   └── nginx             # 启动程序
└── objs/                 # 编译时的中间产物(在源码目录里)

5.7 把 Nginx 加入 PATH

安装目录不在系统默认 PATH 里,直接敲 nginx 会提示 command not found。把 sbin 目录写进环境变量:

bash 复制代码
# 编辑当前用户的环境变量文件
[root@nginx1 nginx]# vim ~/.bash_profile

# 在文件末尾追加一行,把 /usr/local/nginx/sbin 加入 PATH
export PATH=$PATH:/usr/local/nginx/sbin

# 让修改立即在当前 shell 生效
[root@nginx1 nginx]# source ~/.bash_profile

# 验证:能输出版本说明 PATH 生效
[root@nginx1 nginx]# nginx -v
nginx version: nginx/1.28.3

~/.bash_profile 只对当前用户、登录 shell 生效。要所有用户都能用,写到 /etc/profile.d/nginx.sh 里。

5.8 创建运行用户并首次启动

直接启动试试:

bash 复制代码
# 首次启动,预期会报错:找不到 nginx 用户
[root@nginx1 nginx]# nginx
nginx: [emerg] getpwnam("nginx") failed

getpwnam() 是"按用户名查用户"的系统调用。配置里写了 user nginx;,但系统里没这个用户,所以启动失败。

创建系统用户:

bash 复制代码
# -r 创建系统用户(UID 较小,不能用于登录)
# -s /sbin/nologin 指定不可登录的 shell,防止有人用它登录系统
# -M 不创建家目录
[root@nginx1 nginx]# useradd -r -s /sbin/nologin -M nginx

# 再次启动
[root@nginx1 nginx]# nginx

# 检查 80 端口是否被 Nginx 监听
[root@nginx1 nginx]# ss -lntp | grep nginx
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=...,fd=6))

为什么 worker 不能用 root? 如果 Nginx 以 root 处理请求,一旦出现漏洞,攻击者就拿到了整台服务器的最高权限。用 nologin 的普通用户运行 worker,是 Linux 服务的基本安全实践。

5.9 添加 systemd 管理

手动执行 nginx 不适合生产:机器重启后不会自动拉起,也不方便统一管理。写一个 systemd 服务单元:

bash 复制代码
# 创建 systemd 单元文件
[root@nginx1 nginx]# vim /lib/systemd/system/nginx.service
ini 复制代码
[Unit]
Description=Nginx web server
# 在网络、日志、远程文件系统就绪后再启动
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
# nginx 默认会转入后台运行,所以类型是 forking
Type=forking
# 主进程 PID 文件位置,systemd 靠它跟踪服务
PIDFile=/usr/local/nginx/logs/nginx.pid
# 启动前先做配置检测,检测失败就不启动,避免带病上线
ExecStartPre=/usr/local/nginx/sbin/nginx -t
# 启动命令
ExecStart=/usr/local/nginx/sbin/nginx
# 重载:平滑加载新配置,不中断已有连接
ExecReload=/usr/local/nginx/sbin/nginx -s reload
# 停止:发送 QUIT 信号,优雅退出(处理完当前请求再退出)
ExecStop=/bin/kill -QUIT $MAINPID
# 停止超时时间,超过则强制结束
TimeoutStopSec=120
# 给服务一个独立的 /tmp,提升隔离性
PrivateTmp=true

[Install]
WantedBy=multi-user.target

启用服务:

bash 复制代码
# 先停掉前面手动启动的 nginx,否则 80 端口被占用,systemd 启动会失败
[root@nginx1 nginx]# nginx -s stop

# 让 systemd 重新读取单元文件
[root@nginx1 nginx]# systemctl daemon-reload

# 设置开机自启,并立即启动
[root@nginx1 nginx]# systemctl enable --now nginx

# 查看服务状态,看到 active (running) 即成功
[root@nginx1 nginx]# systemctl status nginx

5.10 日常会用到的 Nginx 命令

bash 复制代码
nginx -v                 # 查看版本
nginx -V                 # 查看版本 + 编译参数(升级时照抄这些参数)
nginx -t                 # 检查配置文件语法
nginx -T                 # 检查并打印完整生效配置(排查 include 很有用)
nginx -s reload          # 平滑重载配置
nginx -s reopen          # 重新打开日志文件(配合 logrotate)
nginx -s quit            # 优雅停止
nginx -s stop            # 立即停止

修改配置后的标准动作是:先 nginx -t,通过后再 systemctl reload nginx。直接 restart 会断开现有连接,reload 不会。


六、Nginx 配置文件与 location 匹配

6.1 配置目录结构

text 复制代码
/usr/local/nginx/
├── conf/
│   ├── nginx.conf            # 主配置
│   ├── mime.types            # 文件扩展名与 Content-Type 的映射
│   ├── fastcgi.conf          # FastCGI 参数(含 SCRIPT_FILENAME)
│   ├── fastcgi_params        # FastCGI 参数(不含 SCRIPT_FILENAME)
│   └── conf.d/               # 我们后面放子配置的目录(需手动创建)
├── html/                     # 默认站点
├── logs/
│   ├── access.log
│   ├── error.log
│   └── nginx.pid
└── sbin/nginx

6.2 主配置文件骨架

nginx.conf 是典型的"块状结构",每个 {} 是一个作用域:

text 复制代码
main(全局)
├── events { ... }                  # 事件模型、连接数
└── http {                          # HTTP 相关
    ├── upstream { ... }            # 后端服务器组(负载均衡)
    ├── server {                    # 一个虚拟主机
    │   ├── location { ... }        # URI 匹配规则
    │   └── location { ... }
    │   }
    └── server { ... }              # 另一个虚拟主机
}
└── stream { ... }                  # 四层 TCP/UDP 代理(与 http 同级)

指令的作用域 很重要:proxy_cache_path 只能写在 http 里,listen 只能写在 server 里,location 只能写在 server 或 location 里。写错层级,nginx -t 会直接报错。

6.3 全局配置块

nginx 复制代码
# 运行 worker 的用户(master 仍是 root)
user  nginx;

# worker 进程数:auto 表示等于 CPU 核心数,充分利用多核
worker_processes  auto;

# 错误日志路径与级别(debug|info|notice|warn|error|crit|alert|emerg)
error_log  logs/error.log  notice;

# master 进程 PID 文件
pid        logs/nginx.pid;

# 单个 worker 能打开的最大文件描述符数(高并发时调大)
worker_rlimit_nofile 100000;

6.4 events 块

nginx 复制代码
events {
    # 每个 worker 允许的最大并发连接数,默认 1024
    worker_connections  1024;

    # 一次尽量多接受连接,提升吞吐
    multi_accept on;

    # 使用哪种 I/O 多路复用;Linux 默认 epoll,写 auto 即可
    use epoll;
}

6.5 http 块

nginx 复制代码
http {
    # 引入 MIME 类型映射,否则浏览器可能把 CSS/JS 当成纯文本
    include       mime.types;

    # 未识别类型时的默认 Content-Type
    default_type  application/octet-stream;

    # 开启零拷贝,静态文件发送更快
    sendfile        on;

    # 长连接超时时间(后面会专门优化)
    keepalive_timeout  65;

    # 日志格式:main 是格式的名字,后面 access_log 会引用
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    # 隐藏响应头中的 Nginx 版本号(安全加固,后面升级章节还会从源码层面隐藏)
    server_tokens off;

    # 引入子配置(第九节创建站点时会用到)
    include /usr/local/nginx/conf.d/http/*.conf;

    # 默认 server(没有匹配到任何 server_name 时使用)
    server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

6.6 server 块

server 代表一个"虚拟主机",也就是一个网站:

nginx 复制代码
server {
    listen       80;                     # 监听端口;写成 IP:PORT 可只监听某块网卡
    server_name  www.qw.com;             # 域名,多个用空格分隔
    root         /var/www/qw/html;       # 站点根目录
    index        index.html index.php;   # 默认首页
    charset      utf-8;                  # 响应字符集,避免中文乱码
}

server_name 的匹配优先级:精确域名 > 以 * 开头的通配符 > 以 * 结尾的通配符 > 正则(~ 开头)> default_server。

6.7 root 与 alias 的区别(重点)

这两个指令都用来把 URI 映射到磁盘路径,但拼接方式不同:

root:把 location 的路径也拼进去

nginx 复制代码
location /img/ {
    root /var/www;
}
# 请求 /img/a.png → /var/www/img/a.png

alias:用 alias 路径替换 location 前缀

nginx 复制代码
location /img/ {
    alias /var/www/images/;
}
# 请求 /img/a.png → /var/www/images/a.png
指令 映射规则 请求 /img/a.png 对应的文件
root /var/www; root + 完整 URI /var/www/img/a.png
alias /var/www/images/; alias 替换 location 前缀 /var/www/images/a.png

易错点:

  • alias 后面的路径建议以 / 结尾,location 也以 / 结尾,两边保持一致,否则可能出现 // 或路径错位;
  • alias 用在正则 location 中时,必须用捕获组,例如 alias /data/$1;;
  • 浏览器访问目录时是否补斜杠,会影响相对路径,必要时用 return 301 统一。

6.8 location:URI 到底匹配哪条规则

location 用来把用户请求的 URI 映射到文件系统或后端。一个 server 里可以有多条 location,Nginx 按固定顺序挑出"最佳匹配"。

匹配符号
符号 名称 作用
= 精确匹配 URI 必须一字不差,大小写敏感,优先级最高,匹配成功立即终止
^~ 前缀匹配(禁止正则) URI 以指定字符串开头;匹配成功后不再检查正则
~ 正则匹配 区分大小写
~* 正则匹配 不区分大小写
无符号 普通前缀匹配 URI 以指定字符串开头;匹配成功后仍会检查正则

注意:\ 不是 location 匹配符号,它是正则表达式里的转义符(比如 \. 表示字面意义的点),不要混淆。

真实的匹配顺序
text 复制代码
请求 URI
   │
   ▼
┌──────────────┐  命中   ┌──────────────┐
│ = 精确匹配    │────────▶│ 立即使用      │
└──────┬───────┘         └──────────────┘
       │ 未命中
       ▼
┌──────────────┐  命中且带 ^~  ┌──────────────┐
│ 最长前缀匹配  │──────────────▶│ 立即使用      │
└──────┬───────┘               └──────────────┘
       │ 命中但无 ^~
       ▼
┌──────────────┐  第一个命中   ┌──────────────┐
│ 按顺序试正则  │──────────────▶│ 使用该正则    │
└──────┬───────┘               └──────────────┘
       │ 全不命中
       ▼
   使用最长前缀
例子
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;

    location = / {
        return 200 "exact: /\n";
    }

    location ^~ /images/ {
        return 200 "prefix: /images/ (^~ 阻止正则)\n";
    }

    location ~* \.(png|jpg|gif)$ {
        return 200 "regex: image\n";
    }

    location /static/ {
        return 200 "prefix: /static/\n";
    }

    location / {
        return 200 "default\n";
    }
}
请求 URI 命中的 location 原因
/ = / 精确匹配优先级最高
/images/a.png ^~ /images/ 最长前缀带 ^~,不再看正则
/static/a.png 正则 ~* 匹配图片后缀 前缀 /static/ 不带 ^~,正则先命中
/static/a.txt /static/ 正则不匹配,回退到最长前缀
/other / 只有默认前缀匹配
命名 location

以 @ 开头的 location 不能直接被客户端访问,只能被 try_files、error_page 内部跳转:

nginx 复制代码
location / {
    try_files $uri $uri/ @backend;
}

location @backend {
    proxy_pass http://127.0.0.1:8080;
}

七、Nginx 平滑升级

7.1 为什么要平滑升级

线上 Nginx 需要打安全补丁或加新模块时,直接 systemctl stop 再启动会中断所有用户请求。平滑升级(热升级) 可以在不停止业务的情况下换掉二进制:

  1. 老 master(旧版本)继续服务,已有 worker 处理完手头请求;
  2. 用新二进制启动一个新的 master 和一批新 worker;
  3. 新连接逐渐由新 worker 处理;
  4. 老 worker 处理完存量连接后退出,最后再让老 master 退出。

升级期间,新旧两个 master 会短暂共存。

7.2 隐藏 Nginx 版本号

响应头里的 Server: nginx/1.28.3 等于告诉攻击者"我用的哪个版本、可能有哪些已知漏洞"。虽然隐藏版本号不能替代打补丁,但能减少被自动化扫描器盯上的概率。

bash 复制代码
# 解压新版本源码
[root@nginx1 src]# tar zxf nginx-1.30.4.tar.gz

# 进入存放版本宏的目录
[root@nginx1 src]# cd nginx-1.30.4/src/core/

# 编辑版本头文件
[root@nginx1 core]# vim nginx.h

找到这两行:

c 复制代码
#define NGINX_VERSION      "1.30.4"
#define NGINX_VER          "nginx/" NGINX_VERSION

按下面的方式修改:

c 复制代码
#define NGINX_VERSION      "server"
#define NGINX_VER          "web" NGINX_VERSION

C 语言会把相邻的字符串字面量拼接起来,所以 "web" "server" 实际就是 "webserver",最终响应头变成 Server: webserver。

更推荐的做法 :只隐藏产品名、保留真实版本,方便自己排查:#define NGINX_VER "webserver/" NGINX_VERSION,这样响应头是 webserver/1.30.4。另外别忘了配置里加 server_tokens off;,两者配合效果更好。

注意 :NGINX_VERSION 也会被错误页、内置变量等使用,改成 "server" 会影响这些地方显示的内容。生产环境建议只改 NGINX_VER。

7.3 用相同参数编译新版本

升级时必须沿用原来的 --prefix 和模块参数,否则新二进制找不到配置目录,或者缺少正在使用的模块。

bash 复制代码
# 回到新版本源码根目录
[root@nginx1 nginx-1.30.4]# cd /usr/local/src/nginx-1.30.4/

# 使用与旧版本相同的参数(如果新增模块,在这里追加 --add-module)
[root@nginx1 nginx-1.30.4]# ./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module

# 只编译,不执行 make install(安装会覆盖配置,升级时手动替换二进制更可控)
[root@nginx1 nginx-1.30.4]# make

编译完成后,新二进制在 objs/nginx。

另开一个终端,持续观察进程变化:

bash 复制代码
# 每秒刷新一次 nginx 进程,观察新旧 master/worker 的交替
[root@nginx1 ~]# watch -n1 "ps -aux | grep nginx"

图中:

  • 旧 master:PID 1279 ,旧 worker:PID 1280;
  • 新 master:PID 13042 ,新 worker:PID 13043。

7.4 替换二进制并启动新 master

bash 复制代码
# 进入编译产物目录
[root@nginx1 nginx-1.30.4]# cd /usr/local/src/nginx-1.30.4/objs

# 备份旧二进制:-p 保留权限和时间戳
[root@nginx1 objs]# cp -p /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old

# 用新二进制覆盖旧二进制
# 前面的 \ 表示绕过 cp 的别名(RHEL 默认 cp -i 会交互询问),直接强制覆盖
[root@nginx1 objs]# \cp -f /usr/local/src/nginx-1.30.4/objs/nginx /usr/local/nginx/sbin/nginx

# 此时磁盘上的二进制已是新版,但正在运行的旧 master 仍在内存中执行旧代码
# 所以这里看到的版本号仍然是 1.28.3
[root@nginx1 objs]# curl -v 192.168.17.10
< HTTP/1.1 200 OK
< Server: nginx/1.28.3

# 用新二进制检查配置文件,确认新版本能正常解析现有配置
[root@nginx1 objs]# nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

# 向旧 master 发送 USR2:启动新 master,旧 master 把 pid 文件改名为 nginx.pid.oldbin
[root@nginx1 objs]# kill -USR2 1279

# 向旧 master 发送 WINCH:让旧 worker 优雅退出(不再接新连接,处理完存量再走)
[root@nginx1 objs]# kill -WINCH 1279

验证升级结果:

bash 复制代码
# 现在新 worker 处理请求,Server 头变成了 webserver
[root@nginx1 objs]# curl -v 192.168.17.10
< HTTP/1.1 200 OK
< Server: webserver
< Date: Sat, 05 Sep 2026 16:35:25 GMT

确认业务正常后,让旧 master 彻底退出:

bash 复制代码
# 旧 master 已完成使命,发送 QUIT 优雅退出
[root@nginx1 objs]# kill -QUIT 1279

注意:只做到 WINCH 是不够的,旧 master 会一直挂着。它虽然不处理请求,但占着内存和 pid 文件(nginx.pid.oldbin)。验证无误后应补一条 kill -QUIT <旧master PID>。

7.5 回滚

如果新版本启动失败、配置不兼容或业务异常,可以快速退回旧版本。前提是旧 master 还活着(没有执行 QUIT)。

bash 复制代码
# 进入二进制目录
[root@nginx1 objs]# cd /usr/local/nginx/sbin/

# 先把当前新二进制另存,方便以后再次升级/排查
[root@nginx1 sbin]# cp -p nginx nginx.new

# 恢复旧二进制
[root@nginx1 sbin]# \cp -f nginx.old nginx

# 向旧 master(1279)发送 HUP:让它重新拉起旧 worker,恢复服务
[root@nginx1 sbin]# kill -HUP 1279

# 向新 master(13042)发送 WINCH:让新 worker 优雅退出
[root@nginx1 sbin]# kill -WINCH 13042

# 确认已回滚到旧版本
[root@nginx1 sbin]# curl -v 192.168.17.10
< HTTP/1.1 200 OK
< Server: nginx/1.28.3
< Date: Sat, 05 Sep 2026 16:43:20 GMT

回滚成功后,可以再让新 master 退出:

bash 复制代码
# 新 master 已经没有 worker,直接优雅退出
[root@nginx1 sbin]# kill -QUIT 13042

# 清理临时 pid 文件
[root@nginx1 sbin]# rm -f /usr/local/nginx/logs/nginx.pid.oldbin

7.6 Nginx 信号速查

信号 作用 使用场景
HUP 重载配置,启动新 worker、优雅关闭旧 worker 改完配置后 nginx -s reload
USR1 重新打开日志文件 logrotate 切割日志
USR2 平滑升级二进制,启动新 master 热升级
WINCH 优雅关闭 worker(master 保留) 升级/回滚时让旧 worker 退出
QUIT 优雅关闭整个服务 systemctl stop
TERM / INT 立即关闭 紧急停止

记忆技巧 :USR2 是"生一个新的",WINCH 是"让旧的收工",QUIT 是"老的也退休"。升级顺序:USR2 → 观察 → WINCH → 验证 → QUIT。

八、高并发优化:文件描述符与进程数

8.1 Nginx 的并发受什么限制

Nginx 本身是事件驱动的,但"能扛多少连接"不是无限的,至少受三层限制:

  1. 操作系统 :单个进程能打开的文件描述符数量(nofile)、进程数(nproc)、全局文件句柄上限;
  2. Nginx 配置 :worker_processes、worker_connections、worker_rlimit_nofile;
  3. 硬件与后端:CPU、内存、带宽,以及后端 PHP/数据库的处理速度。

8.2 调整系统资源限制

先看当前限制:

bash 复制代码
# 查看当前 shell 能打开的最大文件数
[root@nginx1 ~]# ulimit -n
1024

# 查看 nginx 用户的实际限制(-u 表示以该用户身份执行命令)
[root@nginx1 ~]# sudo -u nginx ulimit -n
1024

修改 /etc/security/limits.conf:

bash 复制代码
# 编辑 PAM 登录资源限制文件
[root@nginx1 ~]# vim /etc/security/limits.conf
text 复制代码
# <domain>      <type>  <item>    <value>
# domain:用户或组(@nginx 表示 nginx 组)
# type:soft 软限制 / hard 硬限制 / - 表示同时设置软硬限制
# item:nofile 文件描述符数,nproc 进程数

*               -       nofile          100000
*               -       nproc           100000
root            -       nofile          100000
root            -       nproc           100000
@nginx          -       nofile          10000
@nginx          -       nproc           10000

注意上下顺序:@nginx 的规则要放在 * 后面,否则可能被通配规则覆盖。实验里把全局设大、nginx 组设小,是为了验证 sudo -u nginx ulimit -n 能读到 10000。

关键坑:systemd 启动的服务不读 limits.conf。

systemd 服务由 PID 1 启动,不走 PAM 登录流程,所以 limits.conf 对 systemctl start nginx 无效。必须给服务单独加限制:

bash 复制代码
# 为 nginx.service 创建覆盖配置目录
[root@nginx1 ~]# mkdir -p /etc/systemd/system/nginx.service.d

# 写入 LimitNOFILE
[root@nginx1 ~]# vim /etc/systemd/system/nginx.service.d/override.conf
ini 复制代码
[Service]
LimitNOFILE=100000
LimitNPROC=100000
bash 复制代码
# 重新加载 systemd 配置
[root@nginx1 ~]# systemctl daemon-reload

# 重启 Nginx 让限制生效
[root@nginx1 ~]# systemctl restart nginx.service

# 验证 nginx master 进程的 limits(Max open files 应为 100000)
[root@nginx1 ~]# cat /proc/$(cat /usr/local/nginx/logs/nginx.pid)/limits | grep "open files"

再调整系统级上限:

bash 复制代码
# 查看系统全局文件句柄上限
[root@nginx1 ~]# sysctl fs.file-max

# 永久调大(写入 sysctl 配置)
[root@nginx1 ~]# echo "fs.file-max = 200000" > /etc/sysctl.d/99-nginx.conf

# 生效
[root@nginx1 ~]# sysctl --system

8.3 Nginx 自身的并发配置

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf
nginx 复制代码
# worker 数量:auto = CPU 核数
worker_processes auto;

# 单个 worker 可打开的文件数,建议不小于 worker_connections × 2
worker_rlimit_nofile 100000;

events {
    # 每个 worker 的最大连接数
    worker_connections 10000;
    multi_accept on;
    use epoll;
}

8.4 用 ab 做压力测试

bash 复制代码
# ab 来自 httpd-tools 包,先安装
[root@nginx1 ~]# dnf install httpd-tools -y

# -n 总请求数 1000,-c 并发数 100
[root@nginx1 ~]# ab -n 1000 -c 100 http://www.qw.com/

重点看输出里的几项:

  • Requests per second:每秒处理请求数,越高越好;
  • Time per request:平均每个请求耗时;
  • Failed requests:失败数,正常应为 0;
  • 99% 分位耗时:大部分用户的体验上限。

九、搭建第一个站点

9.1 用 include 管理子配置

把所有站点都写进 nginx.conf 会越来越乱。更好的做法是按功能拆分:主配置只负责全局设置,站点配置放到 conf.d/http/ 下,用 include 引入。

bash 复制代码
# 编辑主配置
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf

在 http 块内部 加一行:

nginx 复制代码
http {
    # 引入 conf.d/http 下所有 .conf 文件(注意这行必须在 http {} 里面)
    include /usr/local/nginx/conf.d/http/*.conf;
}

创建目录:

bash 复制代码
# 创建子配置目录
[root@nginx1 ~]# mkdir -p /usr/local/nginx/conf.d/http

9.2 编写站点配置

bash 复制代码
# 新建站点配置文件
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;                          # 监听 80 端口
    server_name www.qw.com;             # 该站点对应的域名
    root /var/www/qw/html;              # 站点根目录
    index index.html;                   # 默认首页文件
}

创建站点目录和首页:

bash 复制代码
# 创建站点目录
[root@nginx1 ~]# mkdir -p /var/www/qw/html

# 写入一个测试首页
[root@nginx1 ~]# echo "192.168.17.10-qw" > /var/www/qw/html/index.html

# 检查配置并重载
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl reload nginx.service

9.3 测试访问

客户端需要能把 www.qw.com 解析到 192.168.17.10。实验里最方便的是改 hosts:

text 复制代码
# Windows: C:\Windows\System32\drivers\etc\hosts
# Linux:   /etc/hosts
192.168.17.10  www.qw.com

或者不改 hosts,直接用 curl 指定解析:

bash 复制代码
# --resolve 域名:端口:IP,临时把域名解析到指定 IP
[root@localhost ~]# curl --resolve www.qw.com:80:192.168.17.10 http://www.qw.com/
192.168.17.10-qw

# 也可以直接带 Host 头访问 IP
[root@localhost ~]# curl -H "Host: www.qw.com" http://192.168.17.10/
192.168.17.10-qw

如果访问到的是 Nginx 默认欢迎页,说明 server_name 没匹配上,请求落到了默认 server。检查 hosts、server_name 拼写,以及配置是否真的被 include。


十、Nginx 访问控制

10.1 基于用户的访问控制(Basic Auth)

Nginx 的 ngx_http_auth_basic_module 模块可以实现 HTTP 基本认证:浏览器弹出用户名/密码框,认证通过才允许访问。

原理 :浏览器把 用户名:密码 做 Base64 编码后放在 Authorization 请求头里。注意 Base64 只是编码,不是加密,明文等价,所以一定要配合 HTTPS。

生成密码文件
bash 复制代码
# htpasswd 命令来自 httpd-tools,没装会提示 command not found
[root@nginx1 ~]# dnf install httpd-tools -y

# -c 创建新文件(会覆盖同名文件)
# -m 使用 MD5 算法(RHEL 默认)
# -b 在命令行直接给密码(方便但会留在 history 里,生产环境建议交互式输入)
[root@nginx1 ~]# htpasswd -cmb /usr/local/nginx/conf.d/.htpasswd admin 123
Adding password for user admin

生成的文件内容类似:

text 复制代码
admin:$apr1$xxxxxxxx$yyyyyyyyyyyyyyyyyyyyyy
配置认证

先准备一个受保护目录:

bash 复制代码
# 创建管理目录和测试页面
[root@nginx1 ~]# mkdir -p /var/www/qw/html/admin
[root@nginx1 ~]# echo "admin" > /var/www/qw/html/admin/index.html
bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # ^~ 前缀匹配:/admin/ 及其子路径都走这里
    location ^~ /admin/ {
        # 认证提示语,会显示在浏览器的登录框里
        auth_basic "Please input username and password";
        # 指定密码文件
        auth_basic_user_file /usr/local/nginx/conf.d/.htpasswd;
    }
}
bash 复制代码
# 检查并重载
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

# -u 用户名:密码,认证通过后返回页面内容
[root@localhost ~]# curl -u admin:123 www.qw.com/admin/
admin

不带认证信息访问会返回 401 Unauthorized,并带 WWW-Authenticate 响应头,浏览器据此弹出登录框。

10.2 基于 IP 的访问控制

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
location ^~ /admin/ {
    # 只允许 192.168.17.20 访问
    allow 192.168.17.20;
    # 其余全部拒绝
    deny all;
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

# 在 192.168.17.20 上访问:允许
[root@localhost ~]# curl www.qw.com/admin/
admin

# 在其它机器上访问:返回 403 Forbidden
[root@localhost ~]# curl -I www.qw.com/admin/
HTTP/1.1 403 Forbidden

allow / deny 规则的特点:

  • 按配置顺序从上到下匹配,第一条命中的规则生效;
  • 支持网段:allow 192.168.17.0/24;;
  • 可以写 deny 192.168.17.100; allow all; 实现"只封某个 IP"。

反向代理场景的坑 :如果 Nginx 前面还有一层代理,$remote_addr 拿到的是代理的 IP,IP 限制会失效。需要在最外层配置 realip:

nginx 复制代码
# 信任来自 192.168.17.0/24 的代理,从 X-Forwarded-For 中取真实 IP
set_real_ip_from 192.168.17.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

十一、自定义错误页面

默认的 404/403 页面又丑又暴露服务器信息。可以用 error_page 把错误码指向自定义页面。

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # 当出现这些错误码时,内部跳转到 /40x.html
    error_page 404 401 402 403 405 /40x.html;

    # 精确匹配 /40x.html,并指定它所在的实际目录
    location = /40x.html {
        root /error;
    }
}
bash 复制代码
# 创建错误页面目录
[root@nginx1 ~]# mkdir /error

# 写入自定义错误页面
[root@nginx1 ~]# echo "你访问的地址出错" > /error/40x.html

# 重载配置
[root@nginx1 ~]# systemctl restart nginx.service

# 访问一个不存在的路径,触发 404
[root@localhost ~]# curl www.qw.com/1
你访问的地址出错

几个关键点:

  1. error_page 末尾的 /40x.html 是一个内部跳转,浏览器地址栏不会变;
  2. 状态码默认保持不变 (仍然是 404),只是响应体换了。想看状态码用 curl -I;
  3. location = /40x.html 里的 root /error; 会让 Nginx 去找 /error/40x.html;
  4. 如果错误页本身也找不到,会出现 500 Internal Server Error,配置时务必确认文件存在。

常见写法还有:

nginx 复制代码
# 5xx 错误统一指向 50x 页面
error_page 500 502 503 504 /50x.html;

# 把 404 伪装成 200(不推荐,会误导监控和搜索引擎)
error_page 404 =200 /404.html;

# 用命名 location 处理,适合需要反向代理的场景
error_page 502 503 504 @fallback;
location @fallback {
    proxy_pass http://127.0.0.1:8080;
}

十二、自定义访问日志与错误日志

12.1 默认日志位置

Nginx 默认把日志写在安装目录的 logs/ 下:

text 复制代码
/usr/local/nginx/logs/access.log    # 访问日志
/usr/local/nginx/logs/error.log     # 错误日志

可以把不同站点、不同业务的日志分开存放,方便排查和统计。

12.2 为站点单独配置日志

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # 该站点的错误日志
    error_log /var/log/nginx/qw/error.log;

    # 该站点的访问日志
    access_log /var/log/nginx/qw/access.log;
}
bash 复制代码
# 创建日志目录
[root@nginx1 ~]# mkdir -p /var/log/nginx/qw

# 重载配置
[root@nginx1 ~]# systemctl restart nginx.service

# 触发一次 404,再查看错误日志
[root@localhost ~]# curl www.qw.com/1
[root@nginx1 ~]# cat /var/log/nginx/qw/error.log
2026/09/19 21:51:27 [error] 33384#0: *1 open() "/var/www/qw/html/1" failed (2: No such file or directory), client: 192.168.17.20, server: www.qw.com, request: "GET /1 HTTP/1.1", host: "www.qw.com"

日志字段解读:

字段 含义
2026/09/19 21:51:27 错误发生时间
[error] 日志级别
33384#0 进程 PID#线程号
*1 连接编号
open() ... failed (2: ...) 具体错误:文件不存在
client: 192.168.17.20 客户端 IP
server: www.qw.com 命中的虚拟主机
request: "GET /1 HTTP/1.1" 原始请求行
host: "www.qw.com" 请求的 Host 头

12.3 日志级别

error_log 的级别从低到高:

text 复制代码
debug < info < notice < warn < error < crit < alert < emerg

级别越低记录越详细。debug 需要编译时加 --with-debug,生产环境一般用 warn 或 error,排查问题时临时调成 notice。

12.4 访问日志格式

nginx 复制代码
# 在 http 块里定义格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for" '
                '$request_time $upstream_response_time';

access_log /var/log/nginx/qw/access.log main buffer=32k flush=5s;
  • buffer=32k flush=5s:先写内存缓冲区,攒够 32K 或每 5 秒落盘一次,减少磁盘 I/O;
  • $request_time:Nginx 处理整个请求的耗时;
  • $upstream_response_time:后端响应耗时,排查"到底是 Nginx 慢还是后端慢"的关键。

12.5 日志切割

日志会无限增长,必须切割。用 logrotate 管理:

bash 复制代码
# 新建 logrotate 配置
[root@nginx1 ~]# vim /etc/logrotate.d/nginx
text 复制代码
/var/log/nginx/*/*.log {
    daily                # 每天切割
    rotate 14            # 保留 14 份
    missingok            # 文件不存在不报错
    notifempty           # 空文件不切割
    compress             # 压缩旧日志
    delaycompress        # 延迟一天压缩
    sharedscripts
    postrotate
        # 通知 Nginx 重新打开日志文件
        /bin/kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid)
    endscript
}

注意 :切割后如果只删文件不通知 Nginx,Nginx 还会往已经被删除的 inode 里写,日志就"消失"了。USR1 信号就是让它重新打开日志文件。


十三、基于文件检测的优化:try_files

try_files 会按顺序检查文件或目录是否存在,返回第一个找到的结果;如果都找不到,就内部重定向到最后一个参数。

nginx 复制代码
try_files $uri $uri/ /index.html;

各参数含义:

参数 含义
$uri 先找 URI 对应的文件,例如 /a.png → root/a.png
$uri/ 再找同名目录(结尾 / 表示目录)
/index.html 兜底:前面的都不存在时,内部跳转到首页

重要规则:

  • 只有最后一个参数会触发内部重定向;
  • 最后一个参数对应的文件必须存在,否则会返回 500;
  • 前面的参数只是"检测是否存在",不会重定向。

13.1 访问不存在的页面时回到首页

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # 依次尝试:原文件 → 加 .html 后缀 → 目录下的 index.html → 首页
    try_files $uri $uri.html $uri/index.html /index.html;
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

# /1 不存在,最终落到 /index.html
[root@localhost ~]# curl www.qw.com/1
192.168.17.10-qw

13.2 常见组合

nginx 复制代码
# 单页应用(Vue/React)history 路由:找不到文件就回 index.html
location / {
    try_files $uri $uri/ /index.html;
}

# PHP 站点:文件不存在交给 index.php 处理
location / {
    try_files $uri $uri/ /index.php?$args;
}

# 严格模式:找不到就返回 404,避免把错误页面伪装成 200
location / {
    try_files $uri $uri/ =404;
}

注意 :try_files 的兜底如果写成 /index.html,所有不存在的 URL 都会返回首页内容,HTTP 状态码是 200。对 SEO 和监控都不友好,按业务选择 =404 还是回首页。


十四、长连接优化

14.1 长连接解决了什么

HTTP/1.1 默认使用 Keep-Alive:一次 TCP 连接上可以连续发送多个请求,避免每个请求都重新三次握手、重新慢启动。对于图片、CSS、JS 很多的页面,收益非常明显。

但连接不能无限保持,否则会占满 worker 的连接数。所以需要设置超时。

14.2 keepalive_timeout

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf
nginx 复制代码
http {
    # 第一个值:连接空闲 65 秒后服务端真正关闭
    # 第二个值:响应头 Keep-Alive: timeout=60,告诉客户端 60 秒后可以主动关
    keepalive_timeout 65 60;
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

两个值的区别:

  • 65:Nginx 实际保持空闲连接的时间;
  • 60 :通过 Keep-Alive: timeout=60 响应头告诉浏览器/客户端的时间。

通常第二个值略小于第一个,让客户端先关,避免服务端主动断开造成的竞态。

14.3 keepalive_requests

限制一条长连接上最多处理多少个请求:

nginx 复制代码
http {
    # 同一条连接处理 3 个请求后,服务端主动关闭
    keepalive_requests 3;
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

# 连续发多个请求,观察连接是否被复用/关闭
[root@localhost ~]# curl -v -o /dev/null http://www.qw.com/ http://www.qw.com/ http://www.qw.com/ http://www.qw.com/

在 curl 的详细输出里,如果看到 Re-using existing connection,说明长连接生效;达到 3 次后会出现 Closing connection。

14.4 相关超时与安全

nginx 复制代码
# 读取请求头的超时,防止 Slowloris 慢速攻击一直占着连接
client_header_timeout 15s;

# 读取请求体的超时
client_body_timeout 15s;

# 向客户端发送响应的超时
send_timeout 15s;

# 请求体大小限制,防止超大上传打满磁盘/内存
client_max_body_size 10m;

# 请求头缓冲区大小,防止超大头攻击
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

十五、Nginx 下载站点配置

ngx_http_autoindex_module 模块会处理以 / 结尾的请求,自动生成目录列表,很适合做内部文件下载站。

15.1 相关指令

nginx 复制代码
autoindex on | off;                  # 自动文件索引,默认 off
autoindex_exact_size on | off;       # on 显示精确字节数;off 显示 K/M,默认 on
autoindex_localtime on | off;        # on 显示本机时间;off 显示 GMT,默认 off
autoindex_format html | xml | json | jsonp;   # 页面格式,默认 html
limit_rate rate;                     # 限制响应传输速率,单位 B/s,默认 0 表示不限速
set $limit_rate 4k;                  # 通过变量限速,优先级高于 limit_rate

15.2 开启目录浏览

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # 注意结尾的 /:只匹配 /download/ 及其子路径
    location /download/ {
        autoindex on;               # 开启目录浏览
        autoindex_exact_size off;   # 显示大概大小(KB/MB),更直观
        autoindex_localtime on;     # 显示本机时间
    }
}

小坑 :如果写成 location /download {(不带斜杠),那么 /download123 也会命中这条规则;加上斜杠能避免误匹配。

bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

15.3 创建测试文件

用 dd 生成两个指定大小的文件:

bash 复制代码
# 创建下载目录
[root@nginx1 ~]# mkdir -p /var/www/qw/html/download

# if=/dev/zero:从零设备读,产生全 0 数据
# of=...:输出文件
# bs=1M count=100:每块 1MB,写 100 块,共 100MB
[root@nginx1 ~]# dd if=/dev/zero of=/var/www/qw/html/download/1 bs=1M count=100
记录了100+0 的读入
记录了100+0 的写出
104857600字节(105 MB,100 MiB)已复制,0.0998261 s,1.1 GB/s

# 再生成一个 300MB 的文件
[root@nginx1 ~]# dd if=/dev/zero of=/var/www/qw/html/download/2 bs=1M count=300
记录了300+0 的读入
记录了300+0 的写出
314572800字节(315 MB,300 MiB)已复制,4.03999 s,77.9 MB/s

# 查看文件大小
[root@nginx1 ~]# ls -l /var/www/qw/html/download
总用量 438208
-rw-r--r-- 1 root root 104857600  9月 20 00:10 1
-rw-r--r-- 1 root root 314572800  9月 20 00:10 2

浏览器访问 http://www.qw.com/download/:

15.4 下载限速

nginx 复制代码
location /download/ {
    autoindex on;
    # 每个连接限速 100KB/s
    limit_rate 100k;
}

或者用变量动态限速:

nginx 复制代码
location /download/ {
    autoindex on;
    # 4k = 4KB/s,变量方式优先级更高
    set $limit_rate 4k;
}

limit_rate 只限制单个连接 的速率。如果客户端开多线程下载,总速率仍会叠加,需要配合 limit_conn 限制并发连接数:

nginx 复制代码
# 在 http 块定义共享内存区
limit_conn_zone $binary_remote_addr zone=addr:10m;

location /download/ {
    limit_conn addr 2;     # 每个 IP 最多 2 个并发下载
    limit_rate 100k;
}

十六、Nginx 状态页

状态页由 ngx_http_stub_status_module 提供,可以实时看到连接数、请求数等指标。

编译时必须加 --with-http_stub_status_module,否则配置 stub_status 时会报 unknown directive。本文编译参数里已经包含。

16.1 配置状态页

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    location /state {
        stub_status;                                          # 开启状态页
        auth_basic "Please input username and passwd";        # 再加一层密码
        auth_basic_user_file "/usr/local/nginx/conf.d/.htpasswd";
        allow 192.168.17.0/24;                                # 只允许内网访问
        deny all;
    }
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

# 带认证访问状态页
[root@localhost ~]# curl -u admin:123 http://www.qw.com/state

auth_basic 和 allow/deny 都在 ACCESS 阶段,默认 satisfy all,也就是两个条件都必须满足:既要在允许的网段,又要密码正确。这是很好的纵深防御示例。

16.2 状态页输出解读

text 复制代码
Active connections: 1
server accepts handled requests
 1 1 2
Reading: 0 Writing: 1 Waiting: 0
字段 含义
Active connections 当前活动客户端连接数,包括正在读写和空闲等待的连接
accepts 自启动以来接受的客户端连接总数
handled 自启动以来处理完成 的连接总数,通常等于 accepts;若因 worker_connections 等限制被拒绝,handled 会小于 accepts
requests 自启动以来客户端发来的总请求数(长连接上一个连接可以有多个请求)
Reading 正在读取客户端请求头的连接数,数值大说明排队严重、性能不足
Writing 正在向客户端发送响应的连接数,数值大说明访问量大
Waiting 空闲等待客户端发请求的连接数(Keep-Alive 空闲连接)

关系:

text 复制代码
Active connections = Reading + Writing + Waiting

监控思路:

  • accepts - handled 持续增大 → 连接被丢弃,需要调大 worker_connections / nofile;
  • Reading 长期偏高 → 可能存在慢请求或慢速攻击;
  • Writing 长期偏高 → 出口带宽或后端响应可能成为瓶颈;
  • Waiting 很大 → 大量 Keep-Alive 空闲连接,适当缩短 keepalive_timeout。

十七、Nginx 的压缩功能

17.1 原理

浏览器在请求头里发送:

text 复制代码
Accept-Encoding: gzip, deflate, br

表示"我支持这些压缩算法"。Nginx 如果开启 gzip,会把响应体压缩后再发,并加上:

text 复制代码
Content-Encoding: gzip

浏览器收到后自动解压。好处是节省出口带宽、加快传输;代价是消耗 CPU。

Nginx 的 gzip 功能由 ngx_http_gzip_module 提供,默认内置;预压缩功能 gzip_static 来自 ngx_http_gzip_static_module,需要编译时开启(本文已开启)。

17.2 常用指令

nginx 复制代码
# 启用或禁用 gzip,默认 off
gzip on | off;

# 压缩级别 1~9,默认 1;级别越高文件越小但越耗 CPU,一般 4~5
gzip_comp_level 4;

# 禁用早期 IE6 的 gzip(老版本 IE 对压缩支持有 bug)
gzip_disable "MSIE [1-6]\.";

# 小于该值的文件不压缩;太小的文件压缩收益低,还浪费 CPU
gzip_min_length 1k;

# 启用压缩的 HTTP 最低版本,默认 1.1
gzip_http_version 1.0 | 1.1;

# 压缩缓冲区个数和大小,一般 32 4k 或 16 8k
gzip_buffers 32 4k;

# 指定需要压缩的 MIME 类型;text/html 默认就会压缩,不用写
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;

# 在响应头加 Vary: Accept-Encoding,告诉缓存服务器"同一 URL 有不同编码版本"
gzip_vary on;

# 预压缩:直接返回磁盘上已存在的 .gz 文件,不消耗 CPU
gzip_static on | off;

17.3 配置并测试

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf

在 http 块中加入:

nginx 复制代码
    gzip on;
    gzip_comp_level 4;
    gzip_min_length 10k;              # 小于 10KB 不压缩,方便观察效果
    gzip_disable "MSIE [1-6]\.";
    gzip_buffers 32 4k;
    gzip_vary on;
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/svg+xml;

生成两个测试文件:一个 5KB(小于 10KB,不压缩),一个 50MB(会压缩):

bash 复制代码
# 5KB 文件
[root@nginx1 ~]# dd if=/dev/zero of=/var/www/qw/html/1.txt bs=5k count=1
记录了1+0 的读入
记录了1+0 的写出
5120字节(5.1 kB,5.0 KiB)已复制,0.00883339 s,580 kB/s

# 50MB 文件
[root@nginx1 ~]# dd if=/dev/zero of=/var/www/qw/html/2.txt bs=50M count=1
记录了1+0 的读入
记录了1+0 的写出
52428800字节(52 MB,50 MiB)已复制,1.86877 s,28.1 MB/s

# 重载配置
[root@nginx1 ~]# systemctl restart nginx.service

用 curl 检查响应头:

bash 复制代码
# 5KB 文件小于 gzip_min_length,不会压缩,没有 Content-Encoding
[root@localhost ~]# curl -I -H "Accept-Encoding: gzip" http://www.qw.com/1.txt

# 50MB 文件会被压缩,响应头出现 Content-Encoding: gzip 和 Vary
[root@localhost ~]# curl -I -H "Accept-Encoding: gzip" http://www.qw.com/2.txt
Content-Encoding: gzip
Vary: Accept-Encoding

17.4 预压缩 gzip_static

如果文件是静态的,可以在上传前就压缩好,Nginx 直接返回 .gz,完全省掉运行时压缩的 CPU:

bash 复制代码
# 生成预压缩文件(-k 保留原文件)
[root@nginx1 ~]# gzip -k /var/www/qw/html/2.txt

# 目录下会有 2.txt 和 2.txt.gz
[root@nginx1 ~]# ls -lh /var/www/qw/html/2.txt*
nginx 复制代码
location / {
    gzip_static on;    # 客户端支持 gzip 时,优先返回 2.txt.gz
}

预压缩适合不常变化的大文件(JS、CSS、日志归档);经常变化的内容还是用运行时 gzip。


十八、Nginx 中的变量

变量可以在配置里引用,用于判断、日志、重定向、限速等:

  • 变量分为内置变量 (Nginx 模块自带)和自定义变量 (set 定义);
  • 变量名区分大小写;
  • 变量在请求处理过程中动态求值。

18.1 常用内置变量

nginx 复制代码
$remote_addr;          # 客户端 IP(注意:如果前面有代理,这里是代理的 IP)
$remote_port;          # 客户端随机端口
$remote_user;          # 通过 Basic Auth 认证后的用户名

$args;                 # URL 中的查询参数
                       # 例:/search?keyword=手机&enc=utf-8 → keyword=手机&enc=utf-8
$is_args;              # 有参数时为 "?",否则为空
$arg_<name>;           # URL 中指定参数的值,例:$arg_id

$document_root;        # 当前请求对应的站点根目录(root 指令的值)
$document_uri;         # 不含参数的 URI,例:/var/?id=1111 → /var
$request_uri;          # 含参数的原始 URI,例:/var/?id=1111
$request_filename;     # 请求资源在磁盘上的绝对路径(root/alias + URI 生成)
$request_method;       # 请求方法:GET、POST、PUT、DELETE 等
$request_body_file;    # 反向代理时,请求体临时文件的路径

$host;                 # 请求的 Host 头(不含端口),可能被客户端伪造
$server_name;          # 当前匹配的虚拟主机名
$server_addr;          # 服务器 IP
$server_port;          # 服务器端口
$server_protocol;      # 协议版本:HTTP/1.0、HTTP/1.1、HTTP/2.0
$scheme;               # 请求协议:http、https

$http_user_agent;      # 客户端浏览器信息
$http_cookie;          # 客户端所有 Cookie
$cookie_<name>;        # 指定 Cookie 的值
$http_<name>;          # 任意请求头,name 小写,横线换成下划线
                       # 例:$http_x_forwarded_for
$sent_http_<name>;     # 响应头字段,name 小写,横线换下划线
                       # 例:$sent_http_content_type

$status;               # 响应状态码
$body_bytes_sent;      # 发送给客户端的响应体字节数(不含响应头)
$request_time;         # 从读到请求第一个字节到响应发送完毕的耗时(秒)
$upstream_response_time; # 后端响应耗时,排查后端慢的关键
$upstream_addr;        # 实际处理请求的后端地址

$connection;           # 连接编号
$connection_requests;  # 当前连接上已处理的请求数
$limit_rate;           # 当前连接的限速值,未设置为 0

$ssl_protocol;         # HTTPS 协议版本,例:TLSv1.3
$ssl_cipher;           # HTTPS 使用的加密套件

几个容易混淆的:

变量 请求 /a/b.html?id=1 时的值
$uri / $document_uri /a/b.html(不含参数)
$args id=1
$is_args ?
$request_uri /a/b.html?id=1(原始 URI,含参数)
$request_filename /var/www/qw/html/a/b.html(取决于 root)

18.2 自定义变量

nginx 复制代码
set $variable value;

value 可以是文本、内置变量,或两者拼接:

nginx 复制代码
location / {
    set $site "qw";
    set $full "$site-$server_name";
    add_header X-Site $full;
}

注意:set 只能写在 server、location、if 中;变量在请求开始时创建,每个请求独立。

18.3 用变量增强日志

nginx 复制代码
log_format detail '$remote_addr "$request" $status '
                  'rt=$request_time urt=$upstream_response_time '
                  'ua="$http_user_agent" xff="$http_x_forwarded_for"';

access_log /var/log/nginx/qw/detail.log detail;

这样能快速区分:

  • rt 很大、urt 很小 → Nginx 自身或客户端网络慢;
  • rt 和 urt 都大 → 后端 PHP/数据库慢;
  • urt 为 - → 请求没有走后端(静态文件或本地处理)。

十九、防盗链

19.1 什么是盗链

别人在自己的网站上直接引用你站点的图片、视频、下载链接,消耗你的带宽,却不给你带来访问量,这就是盗链。

浏览器请求资源时会带上 Referer 请求头,表示"我是从哪个页面跳过来的"。Nginx 通过 ngx_http_referer_module 检查 Referer,判断请求是否合法。

正常的 Referer 值有几类:

值 含义
none 请求头里没有 Referer,比如用户直接在地址栏输入 URL
blocked 有 Referer 头,但没有有效值(如被代理/隐私设置清空)
server_names Referer 中包含当前 server_name
自定义字符串 可带 * 通配符,如 *.qw.com
正则 以 ~ 开头,如 ~\.baidu\.com$

19.2 配置防盗链

先准备一张测试图片:

bash 复制代码
# 创建图片目录并放一个测试文件
[root@nginx1 ~]# mkdir -p /var/www/qw/html/images
[root@nginx1 ~]# echo "test image" > /var/www/qw/html/images/test.png
bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
location /images/ {
    # 允许:无 Referer、空 Referer、本站域名、*.qw.com、百度域名
    valid_referers none blocked server_name *.qw.com ~\.baidu\.com$;

    # $invalid_referer 为 "1" 表示 Referer 不合法
    if ($invalid_referer) {
        return 404;       # 返回 404 让盗链方以为图片不存在
    }
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

测试三种情况:

bash 复制代码
# 1. 不带 Referer(直接访问):允许,返回 200
[root@localhost ~]# curl -I http://www.qw.com/images/test.png

# 2. 带本站 Referer:允许
[root@localhost ~]# curl -I -e "http://www.qw.com/" http://www.qw.com/images/test.png

# 3. 带外部站点 Referer:被拦截,返回 404
[root@localhost ~]# curl -I -e "http://www.evil.com/" http://www.qw.com/images/test.png
HTTP/1.1 404 Not Found

19.3 防盗链的局限

Referer 是客户端可控的:

  • 攻击者可以用 curl -e 随意伪造;
  • 浏览器隐私模式、代理、HTTPS→HTTP 跳转可能不带 Referer,导致正常用户被误伤;
  • 移动端 App 内嵌浏览器可能完全不带 Referer。

所以 Referer 防盗链只能挡住"普通外链",不能当成安全边界。更可靠的方案:

  1. 签名 URL:给资源链接加上时间戳和签名(HMAC),过期失效;
  2. CDN 鉴权:使用云厂商的 URL 鉴权、Referer 黑白名单;
  3. Token + Cookie:登录后下发短期 Token;
  4. 水印:图片加水印,降低盗用价值。

二十、Nginx rewrite

20.1 rewrite 能做什么

ngx_http_rewrite_module 用于修改 URI,常见场景:

  • 网站改版后旧链接 301 到新链接,书签和搜索引擎不失效;
  • 强制 HTTP 跳 HTTPS;
  • 隐藏真实文件路径(/article/123 → /article.php?id=123);
  • 根据 UA、Referer、IP 做分流;
  • 统一域名(qw.com → www.qw.com)。

rewrite 依赖 PCRE 正则库,编译前必须安装 pcre-devel(本文已装)。

20.2 if 条件判断

语法:

nginx 复制代码
if (条件) {
    动作;
}

Nginx 的 if 只支持单次判断,不支持 else / elif。多条件可以嵌套或组合。

常用比较符:

运算符 含义
= 字符串相等
!= 字符串不等
~ 正则匹配,区分大小写
~* 正则匹配,不区分大小写
!~ 正则不匹配,区分大小写
!~* 正则不匹配,不区分大小写
-f / !-f 文件存在 / 不存在
-d / !-d 目录存在 / 不存在
-x / !-x 文件可执行 / 不可执行
-e / !-e 文件或目录存在 / 不存在(含软链接)

特殊规则:

  • 变量值为空字符串或 0 时,条件为假;
  • Nginx 1.0.1 之前,以 0 开头的字符串会被当成假,现在已修复。

示例:根据 User-Agent 返回不同内容:

nginx 复制代码
location / {
    if ($http_user_agent ~* firefox) {
        return 200 "test if messages";
    }
}

重要提醒 :社区里常说 "if is evil"。if 在 location 中会创建隐式的嵌套 location,部分指令(尤其 proxy_pass、try_files)在 if 块里行为不符合直觉。能用 map、try_files、return 解决的,尽量不要用 if。

20.3 set 自定义变量

nginx 复制代码
set $key value;

value 可以是文本、变量或组合:

nginx 复制代码
set $site "qw";
set $backend "http://192.168.17.20";
set $msg "$site-$server_name";

20.4 break

break 用于中断当前 location 中后续的 rewrite 模块指令 (rewrite、set、if、return 等),但其它模块的指令仍会继续执行。

nginx 复制代码
location / {
    set $test1 lee1;
    set $test2 lee2;
    if ($http_user_agent = firefox) {
        break;
    }
    set $test3 lee3;

    # 注意:echo 指令来自第三方 echo-nginx-module,默认编译不包含
    echo $test1 $test2 $test3;
}
  • 普通 UA:输出 lee1 lee2 lee3;
  • Firefox:break 跳过了后面的 set $test3,输出 lee1 lee2。

break 更常见的用途是:改写 URI 后不重新匹配 location ,直接在当前 location 继续处理(常配合 proxy_pass)。

20.5 rewrite 指令

语法:

nginx 复制代码
rewrite regex replacement [flag];
  • regex:PCRE 正则,匹配 URI(不含参数);
  • replacement:替换后的 URI 或完整 URL;
  • flag:控制重写后的行为。
正则速查
符号 含义
. 匹配除换行外的任意字符
\w 字母、数字、下划线、汉字
\s 空白符
\d 数字
\b 单词边界
^ 字符串开头
$ 字符串结尾
* 前一项重复 0 次或多次
+ 前一项重复 1 次或多次
? 前一项重复 0 次或 1 次
{n} 前一项重复 n 次
{n,} 前一项重复 n 次或更多
{n,m} 前一项重复 n 到 m 次
*? +? ?? 非贪婪版本,尽可能少匹配
\W \S \D \B 上述小写形式的反义
[^x] 除 x 外的任意字符
(abc) 分组并捕获,用 $1、$2 引用
\. 转义点号,匹配真正的 .
flag 参数
flag 行为 状态码 适用场景
不加 内部重写,继续在当前 location 处理 --- 简单替换
last 重写后重新匹配 location --- 一个 URL 需要多次重写
break 重写后停止当前 location 的 rewrite,继续用当前 location 处理 --- 改写后直接交给 proxy_pass
redirect 返回临时重定向,客户端重新发起请求 302 临时换地址
permanent 返回永久重定向 301 网站改版、域名迁移

last 和 break 的区别:

text 复制代码
last:改写 URI → 重新走一遍 location 匹配 → 可能命中另一个 location
break:改写 URI → 不再匹配 location → 继续执行当前 location 的剩余(非 rewrite)配置

用例子说明:

nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;

    # last:改写后会重新匹配,命中下面的 location /new/
    location /old/ {
        rewrite ^/old/(.*)$ /new/$1 last;
    }

    location /new/ {
        return 200 "matched /new/\n";
    }
}
bash 复制代码
[root@localhost ~]# curl http://www.qw.com/old/abc
matched /new/

如果把 last 换成 break,URI 虽然变成了 /new/abc,但不会重新匹配 location,仍在 /old/ 里继续处理,最终会去磁盘找 /var/www/qw/html/new/abc,找不到就 404。这就是两者最直观的区别。

外部重定向
nginx 复制代码
# 旧域名永久跳转到新域名(301)
rewrite ^/(.*)$ https://www.new.com/$1 permanent;

# 强制 HTTP 跳 HTTPS(推荐用 return 写更简洁)
return 301 https://$host$request_uri;

用 curl -I 可以看到:

text 复制代码
HTTP/1.1 301 Moved Permanently
Location: https://www.new.com/xxx
内部重写
nginx 复制代码
# 把 /article/123 内部改写成 /article.php?id=123,地址栏不变
rewrite ^/article/([0-9]+)$ /article.php?id=$1 last;
rewrite 的执行顺序
  1. server 块中的 rewrite 先执行;
  2. 然后匹配 location;
  3. location 中的 rewrite 按配置顺序执行;
  4. last 会重新匹配 location,可能形成循环。

循环保护 :Nginx 最多允许 10 次内部重定向,超过会返回 500 Internal Server Error,错误日志提示 rewrite or internal redirection cycle。

rewrite 与 return 的选择
  • 只是跳转、返回固定内容 → 用 return,性能更好、语义更清晰;
  • 需要正则捕获并改写路径 → 用 rewrite;
  • 需要判断文件是否存在 → 用 try_files,不要用一堆 if -f。

二十一、Nginx 反向代理

21.1 正向代理与反向代理

text 复制代码
正向代理(代理客户端):
  客户端 ──▶ 代理服务器 ──▶ 目标网站
  目标网站只看到代理,不知道真实客户端;常用于科学上网、企业出口。

反向代理(代理服务器):
  客户端 ──▶ Nginx(反向代理) ──▶ 内部 Web 服务器
  客户端只看到 Nginx,不知道后端是谁;常用于负载均衡、隐藏后端、统一入口。
对比项 正向代理 反向代理
代理谁 客户端 服务器
部署位置 客户端侧 服务器侧
客户端是否知情 需要配置代理 无感知
典型用途 访问控制、缓存、翻墙 负载均衡、安全隔离、HTTPS 卸载

Nginx 最常用的角色就是反向代理:把外网请求转发给内网服务器,再把结果返回给用户。

21.2 相关模块

nginx 复制代码
ngx_http_proxy_module;        # 把客户端请求以 HTTP 协议转发给后端
ngx_http_upstream_module;     # 定义后端服务器组(proxy_pass 引用)
ngx_stream_proxy_module;      # 以 TCP/UDP 协议转发(四层代理)
ngx_http_fastcgi_module;      # 把 PHP 请求以 FastCGI 协议转发给 php-fpm
ngx_http_uwsgi_module;        # 把 Python 请求以 uwsgi 协议转发

按后端类型还可以分为:

  • 同构代理:用户请求的资源不需要额外程序处理,直接通过 HTTP/TCP 转发给后端;
  • 异构代理 :请求需要经过 PHP、Python 等程序处理后再返回,比如 fastcgi_pass、uwsgi_pass。

21.3 proxy_pass 的路径拼接规则(重点)

proxy_pass 后面有没有 URI(哪怕只是一个 /),结果完全不同:

nginx 复制代码
# 情况 1:后面没有 URI,原样转发完整 URI
location /api/ {
    proxy_pass http://192.168.17.20;
}
# 请求 /api/user → 后端收到 /api/user

# 情况 2:后面有 URI(这里是一个 /),用 URI 替换 location 前缀
location /api/ {
    proxy_pass http://192.168.17.20/;
}
# 请求 /api/user → 后端收到 /user
配置 客户端请求 后端收到
proxy_pass http://backend; /api/user /api/user
proxy_pass http://backend/; /api/user /user
proxy_pass http://backend/v1/; /api/user /v1/user

这是面试和排错的高频考点。后端说"接口 404",先检查这里少没少斜杠。

21.4 常用指令

nginx 复制代码
proxy_pass http://192.168.17.20;      # 转发目标:IP:端口、域名或 upstream 组名

proxy_hide_header field;              # 隐藏后端返回的响应头
# 例:隐藏后端的 ETag
location / {
    proxy_pass http://192.168.17.20;
    proxy_hide_header ETag;
}

proxy_pass_header field;              # 允许后端某些默认被隐藏的头传给客户端
# Nginx 默认不转发后端的 Date、Server、X-Pad、X-Accel-* 等头
# 例:把后端的 Server 和 Date 透传给客户端
proxy_pass_header Server;
proxy_pass_header Date;

proxy_pass_request_body on | off;     # 是否转发请求体,默认 on
proxy_pass_request_headers on | off;  # 是否转发请求头,默认 on

proxy_set_header field value;         # 修改或添加转发给后端的请求头
# 例:把真实客户端 IP 放进 X-Forwarded-For
proxy_set_header X-Forwarded-For $remote_addr;
超时相关
nginx 复制代码
proxy_connect_timeout 6s;   # 与后端建立连接的超时,默认 60s;超时返回 504
proxy_read_timeout 60s;     # 向后端发起读请求后,等待响应的超时,默认 60s
proxy_send_timeout 60s;     # 向后端发送请求后,等待发送完成的超时,默认 60s

proxy_http_version 1.1;     # 代理使用的 HTTP 版本,默认 1.0
proxy_ignore_client_abort off;
# off:客户端断开时,Nginx 也断开对后端的请求,并记录 499
# on :忽略客户端断开,继续等待后端返回

默认 proxy_http_version 1.0 不支持长连接。想让 Nginx 与后端复用连接,要改成 1.1 并把 Connection 头清空,后面负载均衡一节会演示。

21.5 传递真实客户端 IP

后端默认看到的连接来源是 Nginx 的 IP。要拿到真实客户端 IP,需要加一个请求头:

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
location / {
    proxy_pass http://192.168.17.20;
    # 把客户端真实 IP 写入 X-Forwarded-For,后端据此记录日志
    proxy_set_header X-Forwarded-For $remote_addr;
    # 保留原始 Host,便于后端做虚拟主机
    proxy_set_header Host $host;
    # 可选:单独再放一个 X-Real-IP
    proxy_set_header X-Real-IP $remote_addr;
}

在后端 Apache 的日志格式里读取它:

bash 复制代码
[root@localhost ~]# vim /etc/httpd/conf/httpd.conf
apache 复制代码
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\"" combined

X-Forwarded-For 是可以被客户端伪造的。只有当 Nginx 是最外层可信代理 时,用 $remote_addr 覆盖它才安全;如果前面还有 CDN/负载均衡,应使用 realip 模块从可信来源中提取,而不是直接信任请求头。

21.6 反向代理 PHP 站点

先在后端 192.168.17.20 上准备 Apache + PHP:

bash 复制代码
# 后端安装 Apache 和 PHP 模块
[root@localhost ~]# dnf install httpd -y
[root@localhost ~]# dnf install php -y

# 写一个 phpinfo 测试页
[root@localhost ~]# echo "<?php phpinfo();" > /var/www/html/index.php

# 启动并设置开机自启
[root@localhost ~]# systemctl enable --now httpd

# 后端本机先验证
[root@localhost ~]# curl http://127.0.0.1/index.php | head

在 Nginx 上把 .php 请求转给后端:

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
location ~* \.(php)$ {
    # 把 PHP 请求代理到后端 Apache 的 80 端口
    proxy_pass http://192.168.17.20:80;
}
bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service

# 从代理层访问,看到 phpinfo 说明代理成功
[root@localhost ~]# curl -I http://www.qw.com/index.php

注意:这里代理的是 HTTP 协议 (后端 Apache 用 mod_php 解析 PHP),和后面的 fastcgi_pass 是两种不同方式。生产环境更常见的是 Nginx + php-fpm(FastCGI),效率更高。

21.7 反向代理常见错误

状态码 常见原因 排查方向
502 Bad Gateway 后端没启动、端口写错、后端拒绝连接、SELinux 禁止 Nginx 联网 ss -lntp 看后端端口;curl 直连后端;看 Nginx error.log
504 Gateway Timeout 后端处理太慢,超过 proxy_read_timeout 查后端日志、数据库慢查询;适当调大超时
403 Forbidden 后端文件权限、SELinux、后端自身访问控制 看后端 error.log;namei -l;getenforce
413 Request Entity Too Large 请求体超过 client_max_body_size 调大该值或让后端限制
499 Client Closed Request 客户端等不及主动断开 后端太慢;优化接口或加缓存

SELinux 导致 502 的典型修复:

bash 复制代码
# 允许 httpd 通过网络连接其他服务(反向代理场景)
[root@nginx1 ~]# setsebool -P httpd_can_network_connect 1

二十二、Nginx 代理缓存

22.1 为什么需要缓存

静态资源可以交给 Nginx,但动态页面每次都要经过 PHP + 数据库,压力很大。如果页面内容短时间内不变,可以让 Nginx 把后端响应缓存到本地磁盘,下次直接返回,减少后端压力、降低响应时间。

text 复制代码
第一次请求:客户端 → Nginx → 后端 → 返回 → Nginx 存一份缓存
第二次请求:客户端 → Nginx → 命中缓存 → 直接返回(不再找后端)

缓存功能默认关闭,需要显式配置。

22.2 核心指令

nginx 复制代码
proxy_cache zone_name | off;          # 使用哪个缓存区,默认 off
proxy_cache_key string;               # 缓存键,默认 $scheme$proxy_host$request_uri
proxy_cache_valid [code ...] time;     # 对不同状态码设置缓存时长
proxy_cache_path path ...;             # 定义缓存路径和参数,只能写在 http 块

完整的 proxy_cache_path 参数很多,常用的有:

nginx 复制代码
proxy_cache_path /var/cache/nginx/proxy_cache   # 缓存文件存放目录(Nginx 会自动创建)
                 levels=1:2:2                    # 目录层级
                 keys_zone=proxycache:20m        # 内存中存放 key 和元数据,20MB
                 inactive=120s                   # 120 秒未被访问则清理
                 max_size=10g;                   # 磁盘缓存上限

levels=1:2:2 怎么算?

每一级数字表示用几个十六进制字符做子目录名,一个字符有 16 种取值:

text 复制代码
第 1 级:1 个字符 → 16 个目录
第 2 级:2 个字符 → 256 个目录
第 3 级:2 个字符 → 256 个目录
合计:16 × 256 × 256 = 1,048,576 个目录

这样可以把大量缓存文件分散到不同目录,避免单目录文件过多导致查找变慢。

keys_zone=proxycache:20m 中的 20m 是内存大小,用来存 key 和元数据,大约每 MB 能存 8000 个 key。

22.3 配置缓存

先在 http 块定义缓存路径:

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf
nginx 复制代码
http {
    # ... 其它配置 ...

    # 定义名为 proxycache 的缓存区
    proxy_cache_path /usr/local/nginx/proxy_cache levels=1:2:2 keys_zone=proxycache:20m inactive=120s max_size=1g;
}

再在站点里启用:

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
location / {
    proxy_pass http://192.168.17.20;

    proxy_cache proxycache;                       # 使用上面定义的缓存区
    proxy_cache_key $host$request_uri;            # 缓存键建议带上 Host,避免多站点串缓存
    proxy_cache_valid 200 302 301 10m;            # 200/301/302 缓存 10 分钟
    proxy_cache_valid any 1m;                     # 其它状态码缓存 1 分钟

    # 在响应头里显示缓存状态,方便调试
    add_header X-Cache-Status $upstream_cache_status always;
}
bash 复制代码
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

# 第一次访问:MISS(未命中,回源)
[root@localhost ~]# curl -I http://www.qw.com/
X-Cache-Status: MISS

# 第二次访问:HIT(命中缓存)
[root@localhost ~]# curl -I http://www.qw.com/
X-Cache-Status: HIT

$upstream_cache_status 的常见值:

值 含义
MISS 未命中,回源请求后端
HIT 命中缓存,直接返回
EXPIRED 缓存已过期,重新回源
STALE 使用了过期缓存(配合 proxy_cache_use_stale)
UPDATING 缓存正在更新,先返回旧内容
REVALIDATED 缓存校验通过,继续使用
BYPASS 跳过了缓存

22.4 生产常用增强

nginx 复制代码
location / {
    proxy_pass http://192.168.17.20;
    proxy_cache proxycache;
    proxy_cache_key $host$request_uri;
    proxy_cache_valid 200 301 302 10m;

    # 缓存过期后,后端故障时先用旧缓存顶住(提升可用性)
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

    # 同一时间只允许一个请求回源,其它请求等它,避免缓存击穿
    proxy_cache_lock on;

    # 带登录态的请求不读缓存,避免把 A 用户的数据返回给 B 用户
    proxy_cache_bypass $cookie_sessionid;
    proxy_no_cache $cookie_sessionid;
}

22.5 缓存的坑与安全

  1. 不要把个性化页面缓存 :登录后的用户中心、订单页、购物车,一旦缓存就是严重的越权/信息泄露。用 Cookie、Authorization 判断并 proxy_cache_bypass。
  2. 不要盲目忽略 Set-Cookie :proxy_ignore_headers Set-Cookie 会让带登录态的响应被缓存,风险极高。
  3. 缓存键要包含 Host :多站点共用一个 Nginx 时,只用 $request_uri 会把 A 站的页面返回给 B 站。
  4. 注意缓存投毒:攻击者通过伪造 Host、X-Forwarded-Host 等头,让缓存存下恶意跳转。缓存键里包含可信的 Host,或显式校验。
  5. 更新内容要能失效 :改版后要么清理缓存目录,要么用带版本号的 URL(app.js?v=2),否则用户看到旧页面。

排查技巧 :响应头里的 X-Cache-Status 是最直接的证据。MISS 多说明缓存没生效(可能被 Cache-Control: no-store 或 Cookie 绕过);HIT 多但内容旧,检查缓存时长和失效策略。

二十三、七层负载均衡(HTTP)

23.1 什么是七层负载均衡

七层指 OSI 的应用层。Nginx 能看懂 HTTP 请求的方法、URL、Header、Cookie,因此可以按 URI、Host、Cookie 把请求分给不同后端,还能做健康检查、会话保持、缓存。

text 复制代码
             ┌──────────────┐
客户端 ─────▶│ Nginx (L7)   │
             └──────┬───────┘
          ┌─────────┴─────────┐
          ▼                   ▼
   ┌─────────────┐     ┌─────────────┐
   │ Web 节点 1  │     │ Web 节点 2  │
   │ .20:80      │     │ .30:80      │
   └─────────────┘     └─────────────┘

ngx_http_upstream_module 提供服务器分组、权重、状态监测和调度算法。

23.2 upstream 基本语法

nginx 复制代码
# 定义一组后端服务器,必须写在 http 块内
upstream 组名 {
    server 地址 [参数];
    ...
}
nginx 复制代码
upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    server backup1.example.com backup;
}

server 后面的常用参数:

参数 作用
weight=number 权重,默认 1;权重越大分到的请求越多,类似 LVS 的 WRR
max_conns=number 该后端最大活动连接数,默认 0 表示不限
max_fails=number 连续失败多少次标记为不可用,默认 1;只有真实请求经过时才会检测,不是主动探测
fail_timeout=time 标记不可用后,隔多久再尝试;同时作为统计失败次数的时间窗口,默认 10s
backup 备份服务器,所有主服务器都不可用时才启用
down 手动标记为下线,用于平滑摘除节点
resolve 后端是域名时,DNS 变化后自动更新 IP,需要配合 resolver

max_fails / fail_timeout 属于被动健康检查:只有客户端请求打过去失败了才会计数,不会像 Keepalived 那样主动发探测包。开源版 Nginx 不支持主动健康检查(Nginx Plus 支持)。

23.3 默认轮询与权重

先准备两个后端站点:

bash 复制代码
# 在 192.168.17.20 上
[root@localhost ~]# dnf install httpd -y
[root@localhost ~]# echo "web-192.168.17.20" > /var/www/html/index.html
[root@localhost ~]# systemctl enable --now httpd

# 在 192.168.17.30 上
[root@localhost ~]# dnf install httpd -y
[root@localhost ~]# echo "web-192.168.17.30" > /var/www/html/index.html
[root@localhost ~]# systemctl enable --now httpd

在 Nginx 上配置:

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
# 定义后端服务器组
upstream webcluster {
    server 192.168.17.20:80 weight=2 max_fails=2 fail_timeout=5s;
    server 192.168.17.30:80 weight=2 max_fails=2 fail_timeout=5s;
}

server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    location / {
        # 转发到上面定义的组,Nginx 会按算法选择节点
        proxy_pass http://webcluster;
    }
}
bash 复制代码
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

# 连续访问 6 次,观察后端切换
[root@localhost ~]# for i in {1..6}; do curl -s http://www.qw.com/; done
web-192.168.17.20
web-192.168.17.30
web-192.168.17.20
web-192.168.17.30
web-192.168.17.20
web-192.168.17.30

想直观看到权重效果,把权重改成 3:1,再连续访问 8 次,会看到大约 3 次 .20、1 次 .30。

故障切换测试
bash 复制代码
# 停掉 .30 上的 httpd
[root@localhost ~]# systemctl stop httpd

# 继续访问:请求会全部落到 .20
[root@localhost ~]# for i in {1..4}; do curl -s http://www.qw.com/; done
web-192.168.17.20
web-192.168.17.20
web-192.168.17.20
web-192.168.17.20

在 max_fails=2 fail_timeout=5s 下,连续失败 2 次后,.30 会被标记不可用 5 秒;5 秒后再尝试恢复。恢复后会自动重新加入。

23.4 常用调度算法

ip_hash:按客户端 IP 做会话保持
nginx 复制代码
upstream webcluster {
    ip_hash;
    server 192.168.17.20:80;
    server 192.168.17.30:80;
}

同一个客户端 IP 每次都分到同一台后端,适合没有共享 Session 的老应用。

bash 复制代码
[root@nginx1 ~]# systemctl restart nginx.service
# 同一台客户端连续访问,结果应该固定
[root@localhost ~]# for i in {1..4}; do curl -s http://www.qw.com/; done

局限:

  • 只取 IPv4 前三段做 hash,NAT 后面的大量用户可能全被分到一台;
  • 后端节点增减时,大量用户会被重新分配;
  • 不适合 IPv6 或客户端 IP 变化频繁的场景。
hash:按指定 key 做 hash
nginx 复制代码
upstream webcluster {
    # 按请求 URI 做 hash,同一 URL 固定到同一台(适合缓存命中)
    hash $request_uri consistent;

    # 或按 Cookie 中的 sessionid 做会话绑定
    # hash $cookie_sessionid;

    server 192.168.17.20:80;
    server 192.168.17.30:80;
}

consistent 表示使用一致性哈希(ketama 算法)。普通取模哈希在节点变化时几乎全部 key 都要重新分配;一致性哈希只影响相邻的一部分 key,适合后端是缓存服务器的场景。

least_conn:最少连接
nginx 复制代码
upstream webcluster {
    least_conn;           # 优先分给当前连接数最少的后端
    server 192.168.17.20:80;
    server 192.168.17.30:80;
}

适合请求处理时间差异较大的场景,避免某台后端堆积大量长请求。

算法对比
算法 依据 优点 缺点 适用场景
轮询(默认) 顺序 简单、均匀 不考虑负载 后端性能一致
weight 权重 可按机器性能分配 需手工评估 新旧机器混用
ip_hash 客户端 IP 天然会话保持 NAT 聚集、节点变化重分配 无共享 Session
hash 指定 key 缓存命中率高、可会话绑定 key 设计不当会倾斜 缓存、分片
least_conn 当前连接数 动态均衡 不感知处理能力 长请求多

23.5 后端长连接

默认 Nginx 每次代理都新建到后端的 TCP 连接,高并发下开销很大。可以开启 upstream 长连接:

nginx 复制代码
upstream webcluster {
    server 192.168.17.20:80;
    server 192.168.17.30:80;

    # 每个 worker 与后端保持最多 32 个空闲长连接
    keepalive 32;
}

server {
    location / {
        proxy_pass http://webcluster;

        # 必须改成 HTTP/1.1 才能复用连接
        proxy_http_version 1.1;

        # 清空 Connection 头,否则后端会认为要关闭连接
        proxy_set_header Connection "";
    }
}

三个条件缺一不可:keepalive、proxy_http_version 1.1、Connection ""。

23.6 其他实用配置

nginx 复制代码
upstream webcluster {
    server 192.168.17.20:80;
    server 192.168.17.30:80 down;    # 手动下线,平滑摘除节点
}

location / {
    proxy_pass http://webcluster;
    proxy_next_upstream error timeout http_502 http_503 http_504;
    # 当前节点失败时,自动换下一个节点重试
}

二十四、四层负载均衡(TCP/UDP)

24.1 四层与七层的区别

对比项 四层(L4) 七层(L7)
工作层次 传输层(TCP/UDP) 应用层(HTTP/HTTPS)
能看懂什么 IP、端口 URL、Header、Cookie、方法
性能 更高(只转发字节流) 略低(要解析 HTTP)
能否按 URL 分流 不能 能
能否做 HTTP 缓存 不能 能
典型场景 MySQL、Redis、DNS、TCP 长连接 Web 站点、API 网关

四层代理由 ngx_stream_core_module 和 ngx_stream_proxy_module 提供,编译时要加 --with-stream(本文已加)。

层级注意 :stream {} 必须和 http {} 同级 ,都写在 nginx.conf 的最外层,不能写进 http 里面。

24.2 TCP 负载均衡:以 MySQL/MariaDB 为例

目标:客户端连接 192.168.17.10:3306,Nginx 把连接分给 192.168.17.20 和 192.168.17.30 上的 MariaDB。

后端准备

在两台后端上安装并启动 MariaDB:

bash 复制代码
# 安装 MariaDB 服务端
[root@localhost ~]# dnf install mariadb-server -y

# 启动并设置开机自启
[root@localhost ~]# systemctl enable --now mariadb

分别设置 server-id,方便验证请求落到了哪台:

bash 复制代码
# 在 192.168.17.20 上
[root@localhost ~]# vim /etc/my.cnf.d/mariadb-server.cnf
[mysqld]
server-id=20
bash 复制代码
# 在 192.168.17.30 上
[root@localhost ~]# vim /etc/my.cnf.d/mariadb-server.cnf
[mysqld]
server-id=30
bash 复制代码
# 重载配置并重启
[root@localhost ~]# systemctl restart mariadb

如果后端只监听 127.0.0.1,需要把 bind-address 改成内网 IP 或 0.0.0.0,否则 Nginx 连不上。生产环境请只监听内网网卡,并用防火墙限制来源。

创建允许远程登录的用户(实验用 root,生产请用最小权限账号):

bash 复制代码
# 登录本机 MariaDB
[root@localhost ~]# mysql -uroot

# 创建可从任意主机登录的用户(实验环境;生产不要给 root 开 %)
MariaDB [(none)]> CREATE USER 'root'@'%' IDENTIFIED BY '123';
MariaDB [(none)]> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> EXIT;
Nginx 四层代理配置
bash 复制代码
# 在主配置最外层加入 stream 块(与 http 同级)
[root@nginx1 ~]# vim /usr/local/nginx/conf/nginx.conf
nginx 复制代码
# 放在 nginx.conf 最外层,和 http {} 平级
stream {
    include /usr/local/nginx/tcp_conf.d/*.conf;
}
bash 复制代码
# 创建四层代理配置目录
[root@nginx1 ~]# mkdir -p /usr/local/nginx/tcp_conf.d

# 编辑 MySQL 代理配置
[root@nginx1 ~]# vim /usr/local/nginx/tcp_conf.d/mariadb.conf
nginx 复制代码
# 后端 MariaDB 服务器组
upstream mariadbcluster {
    server 192.168.17.20:3306 max_fails=2 fail_timeout=15s;
    server 192.168.17.30:3306 max_fails=2 fail_timeout=15s;
}

server {
    # 监听代理入口的 3306
    listen 192.168.17.10:3306;

    # 转发到上面的服务器组
    proxy_pass mariadbcluster;

    # 与后端建立连接的超时
    proxy_connect_timeout 30s;

    # 连接空闲超时(数据库长连接可以设大一些)
    proxy_timeout 300s;
}

一个容易踩的坑 :如果 tcp_conf.d 里的文件自己带了 stream { ... },又在 nginx.conf 外层用 stream { include ... } 包一层,就会出现重复嵌套。tcp_conf.d/*.conf 里只写 upstream 和 server,外层统一由 nginx.conf 的 stream {} 包住,结构更清晰。

bash 复制代码
# 检查并重启
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service
测试

在客户端安装 MariaDB 客户端(或 mysql 客户端):

bash 复制代码
# 安装客户端工具
[root@localhost ~]# dnf install mariadb -y

# 通过代理连接,查询当前后端的 server-id
[root@localhost ~]# mysql -h 192.168.17.10 -uroot -p123 -e "select @@server_id;"
+-------------+
| @@server_id |
+-------------+
|          20 |
+-------------+

# 再执行几次,会看到 20 和 30 交替(或按权重分配)
[root@localhost ~]# for i in {1..6}; do mysql -h 192.168.17.10 -uroot -p123 -N -e "select @@server_id;"; done
20
30
20
30
20
30

四层代理只看 IP 和端口,不理解 MySQL 协议,因此无法做读写分离。读写分离需要在后端用中间件(如 ProxySQL、MyCat)或应用层实现。

24.3 UDP 负载均衡:以 DNS 为例

DNS 默认使用 UDP 53 ,响应过大或区域传送时用 TCP 53。四层代理需要同时转发 UDP 和 TCP。

后端安装 BIND

在两台后端(192.168.17.20、192.168.17.30)上:

bash 复制代码
# 安装 DNS 服务
[root@localhost ~]# dnf install bind -y

修改主配置:

bash 复制代码
[root@localhost ~]# vim /etc/named.conf

把监听地址和查询权限放开(实验环境):

text 复制代码
// 监听所有地址的 53 端口
listen-on port 53 { any; };
listen-on-v6 port 53 { ::1; };

// 允许任意客户端查询
allow-query     { any; };

// 实验环境关闭 DNSSEC 校验,避免解析失败
dnssec-validation no;

原配置里是 listen-on port 53 { 127.0.0.1; }; 和 allow-query { localhost; };,只允许本机。作为后端 DNS 必须放开,但生产环境千万不要对公网开放递归解析,否则会被利用做 DNS 放大攻击。

添加区域:

bash 复制代码
[root@localhost ~]# vim /etc/named.rfc1912.zones
text 复制代码
zone "qw.com" IN {
    type master;
    file "qw.com.zone";
    allow-update { none; };
};

创建区域文件:

bash 复制代码
# 复制模板,保留权限和时间戳
[root@localhost ~]# cp -p /var/named/named.localhost /var/named/qw.com.zone

# 编辑区域文件
[root@localhost ~]# vim /var/named/qw.com.zone
text 复制代码
$TTL 1D
@       IN SOA  dns.qw.com. root.qw.com.  (
                                        0       ; serial   序列号,修改后要递增
                                        1D      ; refresh  从服务器刷新间隔
                                        1H      ; retry    重试间隔
                                        1W      ; expire   过期时间
                                        3H )    ; minimum  否定缓存时间
        NS      dns.qw.com.                ; 域名服务器
dns     A       192.168.17.10              ; DNS 服务器地址(指向代理入口)
www     A       192.168.17.10              ; 网站地址(指向代理入口)

字段解释:

  • @ 代表当前区域 qw.com;
  • SOA 是起始授权记录,定义主从同步参数;
  • serial 每次修改区域文件都要加 1,从服务器靠它判断是否更新;
  • NS 指定权威 DNS 服务器;
  • A 记录把域名映射到 IPv4 地址。
bash 复制代码
# 检查配置语法
[root@localhost ~]# named-checkconf
[root@localhost ~]# named-checkzone qw.com /var/named/qw.com.zone

# 重启 DNS 服务
[root@localhost ~]# systemctl restart named

# 本机验证
[root@localhost ~]# dig @127.0.0.1 www.qw.com +short
192.168.17.10
Nginx 转发 DNS
bash 复制代码
# 复制一份配置作为模板
[root@nginx1 ~]# cp /usr/local/nginx/tcp_conf.d/mariadb.conf /usr/local/nginx/tcp_conf.d/dns.conf

# 编辑 DNS 代理配置
[root@nginx1 ~]# vim /usr/local/nginx/tcp_conf.d/dns.conf
nginx 复制代码
# 后端 DNS 服务器组
upstream dnscluster {
    server 192.168.17.20:53 max_fails=2 fail_timeout=15s;
    server 192.168.17.30:53 max_fails=2 fail_timeout=15s;
}

# UDP 53:处理普通查询
server {
    listen 192.168.17.10:53 udp;
    proxy_pass dnscluster;
    proxy_connect_timeout 30s;
    proxy_timeout 300s;

    # UDP 是无连接的,收到 1 个响应报文就结束本次会话
    proxy_responses 1;
}

# TCP 53:处理大响应、区域传送
server {
    listen 192.168.17.10:53;
    proxy_pass dnscluster;
    proxy_connect_timeout 30s;
    proxy_timeout 300s;
}
bash 复制代码
# 检查并重启
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

# netstat 来自 net-tools;命令不存在时先 dnf install net-tools -y,或直接用 ss -lntup 代替
# 查看 Nginx 是否同时监听 TCP/UDP 53、3306、80、443
[root@nginx1 ~]# netstat -antlupe | grep nginx
tcp   0  0 192.168.17.10:53    0.0.0.0:*  LISTEN  0  540029  37482/nginx: master
tcp   0  0 0.0.0.0:443          0.0.0.0:*  LISTEN  0  540032  37482/nginx: master
tcp   0  0 192.168.17.10:3306  0.0.0.0:*  LISTEN  0  540030  37482/nginx: master
tcp   0  0 0.0.0.0:80           0.0.0.0:*  LISTEN  0  540031  37482/nginx: master
udp   0  0 192.168.17.10:53    0.0.0.0:*             0  540028  37482/nginx: master

各列含义:协议、接收队列、发送队列、本地地址、对端地址、状态、用户、inode、PID/程序名。

测试
bash 复制代码
# 安装 dig 工具
[root@localhost ~]# dnf install bind-utils -y

# 向代理入口查询 DNS
[root@localhost ~]# dig @192.168.17.10 www.qw.com +short
192.168.17.10

# 多次查询,后端会轮询(可用后端日志或抓包验证)
[root@localhost ~]# for i in {1..4}; do dig @192.168.17.10 www.qw.com +short; done

二十五、全站 HTTPS

25.1 HTTPS 解决什么问题

HTTP 明文传输,账号密码、Cookie、订单信息在链路上都能被看到,也可能被中间人篡改。HTTPS = HTTP + TLS:

  • 加密:传输内容看不懂;
  • 完整性:内容被改动能被发现;
  • 身份认证:证书证明"你连的确实是这个网站"。

一次 TLS 握手(简化):

text 复制代码
客户端                                            服务器
   │ ── ClientHello(TLS 版本、加密套件、随机数)──▶ │
   │ ◀─ ServerHello + 证书(含公钥)─────────────── │
   │    验证证书链、域名、有效期                     │
   │    生成会话密钥,用服务器公钥加密 ─────────────▶ │
   │    双方用会话密钥对称加密通信                   │
  • 非对称加密(RSA/ECDHE)用来安全地协商密钥;
  • 对称加密(AES/ChaCha20)用来加密真正的业务数据,速度快。

25.2 生成自签名证书

实验环境没有公网域名,用自签名证书即可。生产环境请用 Let's Encrypt、阿里云/腾讯云证书或企业 CA。

bash 复制代码
# 创建证书目录(openssl 不会自动创建目录,这一步不能省)
[root@nginx1 ~]# mkdir -p /usr/local/nginx/certs

# 生成私钥 + 自签名证书
# -newkey rsa:2048      生成 2048 位 RSA 私钥
# -nodes                私钥不加密(重启服务时不用输密码)
# -sha256               使用 SHA-256 摘要
# -keyout              私钥输出路径
# -x509                 直接生成自签名证书,而不是证书请求
# -days 365             有效期 365 天
# -out                 证书输出路径
[root@nginx1 ~]# openssl req -newkey rsa:2048 -nodes -sha256 \
  -keyout /usr/local/nginx/certs/qw.key \
  -x509 -days 365 \
  -out /usr/local/nginx/certs/qw.crt

执行过程中会提示输入证书信息:

text 复制代码
Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) []:QW
Organizational Unit Name (eg, section) []:IT
Common Name (eg, your name or your server's hostname) []:www.qw.com
Email Address []:

Common Name 必须填写访问用的域名 (这里是 www.qw.com),否则浏览器会提示证书名称不匹配。

更省事、也更规范的非交互写法(带 SAN 扩展):

bash 复制代码
[root@nginx1 ~]# openssl req -newkey rsa:2048 -nodes -sha256 -x509 -days 365 \
  -keyout /usr/local/nginx/certs/qw.key \
  -out /usr/local/nginx/certs/qw.crt \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=QW/OU=IT/CN=www.qw.com" \
  -addext "subjectAltName=DNS:www.qw.com,DNS:qw.com"

现代浏览器主要看 SAN(Subject Alternative Name) ,只写 CN 已经不够。-addext 需要 OpenSSL 1.1.1 以上,RHEL 9 自带的 OpenSSL 3 支持。

保护私钥:

bash 复制代码
# 私钥只允许 root 读写,防止泄露
[root@nginx1 ~]# chmod 600 /usr/local/nginx/certs/qw.key
[root@nginx1 ~]# ls -l /usr/local/nginx/certs/

25.3 配置 HTTPS 与 HTTP 跳转

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;                    # 同时监听 80,用于接收 HTTP 并跳转
    listen 443 ssl;               # 监听 443,开启 SSL

    # 证书和私钥
    ssl_certificate     /usr/local/nginx/certs/qw.crt;
    ssl_certificate_key /usr/local/nginx/certs/qw.key;

    # TLS 会话缓存:同一客户端再次连接时复用会话,减少握手开销
    # shared:sslcache:20m 表示共享内存区名称和大小,20MB 约可存 8 万个会话
    ssl_session_cache   shared:sslcache:20m;
    ssl_session_timeout 10m;

    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;             # 注意行尾不要多写字符

    location / {
        # 如果请求是 HTTP,就 301 跳到 HTTPS
        if ($scheme = http) {
            rewrite ^/(.*)$ https://$host/$1 redirect;
        }
    }
}
bash 复制代码
# 检查并重启
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

测试:

bash 复制代码
# HTTP 访问:返回 301,Location 指向 https
[root@localhost ~]# curl -I http://www.qw.com/
HTTP/1.1 301 Moved Permanently
Location: https://www.qw.com/

# HTTPS 访问:-k 表示忽略自签名证书的校验
[root@localhost ~]# curl -k -I https://www.qw.com/
HTTP/1.1 200 OK

# 查看证书信息
[root@localhost ~]# openssl s_client -connect www.qw.com:443 -servername www.qw.com </dev/null | openssl x509 -noout -subject -dates

浏览器访问 https://www.qw.com 会提示"证书不受信任",因为这是自签名证书。点击"高级"→"继续访问"即可:

nginx 复制代码
server {
 listen 80;
 server_name www.qw.com;
    return 301 https://$host$request_uri;
   }
   
server {
listen 443 ssl;
 server_name www.qw.com;
    # ... 证书与站点配置 ...
   }

25.4 HTTPS 安全加固

nginx 复制代码
server {
    listen 443 ssl;
    http2 on;                                  # Nginx 1.25.1+ 的新写法,启用 HTTP/2

    ssl_certificate     /usr/local/nginx/certs/qw.crt;
    ssl_certificate_key /usr/local/nginx/certs/qw.key;

    # 只启用安全的 TLS 版本,禁用 TLS 1.0/1.1
    ssl_protocols TLSv1.2 TLSv1.3;

    # 加密套件:优先使用前向安全的 ECDHE
    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 on;

    # 开启 OCSP Stapling,减少客户端校验证书的延迟
    ssl_stapling on;
    ssl_stapling_verify on;

    # HSTS:告诉浏览器以后只用 HTTPS 访问
    # 确认全站(含子域名)都支持 HTTPS 后再开启,否则可能把用户挡在门外
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 隐藏版本信息
    server_tokens off;
}

注意 :如果 Nginx 前面还有负载均衡/CDN,Nginx 看到的 $scheme 可能仍是 http,导致循环跳转。需要根据 X-Forwarded-Proto 判断,或只在最外层做跳转。


二十六、FastCGI 原理

26.1 从 CGI 到 FastCGI

CGI(Common Gateway Interface) 是最早的 Web 服务器与程序通信的协议。它的工作方式很"重":

text 复制代码
每个请求 → Web 服务器创建一个 CGI 进程 → 加载 php.ini、初始化环境 → 执行脚本 → 销毁进程

请求结束后进程就没了,下一次请求又要重新创建、重新初始化,效率很低。

FastCGI 改进了这一点:进程常驻,处理完一个请求后不退出,继续等待下一个请求。

text 复制代码
Web 服务器 ──FastCGI 协议──▶ php-fpm master
                                   │
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
                worker 1       worker 2       worker N
                (常驻,处理多个请求)

好处:

  • 省去频繁创建/销毁进程的开销;
  • php.ini 只加载一次;
  • 支持进程池、优雅重启、慢日志;
  • 与 Web 服务器解耦,Nginx 挂了 PHP 还能活着。

26.2 FastCGI 相关指令

nginx 复制代码
fastcgi_pass address:port;
# 转发请求到后端 FastCGI 服务,address 可以是 IP:端口或 unix socket
# 可用位置:location、location 内的 if

fastcgi_index name;
# FastCGI 默认首页,例如 fastcgi_index index.php;

fastcgi_param parameter value [if_not_empty];
# 设置传递给 FastCGI 的参数,值可以是文本、变量或组合

最关键的参数:

nginx 复制代码
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# PHP 要执行哪个文件:站点根目录 + 请求的脚本路径

常见的 FastCGI 参数:

nginx 复制代码
fastcgi_param REMOTE_ADDR     $remote_addr;     # 客户端 IP
fastcgi_param REMOTE_PORT     $remote_port;     # 客户端端口
fastcgi_param SERVER_ADDR     $server_addr;     # 服务器 IP
fastcgi_param SERVER_PORT     $server_port;     # 服务器端口
fastcgi_param SERVER_NAME     $server_name;     # 虚拟主机名
fastcgi_param REQUEST_METHOD  $request_method;  # 请求方法
fastcgi_param QUERY_STRING    $query_string;    # 查询参数
fastcgi_param CONTENT_TYPE    $content_type;    # 请求体类型
fastcgi_param CONTENT_LENGTH  $content_length;  # 请求体长度
fastcgi.conf 与 fastcgi_params 的区别

这两个文件内容几乎一样,关键差异是:

  • fastcgi.conf 自带 SCRIPT_FILENAME;
  • fastcgi_params 不带 SCRIPT_FILENAME,使用时必须自己补一行。
nginx 复制代码
# 用 fastcgi.conf:直接 include 即可
include fastcgi.conf;

# 用 fastcgi_params:必须自己写 SCRIPT_FILENAME,否则 PHP 报 "File not found"
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

26.3 超时与缓冲区

nginx 复制代码
fastcgi_connect_timeout 60s;    # 连接 php-fpm 的超时
fastcgi_send_timeout 60s;       # 发送请求的超时
fastcgi_read_timeout 60s;       # 读取响应的超时,太短会导致 504

fastcgi_buffer_size 32k;        # 存放响应头的缓冲区
fastcgi_buffers 16 16k;         # 响应体缓冲区,数量 × 大小

如果后端返回的响应头很大,可能报:

text 复制代码
upstream sent too big header while reading response header from upstream

解决:适当调大 fastcgi_buffer_size 和 fastcgi_buffers。


二十七、源码编译 PHP 8.4

27.1 安装依赖

bash 复制代码
# 进入源码目录
[root@nginx1 src]# cd /usr/local/src

# 安装 PHP 编译所需依赖
# bzip2:bzip2 压缩支持
# systemd-devel:php-fpm 集成 systemd 支持
# libxml2-devel:XML/DOM 扩展
# sqlite-devel:SQLite/PDO_SQLITE 支持
# libpng-devel:GD 图像处理库依赖
# libcurl-devel:curl 扩展
# oniguruma-devel:mbstring 正则依赖
[root@nginx1 src]# dnf install -y bzip2 systemd-devel libxml2-devel sqlite-devel libpng-devel libcurl-devel oniguruma-devel

# 下载 PHP 8.4.24 源码
[root@nginx1 src]# wget https://www.php.net/distributions/php-8.4.24.tar.gz

# 解压
[root@nginx1 src]# tar zxf php-8.4.24.tar.gz

# 进入源码目录
[root@nginx1 src]# cd php-8.4.24/

27.2 配置编译参数

bash 复制代码
[root@nginx1 php-8.4.24]# ./configure \
--prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--enable-fpm \
--with-fpm-user=nginx \
--with-fpm-group=nginx \
--with-curl \
--with-iconv \
--with-mhash \
--with-zlib \
--with-openssl \
--with-mysqli \
--with-pdo-mysql \
--disable-debug \
--enable-sockets \
--enable-soap \
--enable-xml \
--enable-ftp \
--enable-gd \
--enable-exif \
--enable-mbstring \
--enable-bcmath \
--with-fpm-systemd
参数 作用
--prefix=/usr/local/php 安装目录
--with-config-file-path=... 指定 php.ini 的查找路径
--enable-fpm 以 FastCGI/FPM 方式运行,供 Nginx 调用
--with-fpm-user/group=nginx FPM worker 的运行用户和组
--with-curl 启用 curl 扩展
--with-iconv 字符编码转换
--with-mhash hash/mhash 支持(新版本若报 unknown option 可删掉)
--with-zlib zlib 压缩支持
--with-openssl OpenSSL 加密支持
--with-mysqli MySQL 扩展(mysqli)
--with-pdo-mysql PDO 的 MySQL 驱动
--disable-debug 关闭调试模式,减小体积、提升性能
--enable-sockets socket 扩展
--enable-soap SOAP 协议支持
--enable-xml XML 支持
--enable-ftp FTP 支持
--enable-gd GD 图像处理库
--enable-exif 读取图片 EXIF 元数据
--enable-mbstring 多字节字符串,处理中文必备
--enable-bcmath 高精度数学运算
--with-fpm-systemd 支持 systemd 管理 php-fpm
bash 复制代码
# 编译(-j$(nproc) 用满所有 CPU 核心,加快速度)
[root@nginx1 php-8.4.24]# make -j$(nproc)

# 安装
[root@nginx1 php-8.4.24]# make install

27.3 配置环境变量

bash 复制代码
# 把 PHP 的可执行目录加入 PATH
[root@nginx1 ~]# vim ~/.bash_profile
export PATH=$PATH:/usr/local/nginx/sbin:/usr/local/php/bin

# 生效
[root@nginx1 ~]# source ~/.bash_profile

# 验证
[root@nginx1 ~]# php -v
PHP 8.4.24 (cli) ...

27.4 配置 php-fpm

bash 复制代码
# 进入配置目录
[root@nginx1 ~]# cd /usr/local/php/etc

# 从模板复制主配置
[root@nginx1 etc]# cp php-fpm.conf.default php-fpm.conf

# 开启 pid 文件,便于 systemd 跟踪
[root@nginx1 etc]# vim php-fpm.conf
pid = run/php-fpm.pid

pid = run/php-fpm.pid 是相对 FPM prefix(通常是 /usr/local/php/var)的路径,实际文件在 /usr/local/php/var/run/php-fpm.pid。

bash 复制代码
# 进入进程池配置目录
[root@nginx1 etc]# cd php-fpm.d/

# 复制默认池配置
[root@nginx1 php-fpm.d]# cp www.conf.default www.conf

# 修改监听地址
[root@nginx1 php-fpm.d]# vim www.conf
ini 复制代码
; 只监听本机,Nginx 和 php-fpm 在同一台机器上
listen = 127.0.0.1:9000

; 进程池运行用户(与 Nginx worker 一致)
user = nginx
group = nginx

; 动态进程管理:按负载自动增减 worker
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10

pm.max_children 要根据内存计算:每个 PHP 进程约占 20~50MB,假设每个 30MB、可用内存 1GB,则最多约 30 个。设置过大容易 OOM。

复制 PHP 主配置文件:

bash 复制代码
# 生产环境推荐用 php.ini-production
[root@nginx1 etc]# cp /usr/local/src/php-8.4.24/php.ini-production php.ini

# 设置时区,避免日志时间差 8 小时
[root@nginx1 etc]# vim php.ini
date.timezone = Asia/Shanghai

# 安全项:禁止 PHP 自动把路径信息当作脚本执行
cgi.fix_pathinfo=0

# 隐藏 PHP 版本(响应头 X-Powered-By)
expose_php = Off

27.5 配置 systemd 并启动

bash 复制代码
# 复制官方提供的 systemd 单元
[root@nginx1 ~]# cp /usr/local/src/php-8.4.24/sapi/fpm/php-fpm.service /lib/systemd/system/

# 编辑服务文件
[root@nginx1 ~]# cd /lib/systemd/system
[root@nginx1 system]# vim php-fpm.service

如果服务启动失败,可以先把这一行注释掉:

ini 复制代码
# ProtectSystem=full 会把 /usr、/boot 挂成只读,
# 可能影响 PHP 写入 /usr/local 下的目录,实验环境先注释
#ProtectSystem=full
bash 复制代码
# 重新加载 systemd
[root@nginx1 system]# systemctl daemon-reload

# 启动 php-fpm
[root@nginx1 system]# systemctl enable --now php-fpm.service

# 查看状态
[root@nginx1 system]# systemctl status php-fpm.service

# 检查 9000 端口
[root@nginx1 system]# ss -lntp | grep 9000

# 验证 PHP 模块
[root@nginx1 system]# php -m | head

生产建议:用 Unix Socket 代替 TCP 端口,性能更好且不会暴露网络端口:

ini 复制代码
listen = /run/php-fpm/www.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0660

然后在 Nginx 里写 fastcgi_pass unix:/run/php-fpm/www.sock;。


二十八、PHP 与 Nginx 整合

28.1 配置 FastCGI 转发

bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html index.php;

    # 匹配所有 .php 结尾的请求
    location ~* \.(php)$ {
        # 转发给本机 php-fpm
        fastcgi_pass 127.0.0.1:9000;

        # 默认首页
        fastcgi_index index.php;

        # 引入包含 SCRIPT_FILENAME 等参数的配置文件
        include fastcgi.conf;
    }
}

创建测试页面:

bash 复制代码
# 写一个 phpinfo 页面(仅用于测试,生产环境务必删除)
[root@nginx1 ~]# vim /var/www/qw/html/index.php
<?php phpinfo(); ?>
bash 复制代码
# 检查并重载
[root@nginx1 ~]# nginx -t
[root@nginx1 ~]# systemctl restart nginx.service

# 访问验证
[root@localhost ~]# curl -I http://www.qw.com/index.php
HTTP/1.1 200 OK

浏览器打开 http://www.qw.com/index.php,看到 PHP 信息页就说明整合成功:

28.2 更严谨的 PHP location 写法

nginx 复制代码
location ~ \.php$ {
    # 文件不存在直接 404,避免把不存在的路径交给 PHP
    try_files $uri =404;

    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;

    # 使用 fastcgi_params 时,必须自己补 SCRIPT_FILENAME
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

}

为什么要加 try_files $uri =404;?

如果 PHP 开启了 cgi.fix_pathinfo=1,访问 /uploads/evil.jpg/x.php 时,PHP 可能把 evil.jpg 当成脚本执行,这是经典的解析漏洞。try_files 确保请求的文件真实存在,cgi.fix_pathinfo=0 进一步加固。

28.3 PHP 站点安全清单

  • 测试完立即删除 phpinfo() 页面;
  • expose_php = Off,不暴露 PHP 版本;
  • cgi.fix_pathinfo=0;
  • 上传目录禁止执行 PHP:
nginx 复制代码
location ^~ /uploads/ {
    # 上传目录只允许静态文件,不匹配 .php
    location ~ \.php$ {
        deny all;
    }
}
  • 限制可访问目录:
ini 复制代码
open_basedir = /var/www/qw/html:/tmp
  • 禁用危险函数(按业务评估):
ini 复制代码
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
  • php-fpm 以 nginx 普通用户运行,绝不用 root;
  • 9000 端口只监听 127.0.0.1;
  • 生产环境开启 PHP 错误日志,但不要把错误显示给用户:
ini 复制代码
display_errors = Off
log_errors = On
error_log = /var/log/php-fpm/error.log

二十九、PHP 扩展与页面缓存

29.1 安装 memcached 服务

memcached 是一个高性能的分布式内存对象缓存系统,常用来缓存数据库查询结果、Session、页面片段。

bash 复制代码
# 安装 memcached
[root@nginx1 ~]# dnf install memcached -y

# 编辑配置文件
[root@nginx1 ~]# vim /etc/sysconfig/memcached
bash 复制代码
PORT="11211"                    # 监听端口
USER="memcached"                # 运行用户
MAXCONN="10000"                 # 最大连接数
CACHESIZE="64"                  # 内存缓存大小,单位 MB
OPTIONS="-l 0.0.0.0,::1"        # 监听地址(实验环境)
bash 复制代码
# 启动并设置开机自启
[root@nginx1 ~]# systemctl enable --now memcached.service

# 检查端口
[root@nginx1 ~]# ss -lntp | grep 11211

重要安全提醒 :memcached 默认没有任何认证 ,任何人连上就能读写缓存、查看数据。原配置监听 0.0.0.0 只适合隔离的实验环境。生产环境应改为 -l 127.0.0.1 或用防火墙只允许应用服务器访问,并禁用 UDP(防放大攻击)。

29.2 让 PHP 支持 memcache 扩展

PHP 本身不带 memcache 扩展,需要用 PECL 源码编译。

bash 复制代码
# 回到源码目录
[root@nginx1 src]# cd /usr/local/src

# 下载 memcache 扩展源码(注意:这是 memcache,不是 memcached 扩展)
[root@nginx1 src]# wget https://pecl.php.net/get/memcache-8.2.tgz

# 解压
[root@nginx1 src]# tar zxf memcache-8.2.tgz

# phpize 需要 autoconf 等工具
[root@nginx1 src]# dnf install autoconf -y

# 进入扩展目录
[root@nginx1 src]# cd memcache-8.2/

# 用 phpize 生成 configure 脚本(把扩展编译环境对接当前 PHP)
[root@nginx1 memcache-8.2]# phpize

# 配置:--with-php-config 指明当前 PHP 的 php-config 路径
[root@nginx1 memcache-8.2]# ./configure --with-php-config=/usr/local/php/bin/php-config

# 编译
[root@nginx1 memcache-8.2]# make

# 安装:把 memcache.so 复制到 PHP 扩展目录
[root@nginx1 memcache-8.2]# make install

phpize 的作用:PHP 扩展必须针对当前安装的 PHP 版本和编译参数 编译,phpize 会生成适配的 configure 脚本,避免 ABI 不兼容。

启用扩展:

bash 复制代码
# 编辑 php.ini
[root@nginx1 memcache-8.2]# vim /usr/local/php/etc/php.ini

# 在文件末尾加入(注意要写 .so 后缀)
extension=memcache.so
bash 复制代码
# 重启 php-fpm
[root@nginx1 memcache-8.2]# systemctl restart php-fpm.service

# 确认扩展加载成功
[root@nginx1 memcache-8.2]# php -m | grep memcache
memcache

# 查看扩展详细信息
[root@nginx1 memcache-8.2]# php --ri memcache

29.3 部署 memcache 管理页面

源码包里自带 memcache.php(图形化管理页)和 example.php(示例):

bash 复制代码
# 复制到站点目录
[root@nginx1 memcache-8.2]# cp memcache.php example.php /var/www/qw/html/

# 修改管理页的账号密码和服务器地址
[root@nginx1 memcache-8.2]# vim /var/www/qw/html/memcache.php
php 复制代码
define('ADMIN_USERNAME','admin');                 // 管理页用户名
define('ADMIN_PASSWORD','123');                   // 管理页密码
$MEMCACHE_SERVERS[] = '192.168.17.10:11211';      // memcached 地址,可添加多个

浏览器访问 http://www.qw.com/memcache.php,登录后可以看到内存使用、命中率、Item 数量等:

访问 http://www.qw.com/example.php,这是官方示例,演示 PHP 如何读写 memcache:

29.4 问题:为什么 example.php 能缓存,index.php 不行?

因为 example.php 里写了 PHP 代码,主动调用 memcache 扩展去 set/get;而 index.php 只是 phpinfo(),没有调用任何缓存逻辑。

也就是说:

  • PHP 的 memcache 扩展:需要程序员在代码里显式读写;
  • Nginx 的 proxy_cache:缓存整个 HTTP 响应,但只对代理生效;
  • 想要"Nginx 自动缓存 PHP 页面的输出",需要 memc + srcache 两个第三方模块。

29.5 用 srcache + memc 缓存 PHP 页面

原理
text 复制代码
请求 /index.php?a=1
   │
   ▼
Nginx 设置缓存键 key = /index.php?a=1
   │
   ├─ srcache_fetch:向 /mem 发起子请求,去 memcached 查 key
   │      ├─ 命中 → 直接返回缓存内容,不找 PHP
   │      └─ 未命中 → 继续
   │
   ├─ fastcgi_pass:请求 php-fpm,生成页面
   │
   └─ srcache_store:把响应写入 memcached,供下次使用
编译第三方模块

Nginx 默认不带这两个模块,需要重新编译并 --add-module。

bash 复制代码
# 先停掉 Nginx(生产环境建议用 USR2 平滑升级,避免中断业务)
[root@nginx1 ~]# systemctl stop nginx.service

# 回到源码目录
[root@nginx1 ~]# cd /usr/local/src/

# 下载 memc-nginx-module(提供 memc_pass 等指令)
[root@nginx1 src]# wget https://github.com/openresty/memc-nginx-module/archive/refs/heads/master.tar.gz -O memc-nginx-module.tar.gz

# 下载 srcache-nginx-module(提供 srcache_fetch/store)
[root@nginx1 src]# wget https://github.com/openresty/srcache-nginx-module/archive/refs/heads/master.tar.gz -O srcache-nginx-module.tar.gz

# 解压两个模块
[root@nginx1 src]# tar zxf srcache-nginx-module.tar.gz
[root@nginx1 src]# tar zxf memc-nginx-module.tar.gz

# 进入 Nginx 源码目录(版本要与当前运行的一致)
[root@nginx1 src]# cd /usr/local/src/nginx-1.30.4
bash 复制代码
# 在原有参数基础上追加 --add-module
[root@nginx1 nginx-1.30.4]# ./configure \
  --prefix=/usr/local/nginx \
  --user=nginx --group=nginx \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_realip_module \
  --with-http_stub_status_module \
  --with-http_gzip_static_module \
  --with-pcre \
  --with-stream \
  --with-stream_ssl_module \
  --with-stream_realip_module \
  --add-module=/usr/local/src/srcache-nginx-module-master \
  --add-module=/usr/local/src/memc-nginx-module-master

# 编译
[root@nginx1 nginx-1.30.4]# make
bash 复制代码
# 备份旧二进制
[root@nginx1 nginx-1.30.4]# cp -p /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak

# 复制新二进制
[root@nginx1 nginx-1.30.4]# cd objs/
[root@nginx1 objs]# cp -pf nginx /usr/local/nginx/sbin/nginx

# 验证模块是否编译进去(输出里应有 srcache 和 memc)
[root@nginx1 objs]# nginx -V 2>&1 | tr ' ' '\n' | grep -E "srcache|memc"

# 启动
[root@nginx1 objs]# systemctl start nginx.service
配置 srcache
bash 复制代码
[root@nginx1 ~]# vim /usr/local/nginx/conf.d/http/qw.conf
nginx 复制代码
# memcached 服务器组
upstream memcache {
    server 127.0.0.1:11211;
    keepalive 512;                 # 与 memcached 保持长连接
}

server {
    listen 80;
    server_name www.qw.com;
    root /var/www/qw/html;
    index index.html;

    # 内部子请求:只在 Nginx 内部调用,外部无法直接访问
    location /mem {
        internal;

        # 与 memcached 交互的超时时间
        memc_connect_timeout 100ms;
        memc_send_timeout 100ms;
        memc_read_timeout 100ms;

        # 子请求的查询字符串就是缓存键
        set $memc_key $query_string;

        # 缓存过期时间 300 秒
        set $memc_exptime 300;

        # 转发到 memcached
        memc_pass memcache;
    }

    location ~* \.(php)$ {
        # 缓存键:URI + 查询参数,例如 /index.php?a=1
        set $key $uri$args;

        # 先查缓存:GET /mem?<key>
        srcache_fetch GET /mem $key;

        # 未命中则回源,并把响应写入缓存:PUT /mem?<key>
        srcache_store PUT /mem $key;

        # 回源到 php-fpm
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi.conf;
    }
}
bash 复制代码
# 检查配置
[root@nginx1 ~]# nginx -t

# 重载
[root@nginx1 ~]# systemctl reload nginx.service
验证缓存效果
bash 复制代码
# 第一次访问:缓存未命中,走 PHP,速度较慢
[root@localhost ~]# curl -o /dev/null -s -w "第一次耗时: %{time_total}s\n" http://www.qw.com/index.php

# 第二次访问:命中 memcached,不再走 PHP,速度明显变快
[root@localhost ~]# curl -o /dev/null -s -w "第二次耗时: %{time_total}s\n" http://www.qw.com/index.php

# 用 ab 压测对比
[root@localhost ~]# ab -n 1000 -c 100 http://www.qw.com/index.php

可以在 location ~* \.(php)$ 里加一行调试头,直观看到命中状态:

nginx 复制代码
add_header X-Srcache-Fetch $srcache_fetch_status always;
add_header X-Srcache-Store $srcache_store_status always;
缓存的注意事项
  1. 缓存键必须包含 Host :多站点共用 Nginx 时,用 set $key $host$uri$args;。
  2. 不要让登录用户命中公共缓存 :带 Cookie: PHPSESSID=... 的请求应跳过缓存,否则会串号。
  3. Set-Cookie 的风险:srcache 默认会把响应(包括头部)存起来,登录态页面绝不能缓存。
  4. 参数顺序 :?a=1&b=2 和 ?b=2&a=1 对业务等价,但对缓存是两个 key,会降低命中率。必要时用 map 规范化参数。
  5. 缓存失效 :内容更新后要主动删除对应 key,或设置较短的 $memc_exptime。
  6. key 长度限制:memcached 的 key 最长 250 字节,过长的 URL 会被截断或报错。

性能提示 :memc_connect_timeout 100ms 这类短超时是为了避免 memcached 故障时拖慢请求。线上还应配置 memc_next_upstream 或降级逻辑,缓存挂了也要能回源。


三十、OpenResty 部署

30.1 OpenResty 是什么

OpenResty 把 Nginx、LuaJIT 和大量高质量的第三方模块打包在一起,可以用 Lua 脚本直接控制 Nginx 的行为:

  • 动态路由、灰度发布;
  • API 网关、鉴权;
  • 限流、防刷、WAF;
  • 动态上游、服务发现;
  • 在 Nginx 内部直接访问 Redis/MySQL。

简单来说,它让 Nginx 从"静态服务器/反向代理"变成一个可编程的 Web 平台,单机可以支撑很高的并发。

30.2 编译安装

bash 复制代码
# 进入源码目录
[root@nginx1 src]# cd /usr/local/src

# 下载 OpenResty 源码
[root@nginx1 src]# wget https://openresty.org/download/openresty-1.31.1.1.tar.gz

# 解压
[root@nginx1 src]# tar zxf openresty-1.31.1.1.tar.gz

# 进入源码目录
[root@nginx1 src]# cd openresty-1.31.1.1/

# 安装依赖(注意包名是 pcre-devel,不要拼错)
[root@nginx1 openresty-1.31.1.1]# dnf install -y gcc pcre-devel openssl-devel perl zlib-devel make

# 创建运行用户(已存在就跳过)
[root@nginx1 openresty-1.31.1.1]# id nginx || useradd -r -s /sbin/nologin -M nginx

# 配置:注意 prefix 统一为 /apps/openresty
[root@nginx1 openresty-1.31.1.1]# ./configure \
  --prefix=/apps/openresty \
  --user=nginx --group=nginx \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_stub_status_module \
  --with-http_gzip_static_module \
  --with-pcre \
  --with-stream \
  --with-stream_ssl_module \
  --with-stream_realip_module

# 编译
[root@nginx1 openresty-1.31.1.1]# make -j$(nproc)

# 安装
[root@nginx1 openresty-1.31.1.1]# make install

路径一致性很重要 :--prefix、PATH、systemd 里的路径必须一致。本文统一使用 /apps/openresty;如果 prefix 写成别的目录,PATH 和 systemd 也要同步修改,否则启动会找不到程序。

30.3 配置环境变量与 systemd

bash 复制代码
# 把 OpenResty 的可执行目录加入 PATH
[root@nginx1 ~]# vim ~/.bash_profile
export PATH=$PATH:/apps/openresty/bin:/apps/openresty/nginx/sbin

# 生效
[root@nginx1 ~]# source ~/.bash_profile

# 验证
[root@nginx1 ~]# openresty -v
nginx version: openresty/1.31.1.1

创建 systemd 单元:

bash 复制代码
[root@nginx1 ~]# vim /lib/systemd/system/openresty.service
ini 复制代码
[Unit]
Description=OpenResty web platform
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
# PID 文件在 OpenResty 自己的 nginx 目录下
PIDFile=/apps/openresty/nginx/logs/nginx.pid
ExecStartPre=/apps/openresty/bin/openresty -t
ExecStart=/apps/openresty/bin/openresty
ExecReload=/apps/openresty/bin/openresty -s reload
ExecStop=/bin/kill -QUIT $MAINPID
TimeoutStopSec=120
PrivateTmp=true

[Install]
WantedBy=multi-user.target
bash 复制代码
# 重新加载 systemd
[root@nginx1 ~]# systemctl daemon-reload

# 注意:如果原来的 Nginx 还占着 80 端口,先停掉,否则 OpenResty 启动失败
[root@nginx1 ~]# systemctl stop nginx.service

# 启动 OpenResty 并设置开机自启
[root@nginx1 ~]# systemctl enable --now openresty.service

# 查看状态
[root@nginx1 ~]# systemctl status openresty.service

# 访问默认页面
[root@nginx1 ~]# curl -I http://127.0.0.1/

如果想让两套服务共存,把 OpenResty 的 listen 改成 8080,或给它们绑定不同 IP,不要同时抢占 80/443。

30.4 第一个 Lua 页面

bash 复制代码
# 编辑 OpenResty 的主配置
[root@nginx1 ~]# vim /apps/openresty/nginx/conf/nginx.conf

在 server {} 中加入:

nginx 复制代码
location /lua {
    default_type text/plain;

    # 直接执行 Lua 代码生成响应
    content_by_lua_block {
        ngx.say("hello, openresty")
        ngx.say("client ip: ", ngx.var.remote_addr)
        ngx.say("current time: ", ngx.time())
    }
}
bash 复制代码
# 检查并重载
[root@nginx1 ~]# openresty -t
[root@nginx1 ~]# systemctl reload openresty.service

# 访问
[root@nginx1 ~]# curl http://127.0.0.1/lua
hello, openresty
client ip: 127.0.0.1
current time: 1790000000

也可以直接用命令行执行 Lua:

bash 复制代码
[root@nginx1 ~]# resty -e 'print("Hello, OpenResty")'
Hello, OpenResty

三十一、知识地图与总结

31.1状态码定位

状态码 含义 常见原因 排查动作
400 Bad Request 请求格式错误 请求头过大、Host 不合法 调大 large_client_header_buffers;检查客户端
401 Unauthorized 未认证 Basic Auth 未提供或密码错 检查 .htpasswd、curl -u
403 Forbidden 禁止访问 文件/目录权限、SELinux、deny、无 index namei -l、getenforce、检查 allow/deny
404 Not Found 文件不存在 root/alias 写错、SCRIPT_FILENAME 错、location 不匹配 nginx -T、看 error.log 的实际路径
413 Too Large 请求体过大 client_max_body_size 太小 调大限制
499 客户端主动断开 后端太慢,用户等不及 优化后端;看 $request_time
500 Internal Server Error Nginx 内部错误 rewrite 循环、错误页缺失、后端 500 看 error.log
502 Bad Gateway 后端无响应 php-fpm/Apache 没启动、端口错、SELinux ss -lntp、直连后端、setsebool
504 Gateway Timeout 后端超时 PHP/数据库慢、超时设置太小 查后端日志、慢查询、调超时

31.2 一张图串起全文

text 复制代码
                         ┌─────────────────────────────┐
                         │  Web 基础                    │
                         │  访问流程 / 静态动态 / LNMP  │
                         └──────────────┬──────────────┘
                                        │
              ┌─────────────────────────┼─────────────────────────┐
              ▼                         ▼                         ▼
   ┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
   │ 原理              │     │ 安装与配置        │     │ 核心功能          │
   │ I/O 模型          │     │ 源码编译          │     │ 虚拟主机          │
   │ epoll             │     │ systemd           │     │ 访问控制          │
   │ 零拷贝            │     │ location/root     │     │ 日志/错误页       │
   │ master/worker     │     │ 平滑升级          │     │ rewrite/try_files │
   └──────────────────┘     └──────────────────┘     └────────┬─────────┘
                                                              │
              ┌───────────────────────────────────────────────┼──────────────┐
              ▼                                               ▼              ▼
   ┌──────────────────┐                           ┌──────────────────┐  ┌──────────────┐
   │ 性能优化          │                           │ 代理与集群        │  │ 动态应用      │
   │ gzip / 缓存       │                           │ 反向代理          │  │ PHP-FPM       │
   │ 长连接 / 限速     │                           │ 七层负载均衡      │  │ FastCGI       │
   │ 文件描述符        │                           │ 四层负载均衡      │  │ memcache      │
   └──────────────────┘                           │ HTTPS             │  │ srcache       │
                                                  └──────────────────┘  │ OpenResty     │
                                                                        └──────────────┘

31.3 常用命令速查

操作 命令
检查配置 nginx -t
打印完整配置 nginx -T
查看版本与编译参数 nginx -V
平滑重载 systemctl reload nginx
优雅停止 nginx -s quit / systemctl stop nginx
重开日志 nginx -s reopen / kill -USR1 <master_pid>
热升级 kill -USR2 <old_master> → kill -WINCH <old_master> → kill -QUIT <old_master>
查看端口 ss -lntup
查看进程 pgrep -a nginx
查看错误日志 tail -f /usr/local/nginx/logs/error.log
查看路径权限 namei -l /path/to/file
测试 HTTP curl -v -I http://www.qw.com/
指定解析测试 curl --resolve www.qw.com:80:192.168.17.10 http://www.qw.com/
压测 ab -n 1000 -c 100 http://www.qw.com/
查 SELinux 拒绝 ausearch -m avc -ts recent
相关推荐
笨笨饿1 小时前
# 59_CSDN博客自动写流程文章
linux·jvm·stm32·单片机·嵌入式硬件·mcu·51单片机
电商全栈程序猿1 小时前
2026 开源商城系统选型指南:开发者实测对比Tigshop、ShopXO、CRMEB
java·运维·开源
Escalating_xu1 小时前
【Linux 网络编程】传输层协议 UDP:从 8 字节首部到消息边界、缓冲区与 MTU
linux·网络·udp
时间售卖铺1 小时前
MySQL 库表设计:数据库范式详解
运维·数据库·mysql·云原生
坤坤子吖1 小时前
C++继承:继承的基本用法与访问规则
开发语言·c++·笔记·学习
启观川1 小时前
[windows] windows下将文件夹映射为磁盘
windows·笔记
ss2731 小时前
AI时代首先数据库-PostgreSQL实战笔记 | 23 PostgreSQL数据库文件系统与数据页结构
数据库·笔记·postgresql
Lucky_Turtle1 小时前
【Linux】ip命令,路由表相关配置
linux·运维·tcp/ip
dawnsky.liu1 小时前
在 RHEL 中使用 goose 实现主机运维
linux·运维·aiops