Nginx架构、阶段、模块

初识 Nginx

Nginx 三个主要应用场景

  1. mir静态资源服务
    • 通过本地文件系统提供服务:例如直接提供一些 h5 资源(html,css)或者图片等静态资源
  2. 反向代理服务
    • Nginx 的强大性能:作为多个应用服务组成的集群的网关,提供高性能
    • 缓存:将一段时间内看起来不变的内容缓存到 Nginx,加速访问
    • 负载均衡:同一台 Nginx 可以同时支持多个后台服务器,并且做负载均衡
  3. API 服务
    • OpenResty:直接访问数据库,提供完整的 API 服务

Nginx 出现原因

  1. 互联网的数据量快速增长,对高并发要求越来越高
    • 互联网的快速普及
    • 全球化
    • 物联网
  2. 摩尔定律:性能提升
    • 物理硬件 CPU、内存提升之后,操作系统和应用也需要配套提升
  3. 低效的 Apache
    • 一个连接对应一个进程:操作系统进程间切换,导致并发数上不来,并发上来之后,使用进程处理,会导致大量进程创建、删除、切换,从而产生很大的开销

Nginx 的优点

  • 高并发,高性能:可以达到百万QPS
  • 可扩展性好:生态圈法阵非常好
  • 高可靠性:可以持续不间断运行数年不重启
  • 热部署:修改配置、升级后可以热加载,不需要重启
  • BSD 许可证:开源、免费,而且可以修改代码之后做商业化

Nginx 组成部分

  1. Nginx 二进制可执行文件
    • 由各模块源码编译出的一个文件
  2. nginx.conf 配置文件
    • 控制 Nginx 的行为
  3. access.log 访问日志
    • 记录每一条 http 请求信息和相应信息
  4. error.log 错误日志
    • 用于定位问题

Nginx 版本发布

https://nginx.org/

一些版本推荐:https://openresty.org/cn/ (开源版)

编译适合自己的包

下载源码

https://nginx.org/en/download.html

目录介绍

1
2
3
4
5
6
7
8
9
10
11
12
total 1624
-rw-r--r--@ 1 xuwenxiang staff 323312 Apr 11 2023 CHANGES // 改动日志
-rw-r--r--@ 1 xuwenxiang staff 494234 Apr 11 2023 CHANGES.ru // 俄罗斯版本语言改动日志
-rw-r--r--@ 1 xuwenxiang staff 1397 Apr 11 2023 LICENSE
-rw-r--r--@ 1 xuwenxiang staff 49 Apr 11 2023 README
drwxr-xr-x@ 25 xuwenxiang staff 800 Apr 11 2023 auto // 供编译使用库和环境信息
drwxr-xr-x@ 11 xuwenxiang staff 352 Apr 11 2023 conf // 存放配置的目录
-rwxr-xr-x@ 1 xuwenxiang staff 2611 Apr 11 2023 configure // 执行中间文件的直径脚本
drwxr-xr-x@ 6 xuwenxiang staff 192 Apr 11 2023 contrib // vim 的插件和脚本
drwxr-xr-x@ 4 xuwenxiang staff 128 Apr 11 2023 html // 标准处理的 html 文件
drwxr-xr-x@ 3 xuwenxiang staff 96 Apr 11 2023 man // 帮助文件
drwxr-xr-x@ 9 xuwenxiang staff 288 Apr 11 2023 src // 源代码存放目录

auto 目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
total 376
drwxr-xr-x@ 13 xuwenxiang staff 416 Apr 11 2023 cc
-rw-r--r--@ 1 xuwenxiang staff 141 Apr 11 2023 define
-rw-r--r--@ 1 xuwenxiang staff 889 Apr 11 2023 endianness
-rw-r--r--@ 1 xuwenxiang staff 2812 Apr 11 2023 feature
-rw-r--r--@ 1 xuwenxiang staff 136 Apr 11 2023 have
-rw-r--r--@ 1 xuwenxiang staff 137 Apr 11 2023 have_headers
-rw-r--r--@ 1 xuwenxiang staff 411 Apr 11 2023 headers
-rw-r--r--@ 1 xuwenxiang staff 1020 Apr 11 2023 include
-rw-r--r--@ 1 xuwenxiang staff 768 Apr 11 2023 init
-rw-r--r--@ 1 xuwenxiang staff 4875 Apr 11 2023 install
drwxr-xr-x@ 13 xuwenxiang staff 416 Apr 11 2023 lib
-rw-r--r--@ 1 xuwenxiang staff 18324 Apr 11 2023 make
-rw-r--r--@ 1 xuwenxiang staff 3934 Apr 11 2023 module
-rw-r--r--@ 1 xuwenxiang staff 38598 Apr 11 2023 modules
-rw-r--r--@ 1 xuwenxiang staff 136 Apr 11 2023 nohave
-rw-r--r--@ 1 xuwenxiang staff 25612 Apr 11 2023 options
drwxr-xr-x@ 8 xuwenxiang staff 256 Apr 11 2023 os
-rw-r--r--@ 1 xuwenxiang staff 8890 Apr 11 2023 sources
-rw-r--r--@ 1 xuwenxiang staff 120 Apr 11 2023 stubs
-rw-r--r--@ 1 xuwenxiang staff 2032 Apr 11 2023 summary
-rw-r--r--@ 1 xuwenxiang staff 394 Apr 11 2023 threads
drwxr-xr-x@ 6 xuwenxiang staff 192 Apr 11 2023 types
-rw-r--r--@ 1 xuwenxiang staff 26166 Apr 11 2023 unix

Configure 文件介绍

1
./configure --help
  1. 可以指定一些模块的文件存放位置
  2. 可以指定使用哪些模块和不适用哪些模块
  3. 可以指定哪些特殊参数,例如是否打印 debug 日志,或者是否加入第三方模块

中间文件介绍

执行 configure 后,会将中间文件生成在 objs 目录中。里面存放编译的一些配置信息

编译

执行 make 进行编译,会生成二进制可执行文件在 objs 目录中

安装

执行 make install。将指定的安装目录中,src 目录下的二进制文件拷贝出去执行即可。

Nginx 配置语法

  1. 配置文件由指令与指令块构成:指令以 ; 结尾,指令块在 {}
  2. 每条指令以 ; 号结尾,指令与参数间以空格符号分割
  3. 指令快以 {} 大括号将多条指令组织在一起
  4. include 语句允许组合多个配置文件以提升可维护性
  5. 使用 # 符号添加注释,提高可读性
  6. 使用 $ 符号使用变量
  7. 部分指令的参数支持正则表达式

官方文件,一些模块:https://nginx.org/en/docs/

例如:

image-20240319173159019

配置参数:时间的单位

image-20240319173649111

配置参数:空间的单位

image-20240319173707309

http 配置的指令快

  1. http:表示所有的指令由 http 模块解析
  2. server:对应一个域名或者一组域名
  3. location:一个 url 表达式
  4. upstream:表示上游服务,例如内网其他服务

Nginx 命令行

  1. 格式:nginx -s reload
  2. 帮助:-? -h
  3. 使用指定的配置文件:-c
  4. 指定配置指令:-g,主要用于 forget 中间的指令
  5. 指定运行目录:-p
  6. 发送信号:-s
    1. 立即停止服务:stop
    2. 优雅的停止服务:quit
    3. 重载配置文件:reload
    4. 重新开始记录日志文件:reopen
  7. 测试配置文件是否有语法错误:-t-T
  8. 打印 nginx 的版本信息、编译信息等:-v-V

示例

重载配置文件

1
2
# 修改配置文件后
nginx -s reload

热部署

1
2
3
4
5
6
7
8
# 当 nginx 正在运行,需要更新nginx,先备份二进制文件,将新的二进制nginx可执行文件拷贝到对应目录中替换原文件
# 获取 nginx 的 master 进程
ps aux | grep nginx
kill -USR2 <pid>
# 会生成一个新的 nginx master 进程,也会新启动一些 worker 进程,老的 worker 进程的请求会过渡到新的 worker 中
# 让老 nginx 进程优雅关闭 worker 进程
kill -WINCH <pid>
# 剩下的就只有老的 nginx 的 master 进程,可以安全回退

切割日志文件

1
2
3
4
5
# 先拷贝 access.log 到另外一个地方
mv access.log bak.log
# 或者 kill -USR1 <pid>
nginx -s reopen
# 切割之后,原来的日志文件大小从0开始

静态资源服务演示

例如一个 index.html 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  # 记录日志的格式,将这个格式命名成 main
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

# 将 main 这种格式的日志记录到某个文件中
access_log logs/access.log main;

# 打开内容压缩
gzip on;
# 压缩长度最短限制
gzip_min_length 1;
# 压缩级别
gzip_comp_level 2;
# 指定类型文件的压缩
gzip_types text/plain;

server {
# 配置好监听端口
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

# 所有的请求都转发到这里
location / {
# alias 表示安装是的目录和当前目录对应,可以理解为指定相对目录
alias xxx/;
# 显示目录及文件
autoindex on;
# 将 url 里面的路径带进来,因此一般使用 alias
# root html;
# index index.html index.htm;
# 限制响应速度,限制每秒传输的字节数,防止一些大文件将带宽占满,导致无法获取小文件
set $limit_rate 1k;
}

变量解释:https://nginx.org/en/docs/

带有缓存的反向代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
http {
include mime.types;
default_type application/octet-stream;

# 配置缓存存放地方,以及使用共享内存大小
proxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

# 配置文件包含目录中的配置文件
include vhost/*.conf;
# 将 local 表示为上游服务器,也就是本机 8081
upstream local {
server 8081;
}

server {
# 域名
server_name xxx.pub;
# 监听端口
listen 80;

# 处理所有请求
location / {
# 将域名也一起给到上游
proxy_set_header Host $host;
# 通过反向代理的时候,将对端地址的 IP 传到上游服务器,替代自己的 IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forward_for;

# 使用缓存,并且指定上面开辟的共享内存
proxy_cache my_cache;

# 设置缓存中的 key,通常相同的网站内容可以缓存起来
proxy_cache_key $host$uri$is_args&args;
# 缓存条件
proxy_cache_valid 200 304 302 1d;

# 将所有请求都代理到 local
proxy_pass http://local;
}
}
}

使用 Go access 实时分析 accesslog 日志

通过 websocket 监听 accesslog 的实施变更,以图形化的方式分析 accesslog

https://goaccess.io/

img

1
goaccess access.log -o report.html --log-format=COMBINED

TLS/SSL 发展

站点通过 HTTPS 保障安全。

image-20240319212751796

TLS 安全密码套件解读

image-20240319212917574

  • 密钥交换算法:解决浏览器和服务器之间各自独立生成密钥,并且密钥相同
  • 身份验证:浏览器和服务器之间交换密钥后验证身份
  • 对称加密算法、强度、分组模式
  • 签名 hash 算法

对称加密

image-20240319213124644

通常说:加密和解密,使用同一份密钥,其他人就算有密钥,但是没有加密算法也无法破译。

例如使用 RC4 的加密算法

image-20240319213231497

使用或运算,可以加密和解密,特点是性能好。

非对称加密

image-20240319213415910

根据授权原理,生成一对密钥,分为私钥和公钥。使用公钥加密,则使用私钥解密,使用私钥加密,则使用公钥解密。

PKI 公钥基础设施

image-20240319213515766

站点在 CA 申请证书,CA 生成一对公钥和私钥,颁发给网站站点。

浏览器请求时,站点会将证书发送给浏览器,浏览器会验证证书是否合法。

证书类型

  • 域名验证(domain validated,DV)证书,验证证书归属是否正确

    https://www.xiaoyeshiyu.com

  • 组织证书(organization validated,OV)证书,验证机构企业名称是否正确

    https://www.baidu.com

  • 扩展验证(extended validation,EV)证书,严格,但是浏览器显式支持好

    image-20240319213954908

证书链

image-20240319214052396

image-20240319214154415

TLS 通讯过程

主要分为四个部分

  1. 验证身份
  2. 达成安全套件共识
  3. 传递密钥
  4. 加密通讯

image-20240319214323670

TLS主要做两件事:交换密钥,加密通讯。

OpenResty 安装

https://openresty.org/cn/installation.html

官网全中文,国人开源,非常友好。

扩展性更好,可以使用 lua 直接处理请求中的内容。

Nginx 架构基础

Nginx 运行在企业内网的最外层,处理的流量非常庞大,这也是 Nginx 的架构非常重要的原因。

Nginx 请求处理流程

image-20240320102954837

  • 外部流量大约可以分为三种:WEBEMAILTCP
  • Nginx 中处理这三种流量是通过非阻塞模型处理,epoll,处理请求通过不同的状态机实现
  • 当请求需要处理静态资源,则通过磁盘缓存处理,如果是反向代理的缓存内容也是从磁盘缓存中读取
  • 当缓存中数据不存在,则会进入 IO 阻塞调用,为了提高性能,还需要使用线程池处理磁盘阻塞调用
  • 请求完成后,会记录日志,也可以通过 syslog 协议记录到远程机器上
  • Nginx 作为负载均衡或者反响代理,将请求以协议级传递给上游服务器
  • 也可以通过应用层的协议,例如 FastCGI 等代理到相应的应用服务器

Nginx 进程结构

Nginx 进程结构分为:

  • 单进程结构:不适合于生产环境,适合开发、调试
  • 多进程结构

image-20240320103904332

多进程结构分为:

  • 一个父进程,Master 进程
  • 多个子进程,分为两类:
    • Worker 进程
    • Cache 相关的进程

Nginx 采用多进程的原因是为了高可靠性。如果是多线程模型,共用一个地址空间,当其他进程出现越界时,会影响 Nginx 进程。

第三方开发时,通常也不会在 Master 进程中加入模块,即使 Nginx 允许。

Master 进程用于做管理、监控其他 Worker 进程是否正常工作,监听热部署、重加载配置等信号,其他的 Worker 进程用于管理。

Cache 需要让缓存在多个进程间共享,不仅需要让 Master 进程使用,还需要在 Cache manager、Cache loader 进程使用。

Cache manager、Cache loader 则负责使用反向代理时的缓存,Cache manager 负责管理缓存,Cache loader 负责加载缓存。

Nginx 的进程间通讯,使用共享内存解决。

为了实现高性能,Nginx 的 worker 数量通常与 CPU 个数一致,并且当 CPU 核和 worker 进程绑定后,还可以减少缓存失效,提高缓存效率。

Nginx 进程管理:信号

Nginx 父子进程之间,通过信号进行管理。(数据共享还是通过共享内存)

  • Master 进程

    • 监控 Worker 进程

      • CHLD:当 Worker 进程出现异常,会向 Master 进程发送信号,Master 检测到信号之后,会立刻将 Worker 进程拉起
    • 管理 Worker 进程

    • 接收信号

      • TERM,INT:立刻停止 Nginx 进程

      • QUIT:优雅的停止 Nginx 进程,不会向用户发送 RESET 这样的 TCP 请求

      • HUP:重载配置文件

      • USR1:重新打开日志文件,做日志文件切割

        上面四个可以通过 nginx 命令行发送,下面两个只能使用 kill 发送信号

      • USR2:热部署使用,生成一个新的 Nginx Master 进程,也会新启动一些 Worker 进程,老的 Worker 进程的请求会过渡到新的 Worker 中

      • WINCH:热部署使用,让老 nginx Master 进程优雅关闭老的 Worker 进程

  • Worker 进程:接受来自 Master 的信号(通常不会直接向 Worker 进程发送信号,而是使用 Master 进程管理,虽然直接发送也可以)

    • TERM,INT
    • QUIT
    • USR1
    • WINCH
  • nginx 命令行:Nginx 使用命令行启动时,会将 pid 记录到文件中,命令行会通过文件读取 pid,发送相应的命令。

    • reload:HUP
    • reopen:USR1
    • stop:TERM

使用 nginx 命令行,跟向 master 进程发送信号的作用一样。

reload 流程

Nginx 可以不停止当前进程,启动新进程,避免当前请求中断。

  1. 向 Master 进程发送 HUP 信号(reload 命令)
  2. Master 进程校验配置语法是否正确(也可以提前使用 -t 先检查)
  3. Master 进程打开新的监听端口(如果引入了新的端口,则会新打开端口,让子进程继承)
  4. Master 进程用新配置启动新的 Worker 子进程(Nginx 保证平滑,因此会先启新进程,然后再向老子进程发送信号)
  5. Master 进程向老 Worker 子进程发送 QUIT 信号(先结束请求,优雅关闭子进程,即使是一些 keep-alive 请求)
  6. 老 Worker 进程关闭监听句柄,处理完当前连接后结束进程(新的连接到新的子进程)

有些时候,reload 之后,子进程数量不会减少,这种场景出现在 upstream 中比较多,例如一些连接没有被客户端处理,或者 websocket

不停机载入新配置:

image-20240320110646429

当出现老 Worker 进程无法停止,Nginx 提供 worker_shutdown_timeout 参数来及时强制结束子进程。

热升级流程

  1. 将旧 Nginx 二进制可执行文件替换成新 Nginx 二进制可执行文件(注意备份)
  2. 向 Master 进程发送 USR2 信号(其实是相当于老 Master 进程以子进程的方式启动新的 Master 进程)
  3. Master 进程修改 pid 文件名,加后缀 .oldbin(给新的 Master 进程让路)
  4. Master 进程用新 Nginx 文件启动新 Master 进程
  5. 向老 Master 进程发送 WINCH 信号,关闭老 Master
  6. 回滚:向老 Master 发送 HUP 信号,向新 Master 发送 QUIT 信号

不停机更新 Nginx 二进制文件:

image-20240320112013511

Worker 进程:优雅的关闭

使用 QUIT 信号,主要针对 HTTP 请求

  1. 设置定时器 worker_shutdown_timeout(针对 websocket 这类无法检测是否完成的请求)
  2. 关闭监听句柄
  3. 关闭空闲连接(为了保证高效,有些连接不会关闭。类似连接池原理)
  4. 在循环中等待全部连接关闭(发现请求完毕,则将该请求的连接关闭)
  5. 退出进程

网络收发与 Nginx 事件间的对应关系

网络数据包被解封装出内部的数据载荷后,进入操作系统处理

image-20240320113234640

操作系统处理事件会定义一个事件收集、分发器,通过定义的一系列消费者来处理。

例如 Nginx 会建立很多对应的消费者处理事件,例如 连接建立事件消费者、读事件消费者、写事件消费者 等等。。。

读事件:

  • Accept 建立连接(TCP 建立连接、关闭连接都是接受报文读取里面的消息)
  • Read 读消息

写事件:

  • Write 写消息

Nginx 事件循环

image-20240320113551100

  • Nginx 等待连接:此时处于 epoll_wait
  • 当三次握手完成,操作系统 Kernel 通知 Nginx 开始处理请求
  • Worker 从 Kernel 中获取事件,操作系统将处理好的事件放在队列中:比如建立连接、TCP 报文

image-20240320113603464

  • 处理事件是一个循环,通过判断事件队列是否为空
  • 在处理事件过程中,可能还会添加一个事件,例如超时时间(浏览器在超时时间内没有发送心跳,则将这个连接关闭)

epoll

网络事件收集器模型对比:

image-20240320144533754

使用 Libevent 测试,当连接数增加,Select 和 Poll 所需要的时间增加都很多。

这是由于 Poll 和 Select 在有大并发连接的时候,Select 一次所能处理的句柄数有限,而 Poll 无法直接处理就绪的句柄,而需要便利所有的句柄,拿到活跃的连接。而 Epoll 则可以直接处理活跃的连接。

image-20240320144912013

  • 前提
    • 高并发连接中,每次处理的活跃连接数量占比很小
  • 实现
    • 红黑树:任何的操作放到、取出红黑树中,操作是 log(n)
    • 链表:当网卡接受到报文,链表新增元素,从内核态读取到用户态,只读取一个节点,效率高
  • 使用
    • 创建
    • 操作:添加/修改/删除
    • 获取句柄
    • 关闭

请求切换

image-20240320151452328

相比传统服务的处理模型,一个进程同时只能处理一种请求,一个请求可能会在不同的阶段交由不同的进程处理,导致频繁的进程间切换。是一个线程仅处理一个连接。不做连接切换(而是进程切换),依赖 OS 的进程调度实现并发。

Nginx 的 Worker 处理模型,是一线程同时处理多连接。当网络事件不满足时,则切换到另外一个事件处理。用户态代码完成连接切换;也尽量减少 OS 进程切换。

阻塞调用

操作系统在执行进程时,如果进程是阻塞调用,则当无法执行下去的时候,例如 IO 阻塞,CPU 会切换执行其他线程,将该线程置于睡眠状态。而非组赛则是当线程的 CPU 时间片没有使用完之前,CPU 是不会切换进程执行其他进程的。

而异步或者非异步,则是从编码角度和业务处理角度来说的。

以 accept 为例

image-20240320161639495

已经完成三次握手,建立连接后,阻塞 accept 下,调用 accept 时,会直接读取套接字,而不会阻塞。但是,如果 accept 队列为空,则操作系统会等待新的三次握手连接到达内核中,然后再唤醒 accept 调用。虽然超时时间可以设置,但是阻塞过程中,也就是等待 IO 完成,会让操作系统进行进程间切换。

image-20240320161947779

因此 Nginx 不会使用这种调用模型。

非阻塞调用

image-20240320162007008

如果使用非阻塞 accept 调用套接字,当 ACCEPT 队列为空,不会等待,而是立刻返回一个 EAGAIN 错误码。此时会再次调用 accept,如果 ACCEPT 队列不为空,则将套接字从 ACCEPT 队列中移出并拷贝给进程。

那么,当接受到 EAGAIN 错误码的时候,是直接再次调用 accept 还是处理其他的请求,会根据代码逻辑来实现。

Nginx 会通过异步调用其他的业务逻辑。

非阻塞调用下的同步与异步

例如反响代理的实例:

1
2
3
4
rc = ngx_http_read_request_body(r, ngx_http_upstream_init) ; 
if ( rc >= NGX_HTTP_SPECIAL_RESPONSE) {
return rc ;
}

Nginx 反向代理考虑到上游服务的处理能力通常不足,所以如果携带 body 请求,Nginx 会先接收完 body,再去向上游服务器发起连接。

ngx_http_read_request_body 方法执行完时调用 post_handler 异步方法

1
2
ngx_int_t
ngx_http_read_client_request_body (ngx_http_request_t * r,ngx_http_client_body_handler_pt post_handler)

最终读取完 body 后调用 ngx_http_upstream_init 方法

1
2
3
void
ngx_http_upstream_init (ngx_http_request_t * r )
{

整个过程比较复杂难懂。

相比而言,Openresty 提供同步调用代码

1
2
3
4
5
6
7
local client = redis:new() 
client:set_timeout(30000)
local ok,err = client:connect(ip,port) // 同步调用,里面使用非阻塞方法
if not ok then
ngx.say("failed: ",err)
return
end

Nginx 模块

Nginx 模块设计非常精良。在编译期可以将模块编译进去,而有些模块还需要在使用时打开才能使用该模块。

官方模块文档:https://nginx.org/en/docs/

nginx 模块的伪代码:

image-20240320163223907

可以看到,模块之间高内聚,相同的功能在一个模块中。

模块内部高度抽象,都提供通用方法:

  • 配置
  • 启停回调方法
  • 子模块抽象
    • http
    • event
    • mail
    • stream

模块分类

Nginx 模块划分为不同的子模块

image-20240320164758250

  1. NGX_CORE_MODULE:Nginx 核心模块

    一下几个,本身会定义出子类型模块

    1. events: Nginx 事件通用处理方法,内聚成 events 方法
      1. NGX_EVENT_MODULE:每一个模块通过下划线后缀表示具体 event 信息,相同的核心模块排在最前面(类似于继承和封装的思想)
    2. http
      1. NGX_HTTP_MODULE
    3. stream
    4. mail
  2. NGX_CONF_MODULE:一个独立模块,负责解析 Nginx 的 conf 文件

连接池

image-20240320165947952

在配置文件中有参数 worker_connections,代表创建对应长度的数组。https://nginx.org/en/docs/ngx_core_module.html#worker_connections

1
2
3
4
Syntax:    worker_connections number;
Default:
worker_connections 512;
Context: events

It should be kept in mind that this number includes all connections (e.g. connections with proxied servers, among others), not only connections with clients.

数组中每一位,都代表某一个事件,不同的事件维护不同的结构体。例如 connectionsread_eventswrite_events

并且由于读和写是不同的事件连接,因此,当分配较大的 worker_connections 时,占用内存也很多。

核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
struct ngx_event_s {
void *data;

unsigned write:1;

unsigned accept:1;

/* used to detect the stale events in kqueue and epoll */
unsigned instance:1;

/*
* the event was passed or would be passed to a kernel;
* in aio mode - operation was posted.
*/
unsigned active:1;

unsigned disabled:1;

/* the ready event; in aio mode 0 means that no operation can be posted */
unsigned ready:1;

unsigned oneshot:1;

/* aio operation is complete */
unsigned complete:1;

unsigned eof:1;
unsigned error:1;

unsigned timedout:1;
unsigned timer_set:1;

unsigned delayed:1;

unsigned deferred_accept:1;

/* the pending eof reported by kqueue, epoll or in aio chain operation */
unsigned pending_eof:1;

unsigned posted:1;

unsigned closed:1;

/* to test on worker exit */
unsigned channel:1;
unsigned resolver:1;

unsigned cancelable:1;

#if (NGX_HAVE_KQUEUE)
unsigned kq_vnode:1;

/* the pending errno reported by kqueue */
int kq_errno;
#endif

/*
* kqueue only:
* accept: number of sockets that wait to be accepted
* read: bytes to read when event is ready
* or lowat when event is set with NGX_LOWAT_EVENT flag
* write: available space in buffer when event is ready
* or lowat when event is set with NGX_LOWAT_EVENT flag
*
* iocp: TODO
*
* otherwise:
* accept: 1 if accept many, 0 otherwise
* read: bytes to read when event is ready, -1 if not known
*/

int available;

ngx_event_handler_pt handler;


#if (NGX_HAVE_IOCP)
ngx_event_ovlp_t ovlp;
#endif

ngx_uint_t index;

ngx_log_t *log;

ngx_rbtree_node_t timer;

/* the posted queue */
ngx_queue_t queue;

#if 0

/* the threads support */

/*
* the event thread context, we store it here
* if $(CC) does not understand __thread declaration
* and pthread_getspecific() is too costly
*/

void *thr_ctx;

#if (NGX_EVENT_T_PADDING)

/* event should not cross cache line in SMP */

uint32_t padding[NGX_EVENT_T_PADDING];
#endif
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct ngx_connection_s {
void *data;
ngx_event_t *read; // 读事件
ngx_event_t *write; // 写事件

ngx_socket_t fd;

ngx_recv_pt recv; // 抽象解耦 OS 底层方法
ngx_send_pt send; // 抽象解耦 OS 底层方法
ngx_recv_chain_pt recv_chain;
ngx_send_chain_pt send_chain;

ngx_listening_t *listening;

off_t sent; // bytes_sent 变量

ngx_log_t *log;

ngx_pool_t *pool; // 初始 connections_pool_size 配置

int type;

struct sockaddr *sockaddr;
socklen_t socklen;
ngx_str_t addr_text;

ngx_proxy_protocol_t *proxy_protocol;

#if (NGX_QUIC || NGX_COMPAT)
ngx_quic_stream_t *quic;
#endif

#if (NGX_SSL || NGX_COMPAT)
ngx_ssl_connection_t *ssl;
#endif

ngx_udp_connection_t *udp;

struct sockaddr *local_sockaddr;
socklen_t local_socklen;

ngx_buf_t *buffer;

ngx_queue_t queue;

ngx_atomic_uint_t number;

ngx_msec_t start_time;
ngx_uint_t requests;

...
};

image-20240320170609268

内存池

image-20240320171327210

虽然 Nginx 使用 C语言写,自带内存池管理,不需要关注内存的申请和释放,但是也提供修改内存池大小,以供在特殊情况下处理请求和连接所需要的内存分配。

ngx_connection_s 中,ngx_pool_t 字段指向内存池,使用内存池的优势是减少内存碎片的大小。

当使用小块内存时,使用 next 指针连接在一起。

当分配大块内存时,还是会经过操作系统的 alloc 来进行分配。

内存池分为两种:

  • 连接内存池:当处理 keep-alive 请求时,一个连接会重复使用处理多个请求,当请求结束时,连接才关闭。
  • 请求内存池:对于 HTTP 1.1 请求而言,一般会分配 4k 大小的内存,用于存放 uriheader

对于第三方开发而言,实现内存池后,无需管理内存,而只需要知道是从连接内存池获取内存还是请求内存池获取内存。

通过设置内存池大小,可以提前预分配,减少后续分配的次数。

https://nginx.org/en/docs/http/ngx_http_core_module.html#connection_pool_size

1
2
3
4
Syntax:    connection_pool_size size;
Default:
connection_pool_size 256|512;
Context: http, server

https://nginx.org/en/docs/http/ngx_http_core_module.html#request_pool_size

1
2
3
4
Syntax:    request_pool_size size;
Default:
request_pool_size 4k;
Context: http, server

connection_pool_size 默认 512 字节(64位),这是由于连接需要保存的上下文信息较少。

request_pool_size 默认 4k,是由于对于连接而言,请求需要保存 uriheader 之类的。

Nginx 进程间的通讯方式

也就是所有 Worker 进程协同工作的关键。

Nginx 进程间通讯的方式有两种:

  • 信号:Master 进程管理 Worker 进程
  • 共享内存:开辟一块内存空间,所有的 Worker 共享使用,可以读取写入

为了使用好一块内存空间,则会引发两个问题:

  1. 锁:解决数据竞态(使用自旋锁替代信号量,避免进程陷入阻塞和进程切换,再切换回来导致上下文切换)
  2. Slab 内存管理器:降低进程使用内存的复杂性

共享内存:跨 Worker 进程通讯

共享内存使用者:

  • 官方模块

    • ngx_http_lua_api

    • rbtree

      image-20240320174200080

    • 单链表

      image-20240320174217530

OpenResty 共享内存代码示例

image-20240320174247606

Slab 内存管理

虽然可以通过共享内存,将数据存储在红黑树中,但是,具体实现小块内存分配还需要一些方法。一般来说,为了高效管理内存,还需要将内存划分为不同大小的小块,以便更好的利用内存,

image-20240320200034481

大片内存按照 slot 分配,slot 按照两倍增加,8byte、16byte等。

  • Bestfit 分配方式
    • 最多两倍内存分配
  • Bestfit 优势
    • 适合小对象
    • 避免碎片
    • 避免重复初始化

ngx_slab_stat 统计 Slab 使用状态

image-20240320200255105

https://tengine.taobao.org/document_cn/ngx_slab_stat_cn.html

Nginx 容器

Nginx 容器是很多高级功能的基础。

  • 数组:多块连续内存
  • 链表
  • 队列
  • 哈希表
  • 红黑树
  • 基数树

Nginx 哈希表

image-20240320200732364

将用户结构体放在一块连续的内存中。

哈希表配置

image-20240320200917592

一般应用于静态不变的内容。不会删除和移动。

在开始使用的时候,就可以确定 Bucket size 和 Max size。

使用场景:例如 Nginx 中的一些变量,反向代理的数据等。

由于 CPU 往 L1、L2、L3 Cache 中读取数据按照机器字为单位,所以 Bucket size 会有 CPU 内存对齐。

红黑树

红黑树是一个查找二叉树。同时,高度差不会太大。

image-20240320201246522

自平衡二叉查找树:

  • 高度不会超过 2 倍数 log(n)
  • 增删改查算法复杂度 O(log(n))
  • 便利复杂度 O(n)

红黑树的使用模块

image-20240320201429641

红黑树一般会用于:

  1. 管理定时器计时器
  2. 管理共享内存的红黑树

动态模块-减少编译环节

在不使用动态模块时,编译 Nginx 的过程:

image-20240320201625382

将 Nginx 官方模块源码和第三方模块源码放在一起编译。

使用动态模块:load_module modules/ngx_http_image_filter_module.so;

image-20240320201736030

除了生成可执行二进制文件,还会生成一个共享的动态库。

静态库:将所有源代码编译到可执行文件中

动态库:在二进制可执行文件中保留库文件位置,在需要用到动态库功能时,需要 Nginx 可执行文件调用动态库文件

好处是:如果编译了大量第三方文件,可以仅仅编译第三方动态库文件进行替换,Nginx reload 就行,而不需要重新编译和启动 Nginx。

  1. Configure 加入动态模块(不是所有的模块可以以动态模块的方式加入);
  2. 编译进 binary(调用 make 命令)
  3. 启动时初始化模块数组
  4. 读取 load_module 配置
  5. 打开动态库并加入模块数组
  6. 基于模块数组开始初始化

详解 HTTP 模块

配置文件中核心的配置模块。

配置块的嵌套

例如下面的配置块,存在嵌套关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 在大括号外面的是 main,一般放整个系统的配置,例如 日志、worker 数
main

# 处理请求模块
http {

upstream { ... }
split_clients {...}
map {...}
geo {...}


server {
if () {...}

location {
limit_except {...}
}
location {
location {
}
}
}

server {
}

}

指令的 Context

例如下面管理 log 的模块指令

1
2
3
4
5
6
7
8
Syntax: log_format name [escape=default|json|none] string ...;
Default: log_format combined "...";
Context: http

Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;
Default: access_log logs/access.log combined;
Context: http, server, location, if in location, limit_except

Context 代表指令能出现的块。例如 log_format 只能出现在 http 块下,access_log 则可以出现在 http, server, location, if in location, limit_except 中。出现在其他地方就会报错。

指令的合并

指令出现在多个块中,可以合并。

  • 值指令:存储配置项的值。可以合并,例如:

    • root
    • access_log
    • gzip
  • 动作类指令:指定行为。不可以合并,例如:

    • rewrite
    • proxy_pass

    生效阶段

    • server_rewrite 阶段
    • rewrite 节点
    • content 阶段

存储值的指令继承规则:向上覆盖

子配置不存在时,直接使用父配置块。

子配置存在时,直接覆盖父配置块。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server { 
# listen 只能出现在 server 中
listen 8080;
# 定义了 root
root /home/geek/nginx/html;
access_log logs/geek.access.log main;
location /test {
# 又重新定义了,则使用子配置
root /home/geek/nginx/test;
access_log logs/access.test.log main;
}

location /dlib {
alias dlib/;
}
location / {
# 这里没有指定 root,就可以使用父配置块
}
}

HTTP 模块合并配置的实现

当第三方模块没有详细文档说明模块配置是否可以合并时,可以通过下面的方法判断:

  • 指令在哪个块下生效

  • 指令允许出现在哪些块下

  • server 块内生效,从 httpserver 合并指令

    1
    char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
  • 配置缓存在内存

    1
    char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);

常用指令

Listen 指令

一般用于指定监听端口或者地址

https://nginx.org/en/docs/http/ngx_http_core_module.html#listen

1
2
3
4
5
6
Syntax:    listen address[:port] [default_server] [ssl] [http2 | quic] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen port [default_server] [ssl] [http2 | quic] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
listen unix:path [default_server] [ssl] [http2 | quic] [proxy_protocol] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
Default:
listen *:80 | *:8000;
Context: server
1
2
3
4
5
6
7
8
9
10
11
12
// 地址和端口
listen 127.0.0.1:8000;
listen 127.0.0.1;
listen 8000;
listen *:8000;
listen localhost:8000;

// ipv6
listen [::]:8000;
listen [::1];
# 监听 unix sock,相比网络调用,本级上效率更高
listen unix:/var/run/nginx.sock;

接收请求事件模块

image-20240320204332742

三次握手成功后,建立连接后,Worker 会根据自身负载均衡算法,选择一个 Worker 处理请求。

Worker 进程通过 Epoll_wait 方法获取连接好的句柄,通过这个读事件,调用 accept 方法,给这个连接分配 512 字节(初始大小)大小的连接内存池。

HTTP 模块从事件模块接手请求处理过程,此时会通过 ngx_http_init_connection 设置回调方法(ngx_http_wait_request_handler 方法,是为后续处理数据时执行),加入到 读事件中,并且添加超时定时器(client_header_timeout)。完后 Nginx 切换到其他的 fd 事件处理。

浏览器开发发送请求,通过监听 epoll_wait 事件,执行前面的回调函数,将内核中的 data 读取到用户态的内存中,从连接内存池中分配 1k(client_header_buffer_size)的内存大小。

当数据超过 1k,则需要在处理请求时解决。

接收请求 HTTP 模块

image-20240320204357215

  1. 先分配请求内存池,默认 4k( client_header_buffer_size 的8倍 ),不能过多,否则性能会下降
  2. 在状态机中,解析请求行(uri、协议等)
  3. 如果超过 1k 的内存大小,则分配更大的内存。(一般用于解决 uri 太长)large_client_header_buffers:4 8k 的意思是先分配 8k,然后将前面 1k 的内容拷贝过来,再用剩下的 7k 接受请求。如果 uri 很长,还没有接受完,则分配第二个 8k,继续在状态机中解析请求行。最多分配 32k
  4. 解析完 uri 之后,例如解析到 \r\n,就标识 uri,标识的目的是使用指针指向 uri(性能高)
  5. 开始接收 headerheader 可能很长,例如有 cookiehost 等字段
  6. 状态机中解析 header
  7. 开始分配大内存,分配的内容会复用 large_client_header_buffers 的配置,跟 uri 公用这个内容大小
  8. 标识 header,代表收到所有的 header
  9. 移除超时定时器,也就是连接建立后,还没有开始处理请求前设置的 client_header_timeout
  10. 开始核心的过程,11个阶段的 http 请求处理

正则表达式

在写域名或者 location 时,可以通过正则表达式提供更强大的用法。

元字符

常用的元字符:

  • .:匹配除换行符意外的任意字符
  • \w:匹配字母数字或下划线或汉字
  • \s:匹配任意的空白符
  • \d:匹配数字
  • \b:匹配单词的开始或结束
  • ^:匹配字符串的开始
  • &:匹配字符串的结束

重复

  • *:重复零次或更多次
  • +:重复一次或更多次
  • ?:重复零次或一次
  • {n}:重复 n 次
  • {n,}:重复 n 次或更多次
  • {n,m}:重复 n 到 m 次

其他

  • \:转义符号,取消元字符的特殊含义
  • ():分组与取值
1
2
3
4
5
6
7
示例:
原始url:/admin/website/article/35/change/uploads/party/5.jpg
转换后的url: /static/uploads/party/5.jpg

匹配原始 url 的正则表达式:
\/admin\/website\/article\/(\d+)\/change\/uploads\/(\w+)\/(\w+)\.(png|jpg|gif|jpeg|b mp)$/
rewrite^/admin/website/solution/(\d+)/change/uploads/(.*)\.(png|jpg|gif|jpeg|bmp)$ /static/uploads/$2/$3.$4 last;

避免测试需要重复重启 Nginx,可以使用 pcre 工具进行校验:https://www.pcre.org/original/doc/html/pcretest.html

server 指令块

server_name 指令

  1. 跟多个域名,第 1 个是主域名

    1
    server_name www.xiaoyeshiyu.com

    当有多个域名时,可以通过 server_name_in_redirect on 将返回的 Location 为主域名。

  2. 泛域名:仅支持在最前或者最后

    1
    server_name *.xiaoyeshiyu.com www.xiaoyeshiyu.*
  3. 正则表达式:加 ~ 前缀

    1
    server_name www.xiaoyeshiyu.com ~^www\d+\.xiaoyeshiyu\.com$;
  4. 用正则表达式创建变量:用小括号

    1
    2
    3
    4
    5
    server {
    server_name ~^(www\.)?(.+)$;
    # $2 指的是上面命令产生的两个变量中的第二个,例如如果是 www.xiaoyeshiyu.com,则 $2 是 xiaoyeshiyu.com
    location / { root /sites/$2; }
    }
    1
    2
    3
    4
    5
    server {
    # 命名变量,后面可以使用 $domain 这个变量
    server_name ~^(www\.)?(?<domain>.+)$;
    location / { root /sites/$domain; }
    }
  5. .xiaoyeshiyu.com 可以匹配 xiaoyeshiyu.com*.xiaoyeshiyu.com

  6. _ 匹配所有

  7. "" 匹配没有传递 Host 头部

server 匹配顺序

  1. 精确匹配
  2. * 在前的泛域名
  3. * 在后的泛域名
  4. 按文件中的顺序匹配正则表达式域名
  5. default server
    1. 第一个
    2. listen 指定 default

HTTP 请求处理时的 11 个阶段

Nginx 所有的模块只能在定义的 11 个阶段生效。

image-20240322151212664

  1. 当一个请求进入到 Nginx 中,首先获取请求头,并且决定用哪个 server 块指令处理
  2. 确认哪个配置块生效
  3. 判断是否限流
  4. 验证请求是否是盗链,或者通过 auth 协议验证
  5. 生成用户响应信息
  6. 如果是转发请求到上游,还需要上游返回结果生成用户响应信息
  7. 如果这个过程中产生子请求或者重定向,那么接续前面几个阶段
  8. 返回请求时,会经过过滤模块,例如压缩、图片处理
  9. 返回给用户时,也会记录日志信息

具体流程以及对应的模块:

image-20240322151234928

请求会从上到下,依次一个一个执行。

11 个阶段顺序处理

image-20240322152200841

灰色由 Nginx 框架执行,蓝色由其他框架或者第三方框架执行。

POSTREAD 阶段

在这个阶段可以获取到真实的用户 IP 地址。

image-20240322152735010

  1. TCP 连接四元组(src ip, src port, dst ip, dst port
  2. HTTP 头部 X-Forwarded-For 用于传递 IP,会往后面加 IP
  3. HTTP 头部 X-Real-IP 用于传递用户 IP,只能存放一个 IP
  4. 网络中存在许多反响代理

拿到用户真实 IP 后,可以基于变量来使用,做一些限流的作用。

binary_remote_addrremote_addr 这样的变量,其值就为真实的 IP(X-Real-IP),这样做连接限制(limit_conn 模块)。

realip 模块

用于将客户端地址和可选端口更改为指定标头字段中发送的地址。

默认情况下,realip 模块不会编译进 Nginx,通过 --with-http_realip_module 启用功能。

https://nginx.org/en/docs/http/ngx_http_realip_module.html

realip 模块生效的前提是:直接连接 nginxip是在set_real_ip_from中指定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义已知发送正确替换地址的受信任地址。如果指定了特殊值 unix:,则所有 UNIX 域套接字将被信任。也可以使用主机名来指定受信任地址(1.13.1)。
# 也就是告诉上游服务器,这个 IP 地址是我本机可信任的地址
Syntax: set_real_ip_from address | CIDR | unix:;
Default: —
Context: http, server, location

# 定义请求头字段,其值将用于替换客户端地址。
# 也就是告诉上游服务器,X-Real-IP 里面存放的是真实的客户端地址
Syntax: real_ip_header field | X-Real-IP | X-Forwarded-For | proxy_protocol;
Default:
real_ip_header X-Real-IP;
Context: http, server, location

# 如果禁用递归搜索,则将与受信任地址之一匹配的原始客户端地址替换为由real_ip_header指令定义的请求头字段中发送的最后一个地址。如果启用了递归搜索,则将与受信任地址之一匹配的原始客户端地址替换为请求头字段中发送的最后一个非受信任地址。
# 当real_ip_recursive为off时,nginx会把real_ip_header指定的HTTP头中的最后一个IP当成真实IP

# 当real_ip_recursive为on时,nginx会把real_ip_header指定的HTTP头中的最后一个不是信任服务器的IP当成真实IP
Syntax: real_ip_recursive on | off;
Default:
real_ip_recursive off;
Context: http, server, location
This directive appeared in versions 1.3.0 and 1.2.1.

REWRITE 阶段

会在 SERVER_REWRITE 阶段(对应 server 配置块下)、REWRITE (对应 location 配置块下)、POST_REWRITE阶段生效

https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite

1
2
3
Syntax:    rewrite regex replacement [flag];
Default: —
Context: server, location, if

return 指令

https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return

1
2
3
4
5
Syntax:    return code [text];
return code URL;
return URL;
Default: —
Context: server, location, if
  • 返回状态码
    • Nginx 自定义
      • 例如 444: 关闭连接
    • HTTP 1.0 标准
      • 301: http 1.0 永久重定向,缓存在浏览器中
      • 302: 临时重定向,禁止被缓存
    • HTTP 1.1 标准
      • 303: 临时重定向,允许改变方法,禁止被缓存
      • 307: 临时重定向,不允许改变方法,禁止被缓存
      • 308: 永久重定向,不允许改变方法

error_page 指令

https://nginx.org/en/docs/http/ngx_http_core_module.html#error_page

1
2
3
Syntax:    error_page code ... [=[response]] uri;
Default: —
Context: http, server, location, if in location

return 有关联。用法是收到(这里注意是收到,而不是产生)一个返回码时,可以重定向为另外一个 URL 或者指定返回不一样的内容。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
error_page 404 =200 /empty.gif;
error_page 404 = /404.php;
location / {
error_page 404 = @fallback;
}
location @fallback {
proxy_pass http://backend;
}
error_page 403 http://example.com/forbidden.html;
error_page 404 =301 http://example.com/notfound.html;

当出现 return 的时候,error_page404 是否能执行

server 下和在 location 下出现 return,是否合并和如何生效。

例如

1
2
3
4
5
6
7
8
9
10
11
12
server {
server_name return.xiaoyeshiyu.com;
listen 8080;
root html/;
error_page 404 /403.html;

return 403;

location / {
return 404 "find nothing!";
}
}
  1. 如果有 return 指令,则直接返回对应信息,而不会返回 error_page;没有 return 指令,而是直接返回 404 状态码,则会被 rewriteerror_page
  2. server 模块下的 return 指令,是在 SERVER_REWRITE 阶段执行,先于 location 模块下的 return 指令在 REWRITE 阶段执行

rewrite 指令

https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite

1
2
3
Syntax:    rewrite regex replacement [flag];
Default: —
Context: server, location, if
  • regex 指定的 url 替换成 replacement 这个新的 url
    • 可以使用正则表达式及变量提取
  • replacementhttp:// 或者 https:// 或者 $schema 开头,则直接返回 302 重定向
  • 替换后的 url 根据 flag 指定的方式进行处理
    • --last:意思是持续,用 replacement 这个 URI 进行新的 location 匹配
    • --break:表示停止,停止当前脚本指令的执行,等价于独立的 break 指令,break 后面的指定都不会执行
    • --redirect:返回 302 重定向
    • --permanent:返回 301 重定向

rewrite_log 指令

https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite_log

1
2
3
4
Syntax:    rewrite_log on | off;
Default:
rewrite_log off;
Context: http, server, location, if

rewrite 的信息记录到 error_log

if 指令

可以根据请求中的变量的值,判断是否满足某个条件之后是否执行下面的配置指令。

https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#if

1
2
3
Syntax:    if (condition) { ... }
Default: —
Context: server, location

条件 condition 为真,则执行大括号内的指令;遵循值指令的继承规则。

表达式:

  1. 检查变量为空或者值是否为 0,直接使用
  2. 将变量与字符串做完全匹配,使用 = 或者 !=
  3. 将变量与正则表达式做匹配
    • 大小写敏感,~ 或者 !~
    • 大小写不敏感,~* 或者 !~*
  4. 检查文件是否存在,使用 -f 或者 !-f
  5. 检查目录是否存在,使用 -d 或者 !-d
  6. 检查文件、目录、软链接是否存在,使用 -e 或者 !-e
  7. 检查是否为可执行文件,使用 -x 或者 !-x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ($http_user_agent ~ MSIE) { # 判断是否是 ie 浏览器
rewrite ^(.*)$ /msie/$1 break;
}
if ($http_cookie ~* "id=([^;]+)(?:;|$)") { # 忽略大小写,从 cookie 中取出 id
set $id $1;
}
if ($request_method = POST) { # 对变量做字符串完全匹配
return 405;
}
if ($slow) { # 对变量进行判断是否为空,map中的变量
limit_rate 10k;
}
if ($invalid_referer) { # 对 invalid_referer 模块
return 403;
}

FIND_CONFIG 阶段

FIND_CONFIG 阶段主要是匹配 location,决定用哪个代码块处理。

https://nginx.org/en/docs/http/ngx_http_core_module.html#location

1
2
3
4
Syntax:    location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default: —
Context: server, location

https://nginx.org/en/docs/http/ngx_http_core_module.html#merge_slashes

1
2
3
4
Syntax:    merge_slashes on | off;
Default:
merge_slashes on;
Context: http, server

当两个 / 在一起时,xxx//xxx.html,打开 merge_slashes 则可以合并成一个 /

Location 匹配规则

仅匹配 URI,忽略参数

  • 前缀字符串
    • 常规,例如 xxx/xxx.html
    • =:精确匹配
    • ^~:匹配上后则不再进行正则表达式匹配
  • 正则表达式
    • ~:大小写敏感的正则匹配
    • ~*:忽略大小写的正则匹配
  • 合并连续的/符号
    • merge_slashes on
  • 用于内部跳转的命名 location
    • @

匹配顺序:

image-20240322165151774

Nginx 中会使用 二叉树 放置全部的 location

匹配时,有三种情况:

  1. 匹配 = 字符串,则停止匹配
  2. 再进行其他的前缀匹配,匹配上的话,对应最长的匹配
  3. 如果是 ^~ 字符串,匹配上之后,则不会进行正则表达式匹配,如果多个匹配上,则用最长匹配上的 urllocation,此时如果最长 location 没有 ^~,因此后续还会进行正则表达式匹配
  4. 如果上面两个都没有,则先记住最长匹配的前缀字符串 location,并且再按照正则表达式匹配,如果正则表达式没有匹配上,则用这一个
  5. 使用正则表达式匹配是按照顺序的,如果匹配上,就直接使用,而不再继续往下匹配

PREACCESS 阶段

用于限制每个客户端的并发连接数

ngx_http_limit_conn_module 模块,默认编译进 Nginx

https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html

生效范围:

  • 全部 worker 进程(基于共享内存)
  • 进入 preaccess 阶段前不生效
  • 限制的有效性取决于 key 的设计:依赖 postread 阶段的 realip 模块取到真实 ip(例如前面有 SLB 做负载均衡,此时连接数限制会失效,因此可以基于 realip 来做限制连接数)

limit_conn 指令

https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone

1
2
3
Syntax:    limit_conn_zone key zone=name:size;
Default: —
Context: http

定义共享内存(包括大小)以及 key 关键字,一般 key 取值于用户的 IP 地址(remote_addr

https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn

1
2
3
Syntax:    limit_conn zone number;
Default: —
Context: http, server, location

限制并发连接数

https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_log_level

1
2
3
4
5
Syntax:    limit_conn_log_level info | notice | warn | error;
Default:
limit_conn_log_level error;
Context: http, server, location
This directive appeared in version 0.8.18.

限制发生时的日志级别

https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_status

1
2
3
4
5
Syntax:    limit_conn_status code;
Default:
limit_conn_status 503;
Context: http, server, location
This directive appeared in version 1.3.15.

限制发生时向客户端返回的错误码

limit_req 指令

默认编译进 Nginx,用于限制请求数和大小。

生效算法:leaky bucket 算法(漏桶):有一个一定容量的桶,请求来了之后,以固定的速度处理,如果桶满了,则直接返回报错

生效范围:

  • 全部 worker 进程(基于共享内存)
  • 进入 preaccess 阶段前不生效
  • 限制的有效性取决于 key 的设计,依赖 postread 阶段的 realip 模块取到真实 ip(例如前面有 SLB 做负载均衡,此时连接数限制会失效,因此可以基于 realip 来做限制连接数)

https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_zone

1
2
3
Syntax:    limit_req_zone key zone=name:size rate=rate [sync];
Default: —
Context: http

定义共享内存(包括大小),以及 key 关键字和限制速率。

rate 单位为 r/s 或者 r/m,代表每秒/分钟的请求数

https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req

1
2
3
Syntax:    limit_req zone=name [burst=number] [nodelay | delay=number];
Default: —
Context: http, server, location

限制并发连接数。

burst 默认为0,代表盆中容纳多少个请求

nodelay ,对 burst 中的请求不再采用延时处理的做法,而是立刻处理(立刻返回错误),如果设置了,则会等待延迟处理,如果超过了设置的 number,则会立刻返回错误。

https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_log_level

1
2
3
4
5
Syntax:    limit_req_log_level info | notice | warn | error;
Default:
limit_req_log_level error;
Context: http, server, location
This directive appeared in version 0.8.18.

限制发生时的日志级别

https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_status

1
2
3
4
5
Syntax:    limit_req_status code;
Default:
limit_req_status 503;
Context: http, server, location
This directive appeared in version 1.3.15.

limit_reqlimit_conn 配置同时生效,limit_req 会优先生效,这是基于 HTTP 处理流程的。

ACCESS 阶段

控制请求是否可以继续向下访问,按照顺序,有 accessauth_basic auth_request 三个模块

access 模块

默认编译到 Nginx,生效范围:进入 access 阶段前不生效

https://nginx.org/en/docs/http/ngx_http_access_module.html

allow 指令

允许哪些地址访问

https://nginx.org/en/docs/http/ngx_http_access_module.html#allow

1
2
3
Syntax:    allow address | CIDR | unix: | all;
Default: —
Context: http, server, location, limit_except

deny 指令

https://nginx.org/en/docs/http/ngx_http_access_module.html#deny

不允许哪些地址访问

1
2
3
Syntax:    deny address | CIDR | unix: | all;
Default: —
Context: http, server, location, limit_except

例如

1
2
3
4
5
6
7
location / {
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
deny all;
}

auth_basic 模块

对用户名、密码做限制。默认编译进 Nginx

使用 RFC2671 的 HTTP Basic Authentication 协议进行用户名密码的认证。

image-20240322200500405

浏览器访问浏览器时,浏览器返回一个 401 的响应,这个响应不会在客户端显式,浏览器会弹出对话框,输入用户名密码。浏览器通过明文将用户名密码发给服务端,因此可以使用 HTTPS。

https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html

1
2
3
4
5
6
7
8
Syntax:    auth_basic string | off;
Default:
auth_basic off;
Context: http, server, location, limit_except

Syntax: auth_basic_user_file file;
Default: —
Context: http, server, location, limit_except

生成密码文件:

  • 文件格式

    1
    2
    3
    4
    # comment
    name1:password1
    name2:password2:comment
    name3:password3
  • 生成工具:htpasswd

    安装依赖包:httpd-tools ,命令行:htpasswd –c file –b user pass

auth_request 模块

默认未编译进 Nginx。

功能:向上游的服务转发请求,若上游服务返回的响应码时 2xx,则继续执行,若上游服务返回的是 401 或者 403,则将响应返回给客户端。可以理解为向第三方验证,但是验证完成后由自己执行后面的操作。

原理:收到请求后,生成子请求(子请求与请求一致),通过反向代理技术把请求传递给上游服务。

https://nginx.org/en/docs/http/ngx_http_auth_request_module.html

1
2
3
4
5
6
7
8
9
Syntax:    auth_request uri | off;
Default:
auth_request off;
Context: http, server, location

# 方便后续处理,请求完成后,设置一个变量
Syntax: auth_request_set $variable value;
Default: —
Context: http, server, location

satisfy 指令

限制所有 access 阶段模块的 satisfy 指令

https://nginx.org/en/docs/http/ngx_http_core_module.html#satisfy

1
2
3
4
Syntax:    satisfy all | any;
Default:
satisfy all;
Context: http, server, location

access 阶段的模块:

  • access 模块
  • auth_basic 模块
  • auth_request 模块
  • 其他模块

satisfy all 代表所有的 access 模块都满足,请求才可以放行。

satisfy any 代表只要有任意一个 access 模块放行了,那么这个请求就可以放行。

image-20240322202129575

  1. 如果有 return 指令,access 阶段不会生效。这是由于 returnREWRITE 阶段在前面
  2. 多个 access 模块的顺序的影响,会按照执行顺序,如果放行则直接放行,如果拒绝则直接拒绝
1
2
3
4
5
6
location /{
satisfy any;
auth_basic "test auth_basic";
auth_basic_user_file examples/auth.pass;
deny all;
}

例如上面,输入密码,可以访问文件;这是由于 satisfy any 作用于 access 模块之前,输入密码之后即可访问

1
2
3
4
5
6
location /{
satisfy any;
deny all;
auth_basic "test auth_basic";
auth_basic_user_file examples/auth.pass;
}

即使是这样,输入密码也可以

1
2
3
4
5
6
location /{
satisfy any;
allow all;
auth_basic "test auth_basic";
auth_basic_user_file examples/auth.pass;
}

如果改为这样,则不需要输入密码,即可访问。这是由于 access 模块在 auth_basic 之前。

PRECONTENT 阶段

try_files 指令

https://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

默认编译到 Nginx 中,也无法取消。

1
2
3
4
Syntax:    try_files file ... uri;
try_files file ... =code;
Default: —
Context: server, location

依次殊途访问多个 url 对应的文件(由 root 或者 alias 指令指定),当文件存在时直接返回文件内容,如果所有文件都不存在,则按最后一个 URL 结果或者 code 返回。

一般可以用于解决反向代理服务器的缓存,当存在文件则返回,如果不存在文件,则转发到上游服务器中。

mirror 指令

https://nginx.org/en/docs/http/ngx_http_mirror_module.html#mirror

一般用于创造一份镜像流量,例如生产环境中,可以将请求 copy 一份发送到测试环境或者生产环境中做处理。

默认编译进 Nginx,处理请求时,生成子请求访问其他服务,对子请求的返回值不做处理。

1
2
3
4
5
6
7
8
9
Syntax:    mirror uri | off;
Default:
mirror off;
Context: http, server, location

Syntax: mirror_request_body on | off;
Default:
mirror_request_body on;
Context: http, server, location

CONTENT 阶段

static 模块

有两个指令,功能都是将 url 映射为文件路径,以返回静态文件内容。

root 指令

https://nginx.org/en/docs/http/ngx_http_core_module.html#root

1
2
3
4
Syntax:    root path;
Default:
root html;
Context: http, server, location, if in location

差别是:root 就将完整 url 映射进文件路径中;由默认配置,并且上下文很多

alias 指令

https://nginx.org/en/docs/http/ngx_http_core_module.html#alias

1
2
3
Syntax:    alias path;
Default: —
Context: location

差别:alias 只会将 location 后的 URL 映射到文件路径;没有默认配置

三个变量

除了 rootalias 之外,static 模块还提供三个变量。

  • request_filename:待访问文件的完整路径(绝对路径)
  • document_root:由 URI 和 root/alias 规则生成的文件夹路径(目录的绝对路径)
  • realpath_root:将 document_root 中的软链接等换成真实路径(软连接对应目录的绝对目录)

content-type

当读取磁盘上的文件时,会根据文件的扩展名做一次映射,放在 hash 表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 将扩展名和 content-type 做映射,放在 hash 表中
Syntax: types { ... }
Default:
types {
text/html html;
image/gif gif;
image/jpeg jpg;
}
Context: http, server, location

# 在 return 时,没有文件名或者没有扩展名时的默认 content-type
Syntax: default_type mime-type;
Default:
default_type text/plain;
Context: http, server, location

# 存放 content-type 桶的大小和容量
Syntax: types_hash_bucket_size size;
Default:
types_hash_bucket_size 64;
Context: http, server, location

Syntax: types_hash_max_size size;
Default:
types_hash_max_size 1024;
Context: http, server, location

log_not_found

当找不到文件时,打印日志。

1
2
3
4
Syntax:    log_not_found on | off;
Default:
log_not_found on;
Context: http, server, location

例如

1
[error] 10156#0: *10723 open() "/html/first/2.txt/root/2.txt" failed (2: No such file or directory)

重定向

static 模块实现了 root/alias 功能时,发现访问目标是目录,但 URL 末尾未加 / 时,会返回 301 重定向。

此时可以根据不同的配置实现不同的效果:

打开之后,重定向的 Location 内的内容就是主域名中的域名

https://nginx.org/en/docs/http/ngx_http_core_module.html#server_name_in_redirect

1
2
3
4
Syntax:    server_name_in_redirect on | off;
Default:
server_name_in_redirect off;
Context: http, server, location

打开之后,在 Location 中显示出端口

https://nginx.org/en/docs/http/ngx_http_core_module.html#port_in_redirect

1
2
3
4
Syntax:    port_in_redirect on | off;
Default:
port_in_redirect on;
Context: http, server, location

绝对路径打开,会返回一个域名,返回的域名默认是请求的域名,或者当请求的 header 中携带 Host,则是对应 Host 中的值,也可以由 server_name_in_redirect 控制,端口是否显式,由 port_in_redirect 控制。如果不打开,则返回的 Location 中不会携带 http://xxx 等,而是直接返回一个绝对路径

https://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect

1
2
3
4
5
Syntax:    absolute_redirect on | off;
Default:
absolute_redirect on;
Context: http, server, location
This directive appeared in version 1.11.8.

index 模块

指定/访问时返回 index 文件内容(当访问的 url/ 结尾时,会去查找目录下是否有 index.html 文件)

https://nginx.org/en/docs/http/ngx_http_index_module.html#index

1
2
3
4
Syntax:    index file ...;
Default:
index index.html;
Context: http, server, location

randomindex 模块

随机选择 index 指令指定的一系列 index 文件中的一个,作为 / 路径的返回文件内容。默认编译进 Nginx。

https://nginx.org/en/docs/http/ngx_http_random_index_module.html#random_index

1
2
3
4
Syntax:    random_index on | off;
Default:
random_index off;
Context: location

autoindex 模块

https://nginx.org/en/docs/http/ngx_http_autoindex_module.html#autoindex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 开启或者关闭
Syntax: autoindex on | off;
Default:
autoindex off;
Context: http, server, location

# 当返回 HTML 格式时才有效,表示显式文件大小是相对单位,还是以 Byte 单位
Syntax: autoindex_exact_size on | off;
Default:
autoindex_exact_size on;
Context: http, server, location

# 表示返回是以哪种格式返回
Syntax: autoindex_format html | xml | json | jsonp;
Default:
autoindex_format html;
Context: http, server, location
This directive appeared in version 1.7.9.

# 表示返回的时区
Syntax: autoindex_localtime on | off;
Default:
autoindex_localtime off;
Context: http, server, location

index 的执行顺序是先于 autoindex

URL/ 结尾时,尝试以 html/xml/json/jsonp 等格式返回 root/alias 中指向目录的目录结构。默认编译进 Nginx

concat 模块

https://github.com/alibaba/nginx-http-concat

当页面需要访问多个小文件时,把它们的内容合并到一次 http 响应中返回,提升性能

使用:

uri 后面加上 ??,后通过多个 , 号分隔文件。如果还有参数,则在最后通过 ? 添加参数

1
2
http://example.com/??style1.css,style2.css,foo/style3.css
http://example.com/??style1.css,style2.css,foo/style3.css?v=102234

LOG 阶段

记录请求访问日志的 log 模块

将 HTTP 请求相关信息记录到日志。ngx_http_log_module 无法禁用

access_log 模块

https://nginx.org/en/docs/http/ngx_http_log_module.html#access_log

1
2
3
4
5
# 定义格式
Syntax: log_format name [escape=default|json|none] string ...;
Default:
log_format combined "...";
Context: http
1
2
3
4
5
6
# 定义日志文件
Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;
Default:
access_log logs/access.log combined;
Context: http, server, location, if in location, limit_except
  • path 路径可以包含变量(比如 host 根据不同主机写入不同文件):不打开 cache 时每记录一条日志都需要打开、关闭日志文件(这种情况性能很低)
  • if 通过通过变量值控制请求日志是否记录
  • format 不填,默认使用 combined
  • 日志缓存
    • 批量将内存中的日志写入磁盘(也就是使用 cache 批量写入,节省性能)
    • 写入磁盘的条件
      • 所有待写入磁盘的日志大小超出缓存文件
      • 达到 flush 指定的过期时间(过长的话,中途宕机可能导致日志丢失)
      • worker 进程执行 reopen 命令,或者正在关闭
  • 日志压缩
    • 批量压缩内存中的日志,再写入磁盘
    • buffer 大小默认为 64KB
    • 压缩级别默认为1(1最快压缩率最低,9最慢压缩率最高)

对日志文件名包含变量时的优化,使用缓存,避免每次写入日志都要先关闭当前文件,再打开要写入的文件。

1
2
3
4
5
Syntax:    open_log_file_cache max=N [inactive=time] [min_uses=N] [valid=time];
open_log_file_cache off;
Default:
open_log_file_cache off;
Context: http, server, location
  • max:缓存内的最大文件句柄数,超出后用 LRU 算法淘汰
  • inactive:文件访问完后在这段时间内不会被关闭。默认 10 秒
  • min_uses:在 inactive 时间内使用次数超过 min_uses 才会继续存在内存中。默认1
  • valid:超出 valid 时间后,将对缓存的日志文件检查是否存在。默认 60 秒
  • off:关闭缓存功能

过滤模块的位置

在将结果返回给客户端之前,对返回的结果还要进行过滤操作。这个操作的位置在 LOG 之前,CONTENT 阶段之后。

1
2
3
4
5
6
7
8
9
limit_req zone=req_one burst=120;
limit_conn c_zone 1;

satisfy any;
allow 192.168.1.0/32;
auth_basic_user_file access.pass;

gzip on;
image_filter resize 80 80;

image-20240322230956352

在返回请求时,会先发送 header,发送完后,才发送 body,因此,不同的过滤模块的位置不同。

加工响应内容

过滤模块中,不同模块的处理顺序:

image-20240322231403898

  1. HTTP 过滤模块:copy_filter:复制包体内容(零拷贝技术,不经过用户态,直接拷贝给用户。但是如果使用 gzip,则需要拷贝到内存中处理)
  2. HTTP 过滤模块:postpone_filter:处理子请求(例如需要关注一些子请求的结果)
  3. HTTP 过滤模块:
    1. header_filter:构造响应头部(例如增加 Nginx 版本信息到 header 中)
    2. write_filter:发送响应(调用系统 WRITE,发送数据到客户端)

sub 模块

将响应中指定的字符串,替换成新的字符串。默认未编译进 Nginx

https://nginx.org/en/docs/http/ngx_http_sub_module.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 匹配后换成新的字符串,忽略大小写
Syntax: sub_filter string replacement;
Default: —
Context: http, server, location

# 是否还需要在 header 中显示原先的 last_modified
Syntax: sub_filter_last_modified on | off;
Default:
sub_filter_last_modified off;
Context: http, server, location
This directive appeared in version 1.5.1.

# 只修改一次,on 代表扫描所有的 body 替换所有的,off 代表只替换一个
Syntax: sub_filter_once on | off;
Default:
sub_filter_once on;
Context: http, server, location

# 只针对什么类型的响应替换,如果是 * 代表所有的内容
Syntax: sub_filter_types mime-type ...;
Default:
sub_filter_types text/html;
Context: http, server, location

addition 模块

在响应的 body 内容前或者响应的 body 后增加内容,而增加内容的方式时通过新 url 响应完成。默认为编译进 Nginx

https://nginx.org/en/docs/http/ngx_http_addition_module.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 在响应之前添加内容,添加的内容是 uri 这个子请求返回的内容
Syntax: add_before_body uri;
Default: —
Context: http, server, location

# 在响应后添加内容
Syntax: add_after_body uri;
Default: —
Context: http, server, location

# 指定 content-type 这样的类型才响应
Syntax: addition_types mime-type ...;
Default:
addition_types text/html;
Context: http, server, location
This directive appeared in version 0.7.9.

Nginx 变量的运行原理

image-20240323140048843

Nginx 中分为提供变量的模块和使用变量的模块,Preconfiguration 是 HTTP 模块读取配置模块之前添加的新变量。定义的变量是变量名和解析出变量的方法,目的是定义规则,当需要使用这个变量时,输入对应的变量名得到对应的值。

使用变量时,则解析 nginx.conf 时定义变量的使用方法,在处理请求时,通过变量名获取这个请求对应的变量值。

变量的特性:

  • 惰性求值

    只有当前请求处理完之后,才能通过变量名获取对应变量值

    变量值可以时刻变化,其值为使用的那一时刻的值

存放变量的哈希表:

https://nginx.org/en/docs/http/ngx_http_core_module.html#variables_hash_bucket_size

通过参数配置变量大小和容量

1
2
3
4
5
6
7
8
9
Syntax:    variables_hash_bucket_size size;
Default:
variables_hash_bucket_size 64;
Context: http

Syntax: variables_hash_max_size size;
Default:
variables_hash_max_size 1024;
Context: http

HTTP 框架提供的变量

一些系统提供的变量

HTTP 请求相关的变量

  • arg_参数名URL 中某个具体参数的值
  • query_string:与 args 变量完全相同
  • args:全部 URL 参数
  • is_args:如果请求 URL 中有参数则返回 ? 否则返回空
  • content_length:HTTP 请求中标识包体长度的 Content-Length 头部的值
  • content_type:标识请求包体类型的 Content-Type 头部的值
  • uri:请求的 URI(不同于 URL,不包括 ? 后的参数)
  • document_uri:与 uri 完全相同
  • request_uri:请求的 URL(包含 URI 以及完整的参数)
  • sheme:协议名,例如 HTTP 或者 HTTPS
  • request_method:请求方法,例如 GET、POST
  • request_length:所有请求内容的大小,包括请求行、头部、包体等
  • remote_user:由 HTTP Basic Authentication 协议传入的用户名
  • request_body_file:临时存放请求包体的文件
    • 如果包体非常小则不会存文件
    • client_body_in_file_only 强制所有包体存入文件,且可决定是否删除
  • request_body:请求中的包体,这个变量当且仅当使用反向代理,且设定用内存暂存包体时才有效
  • request:原始的 url 请求,含有方法与协议版本,例如 GET /?a=1&b=2 HTTP/1.1
  • host:先从请求行中获取
    • 如果含有 Hostheader,则用其值替换掉请求行中的主机名
    • 如果前两者都取不到,则使用匹配上的 server_name
  • http_头部名字:返回一个具体请求头部的值
    • 特殊的几个,Nginx 会做一些特殊处理
      • http_hosthttp_user_agenthttp_refererhttp_viahttp_x_forwarded_forhttp_cookie
    • 通用:其他的不会做任何处理,直接从 Header 中读取到内容就是变量的值

TCP 连接相关的变量

  • binary_remote_addr:客户端地址的整型格式,对于 IPv4 是 4 字节,对于 IPv6 是 16字节(一般会使用这个作为缓存的 key,效率高,但是写入日志可读性就不好)
  • connection:递增的连接序号
  • connection_requests:当前连接上执行过的请求数,对 keepalive 连接有意义
  • remote_addr:客户端度地址
  • remote_port:客户端端口
  • proxy_protocol_addr:如果使用了 proxy_protocol 协议则返回协议中的地址,否则返回空
  • proxy_protocol_port:如果使用了 proxy_protocol 协议则返回协议中的端口,否则返回空
  • server_addr:服务器端地址
  • server_port:服务器端端口
  • TCP_INFOtcp 内核层参数,包括 $tcpinfo_rtt, $tcpinfo_rttvar, $tcpinfo_snd_cwnd, $tcpinfo_rcv_space
  • server_protocol:服务器端协议,例如 HTTP/1.1

Nginx 处理请求过程中产生的变量

  • request_time:请求处理到现在的耗时,单位为秒,精确到毫秒
  • server_name:匹配上请求的 server_name
  • https:如果开启了 TLS/SSL,则返回 no,否则返回空
  • request_completion:若请求处理完则返回 OK,否则返回空
  • request_id:以 16禁止输出的请求标识 id,该 id 公含有 16 个字节,是随机生成的
  • request_filename:待访问文件的完整路径
  • document_root:由 URI 和 root/alias 规则生成的文件夹路径
  • realpath_root:将 document_root 中的软链接等换成真实路径
  • limit_rate:返回客户端响应时的速度上限,单位为每秒字节数。可以通过 set 指令修改对请求产生效果

发送 HTTP 响应时相关的变量

  • body_bytes_sent:响应中 body 包体的长度
  • bytes_sent:全部 http 响应的长度
  • statushttp 响应中的返回码
  • sent_trailer_名字:把响应结尾内容里值返回
  • sent_http_头部名字:响应中某个具体头部的值
    • 特殊处理
      • sent_http_content_type
      • sent_http_content_length
      • sent_http_location
      • sent_http_last_modified
      • sent_http_connection
      • sent_http_keep_alive
      • sent_http_transfer_encoding
      • sent_http_cache_control
      • sent_http_link
    • 通用

Nginx 系统变量

  • time_local:以本地时间标准输出的当前时间,例如 14/Nov/2022:15:55:37 +0800
  • time_iso8601:使用 ISO 8601 标准输出的当前时间,例如 2022-11-14T15:55:37+08:00
  • nginx_version:Nginx 版本号
  • pid:所属 worker 进程的进程 id
  • pipe:使用了管道则返回 p,否则返回 .
  • hostname:所在服务器的主机名,与 hostname 命令输出一致
  • msee:1970年1月1日到现在的时间,单位为秒,小数点后精确到毫秒

简单有效的防盗链手段 referer 模块

场景:当某网站通过 url 引用了你的页面,当用户在浏览器上点击 url 时,http 请求的头部会通过 referer 头部,将该网站当前页面的 url 带上,告诉服务器本次请求是由这个网页发起的。

目的:拒绝非正常的网站访问我们站点的资源

思路:通过 referer 模块,用 invalid_referer 变量根据配置判断 ``referer` 头部是否合法

https://nginx.org/en/docs/http/ngx_http_referer_module.html

默认编译进 Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Syntax:    valid_referers none | blocked | server_names | string ...;
Default: —
Context: server, location

# server_names 里面可能会有很多值,为了加速访问,会将这些值放在 hash 表中
Syntax: referer_hash_bucket_size size;
Default:
referer_hash_bucket_size 64;
Context: server, location
This directive appeared in version 1.0.5.

Syntax: referer_hash_max_size size;
Default:
referer_hash_max_size 2048;
Context: server, location
This directive appeared in version 1.0.5.

valid_referers 可同时携带多个参数,标识多个 referer 头部都生效。

  • none:允许缺失 referer 头部的请求访问
  • blockedreferer 字段存在于请求头中,允许 referer 头部没有对应的值的请求访问(可能是通过了反向代理或者防火墙导致,但是不是以 http:// 开头或者以 https:// 开头)
  • server_names:若 referer 中站点域名与 server_name 中本机域名某个匹配,则允许该请求访问
  • 标识域名及 URL 的字符串,对域名可在前缀或者后缀中含有 * 通配符:若 referer 头部的值匹配字符串后,则允许访问
  • 正则表达式:若 referer 头部的值匹配正则表达式后,则允许访问

invalid_referers 变量:

  • 允许访问时,变量值为空
  • 不允许访问时,变量值为1

通过验证 URL 中哈希值的方式防盗链。

https://nginx.org/en/docs/http/ngx_http_secure_link_module.html

默认未编译进 Nginx

过程:

  • 由某服务器(也可以是 nginx)生成加密后的安全连接 url,返回给客户端
  • 客户端使用安全 url 访问 nginx,由 nginxsecure_link 变量判断是否验证通过

原理:

  • 哈希算法不可逆
  • 客户端只能拿到执行过哈希算法的 URL
  • 仅生成 URL 的服务器、验证 URL 是否安全的 nginx 这二者,才保存执行哈希算法前的原始字符串
  • 原始字符串通常由以下部分有序组成:
    • 资源位置,例如 HTTP 中指定资源的 URI,防止攻击者拿到一个安全 URL 后可以访问任意资源
    • 用户信息,例如用户 IP 地址,限制其他用户盗用安全 URL
    • 时间戳,使安全 URL 及时过期
    • 密钥,仅服务器端拥有,增加攻击者猜测出原始字符串的难度
1
2
3
4
5
6
7
8
9
10
11
Syntax:    secure_link expression;
Default: —
Context: http, server, location

Syntax: secure_link_md5 expression;
Default: —
Context: http, server, location

Syntax: secure_link_secret word;
Default: —
Context: location

变量:

  • secure_link
    • 值为空字符串:校验不通过
    • 值为0: URL 过期
    • 值为1: 验证通过
  • secure_link_expires:时间戳的值

例如一个变量值及带过期时间的配置:

通过命令行生成安全链接:

原始请求:

1
/test1.txt?md5=md5生成值&expires=时间戳(例如147483647)

生成 md5

1
echo -n '时间戳URI客户端IP 密钥' | openssl md5 -binary | openssl base64 | tr +/ - | tr -d =

Nginx 配置

1
2
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri$remote_addr secret";

仅对 URI 进行哈希的简单办法:

  1. 将请求 URL 分为三个部分:/prefix/hash/link
  2. Hash 生成方式:对 link 密钥做 md5 哈希求值
  3. secure_link_secret secret 配置密钥

例如:

原始请求

1
link

生成的安全请求

1
/prefix/md5/link

生成 md5

1
echo -n 'linksecret' | openssl md5 –hex

Nginx 配置

1
secure_link_secret secret;

map 模块的指令

https://nginx.org/en/docs/http/ngx_http_map_module.html

基于已有变量,使用类似 switch {case: ... default: ...} 的语法,创建新变量,为其他基于变量值实现功能的模块提供更多的可能性。

默认编译进 Nginx。

1
2
3
4
5
6
7
8
9
10
11
12
13
Syntax:    map string $variable { ... }
Default: —
Context: http

Syntax: map_hash_bucket_size size;
Default:
map_hash_bucket_size 32|64|128;
Context: http

Syntax: map_hash_max_size size;
Default:
map_hash_max_size 2048;
Context: http

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 语法的意思是,如果已有变量 $http_host 的值,匹配上下面左边,则生成一个新变量 $name 值对应右边
# 如果没有匹配上任何值,则$name 为 0
# 使用 hostnames 遵循 server_name 的匹配规则一致
map $http_host $name {
hostnames;

default 0;

example.com 1;
*.example.com 1;
example.org 2;
*.example.org 2;
.example.net 3;
wap.* 4;
}

map $http_user_agent $mobile {
default 0;
"~Opera Mini" 1;
}

规则:

  • 已有变量:
    • 字符串
    • 一个或者多个变量
    • 变量与字符串的组合
  • case 规则:匹配过程有优先级,先匹配则返回对应内容
    • 字符串严格匹配
    • 使用 hostnames 指令,可以对域名使用前缀 * 泛域名匹配
    • 使用 hostname 指令,可以idui域名使用后缀 * 泛域名匹配
    • ~~* 正则表达式匹配,后者忽略大小写
  • default 规则
    • 没有匹配到任何规则时,使用 defualt
    • 缺失 defualt 时,返回空字符串给新变量
  • 其他
    • 使用 include 语法提升可读性
    • 使用 volatile 禁止变量值缓存

split_clients 模块

主要用于实现 AB 测试,让百分比的用户体验某一部分新功能,然后看反馈。

https://nginx.org/en/docs/http/ngx_http_split_clients_module.html

默认编译进 Nginx

也是基于已有变量创建新变量,为其他 AB 测试提供更多的可能性。(通过变量的值,以百分比的方式生成新的变量)

  • 对已有变量的值执行 MurmurHash2 算法得到 32 位整型哈希数字,记为 hash
  • 32 位无符号整型的最大数字 2^32-1,记为 max
  • 哈希数字与最大数字相除 hash/max,可以得到百分比 percent
  • 配置指令中指示了各个百分比构成的范围,如 0-1%1%-5% 等,及范围对应的值
  • percent 落在哪个范围里,新变量的值就对应着其后的参数

规则:

  • 已有变量
    • 字符串
    • 一个或者多个变量
    • 变量与字符串的组合
  • case 规则
    • xx.xx%,支持小数点后 2 位,所有项的百分比相加不能超过 100%
    • *,由它匹配剩余的百分比(100% 减去以上所有项相加的百分比)
1
2
3
Syntax:    split_clients string $variable { ... }
Default: —
Context: http

例如

1
2
3
4
5
6
7
8
9
10
http {
split_clients "${remote_addr}AAA" $variant {
0.5% .one; # 0% ~ 0.5%
2.0% .two; # 0.5% ~ 2.0%
* "";
}

server {
location / {
index index${variant}.html;

geo 模块

https://nginx.org/en/docs/http/ngx_http_geo_module.html

可以基于 IP地址创建新变量,默认编译进 Nginx

1
2
3
Syntax:    geo [$address] $variable { ... }
Default: —
Context: http

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
geo $country {
default ZZ;
include conf/geo.conf;
delete 127.0.0.0/16;
# proxy,此时 `remote_addr` 的值为 `X-Forwarded_For` 头部值中最后一个 IP 地址
proxy 192.168.100.0/24;
proxy 2001:0db8::/32;

127.0.0.0/24 US;
127.0.0.1/32 RU;
10.1.0.0/16 RU;
192.168.1.0/24 UK;
}
  • 如果 geo 指令后不输入 $address,那么默认使用 $remote_addr 变量作为 IP 地址
  • { } 内的指令匹配:优先最长匹配
    • 通过 IP 地址及子网掩码的方式,定义 IP 范围,当 IP 地址在范围内时新变量使用其后的参数值
    • default 指定了当以上范围都未匹配上时,新变量的默认值
    • 通过 proxy 指令指定可信地址(参考 realip 模块),此时 remote_addr 的值为 X-Forwarded_For 头部值中最后一个 IP 地址
    • proxy_recursive 允许循环地址搜索
    • include,优化可读性
    • delete 删除指定网络

geoip 模块

基于 MaxMind 数据库从客户端地址获取变量,例如地域和所属相关信息。默认未编译进 Nginx

https://nginx.org/en/docs/http/ngx_http_geoip_module.html

依赖 MaxMind GeoIP 库,编译前需要先下载,然后编译时携带进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 国家信息
Syntax: geoip_country file;
Default: —
Context: http

# 城市信息
Syntax: geoip_city file;
Default: —
Context: http

# 指定可信地址,跟 realip 同样的用法
Syntax: geoip_proxy address | CIDR;
Default: —
Context: http
This directive appeared in versions 1.3.0 and 1.2.1.

变量:

  • geoip_country_code:两个字母的国家代码,比如 CN 或者 US
  • geoip_country_code3:三个字母的国家代码,例如 CHN
  • geoip_country_name:国家名称:例如 China

geoip_city 中的指令很多,例如经度、纬度、洲、国家、洲或者省编码、省、邮编等

keepalive 模块

是 HTTP 协议中的 keepalive,而不是 TCP 协议中的 keepalivekeepalive 本身包括对上游的和对下游的,对上游则是反向代理,自身相当于客户端。以下是对下游客户端而言。

https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_disable

功能:多个 HTTP 请求通过复用 TCP 连接,实现以下功能:

  • 减少握手次数
  • 通过减少并发连接数减少了服务器资源的消耗
  • 降低 TCP 拥塞控制的影响

协议:

  • Connection 头部:取值为 close 或者 keepalive,前者表示请求处理完即关闭连接,后者表示复用连接处理下一条请求。(这个 header 可以是客户端发给 Nginx,也可以是 Nginx 发给客户端)
  • Keep-Alive 头部:其值为 timeout=n,后面的数字 n 单位是秒,告诉客户端连接至少保留 n 秒。(这个请求是 Nginx 发送给客户端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 对某些浏览器关闭 keepalive
Syntax: keepalive_disable none | browser ...;
Default:
keepalive_disable msie6;
Context: http, server, location

# 表示在一个 TCP 连接上最多执行多少个请求
Syntax: keepalive_requests number;
Default:
keepalive_requests 1000;
Context: http, server, location
This directive appeared in version 0.8.0.

# 第一个 timeout 表示客户端经过第一个 HTTP 请求后,经过 timeout 时间没有新请求,则关闭连接
# 第二个 timeout 表示 Nginx 发给客户端的 header Keep-Alive 头部,单位秒
Syntax: keepalive_timeout timeout [header_timeout];
Default:
keepalive_timeout 75s;
Context: http, server, location