| 角色 | 主机名 | 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,大致会经历下面这些步骤:
- 浏览器解析 URL :拆出协议
http、域名www.qw.com、端口80、路径/index.html。 - DNS 解析 :把域名换成服务器 IP(实验里通常改
hosts文件)。 - 建立 TCP 连接:三次握手;如果是 HTTPS,还要多做一次 TLS 握手。
- 发送 HTTP 请求报文:请求行、请求头、请求体,经客户端网卡发出。
- 服务端网卡收包:数据通过 DMA 写入内存,触发中断,交给内核网络协议栈处理。
- 内核 → 用户空间 :内核把数据从内核空间的 Socket 接收缓冲区拷贝到 Nginx 进程的用户空间内存,Nginx 这时才"看见"请求。
- Nginx 解析请求:分析请求方法、URI、Host、请求头,决定是返回静态文件,还是转发给后端。
- 读取文件 :如果是静态资源,Nginx 根据
root/alias把 URI 映射成磁盘路径,通过系统调用请内核读文件;内核先查页缓存(Page Cache),没有再读磁盘。 - 构建响应:生成状态行、响应头、响应体。
- 内核 → 网卡:响应数据写入 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,必须满足:
- 路径上每一级目录 都对
nginx用户有x权限(能进入); - 文件本身对
nginx用户有r权限; - 如果访问的是目录,还需要目录有
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 之间传递控制消息。
启动与连接建立
- master 解析配置,创建监听 Socket(如
0.0.0.0:80); - master fork 出多个 worker,worker 继承监听 Socket 的文件描述符;
- 每个 worker 在自己的 epoll 里注册监听 Socket;
- 新连接到来时,内核唤醒其中一个 worker 去
accept; - 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 能扛高并发
- 事件驱动 + 非阻塞 I/O:一个 worker 用 epoll 管大量连接,不需要一连接一线程;
- 多进程:充分利用多核,worker 之间相互独立;
- 零拷贝:静态文件发送少两次内存拷贝;
- 内存池 :减少频繁
malloc/free的开销; - 低内存占用:空闲连接几乎不占额外线程栈。
当然,Nginx 再强也受操作系统限制:文件描述符上限、worker_connections、带宽、后端处理速度,都会成为瓶颈。这些会在"高并发优化"一节具体调。
五、源码编译安装 Nginx
5.1 为什么选择源码编译
用 dnf install nginx 也能装,但源码编译有三个好处:
- 可以自定义安装路径 (本文统一装到
/usr/local/nginx); - 可以按需开启模块(HTTPS、HTTP/2、四层代理、状态页、预压缩等);
- 可以后期加入第三方模块(后面的 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 再启动会中断所有用户请求。平滑升级(热升级) 可以在不停止业务的情况下换掉二进制:
- 老 master(旧版本)继续服务,已有 worker 处理完手头请求;
- 用新二进制启动一个新的 master 和一批新 worker;
- 新连接逐渐由新 worker 处理;
- 老 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 本身是事件驱动的,但"能扛多少连接"不是无限的,至少受三层限制:
- 操作系统 :单个进程能打开的文件描述符数量(
nofile)、进程数(nproc)、全局文件句柄上限; - Nginx 配置 :
worker_processes、worker_connections、worker_rlimit_nofile; - 硬件与后端: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
你访问的地址出错

几个关键点:
error_page末尾的/40x.html是一个内部跳转,浏览器地址栏不会变;- 状态码默认保持不变 (仍然是 404),只是响应体换了。想看状态码用
curl -I; location = /40x.html里的root /error;会让 Nginx 去找/error/40x.html;- 如果错误页本身也找不到,会出现
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 防盗链只能挡住"普通外链",不能当成安全边界。更可靠的方案:
- 签名 URL:给资源链接加上时间戳和签名(HMAC),过期失效;
- CDN 鉴权:使用云厂商的 URL 鉴权、Referer 黑白名单;
- Token + Cookie:登录后下发短期 Token;
- 水印:图片加水印,降低盗用价值。
二十、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 的执行顺序
server块中的rewrite先执行;- 然后匹配
location; location中的rewrite按配置顺序执行;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 缓存的坑与安全
- 不要把个性化页面缓存 :登录后的用户中心、订单页、购物车,一旦缓存就是严重的越权/信息泄露。用 Cookie、Authorization 判断并
proxy_cache_bypass。 - 不要盲目忽略
Set-Cookie:proxy_ignore_headers Set-Cookie会让带登录态的响应被缓存,风险极高。 - 缓存键要包含 Host :多站点共用一个 Nginx 时,只用
$request_uri会把 A 站的页面返回给 B 站。 - 注意缓存投毒:攻击者通过伪造 Host、X-Forwarded-Host 等头,让缓存存下恶意跳转。缓存键里包含可信的 Host,或显式校验。
- 更新内容要能失效 :改版后要么清理缓存目录,要么用带版本号的 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 会提示"证书不受信任",因为这是自签名证书。点击"高级"→"继续访问"即可:

nginxserver { 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 端口,性能更好且不会暴露网络端口:
inilisten = /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;
缓存的注意事项
- 缓存键必须包含 Host :多站点共用 Nginx 时,用
set $key $host$uri$args;。 - 不要让登录用户命中公共缓存 :带
Cookie: PHPSESSID=...的请求应跳过缓存,否则会串号。 - Set-Cookie 的风险:srcache 默认会把响应(包括头部)存起来,登录态页面绝不能缓存。
- 参数顺序 :
?a=1&b=2和?b=2&a=1对业务等价,但对缓存是两个 key,会降低命中率。必要时用map规范化参数。 - 缓存失效 :内容更新后要主动删除对应 key,或设置较短的
$memc_exptime。 - 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 |