李守中

Nginx 笔记

Table of Contents

1 踩坑

1.1 缺少模块

FreeBSD 上 ( 截止到 2021.9.13 FreeBSD 13 ) 没有 ngx_stream_proxy_module 这个模块,所以 proxy protocol 这个特性没法在 FreeBSD 上用。

2 基础

3 个主要应用场景:

  1. 静态资源服务。
  2. 反向代理: 缓存和负载均衡。
  3. API 服务: OpenResty。

4 个主要组成部分:

  1. 二进制可执行文件。
  2. nginx.conf 配置文件。
  3. access.log 访问日志。
  4. error.log 错误日志。

5 个主要优点:

  1. 高并发,高性能。
  2. 扩展性强。
  3. 可靠性高。
  4. 可以热部署: 在不停止服务的情况下升级或更改配置。
  5. 使用 BSD 许可证。

2.1 进程控制信号

可用命令 nginx -s 发送的信号:

  • TERM, INT: nginx -s stop 立即停止 nginx 进程。
  • QUIT: nginx -s quit 优雅地停止 nginx 进程(不向用户发送 tcp reset 这种报文)。
  • HUP: nginx -s reload 重载配置文件。
  • USR1: nginx -s reopen 重新打开日志文件做日志文件的切割。

只能用 kill 命令发送的信号:

  • USR2
  • WINCH

2.2 reload 命令执行流程

  1. 向 master 进程发送 HUP 信号;
  2. master 进程校验配置文件语法是否正确;
  3. master 进程打开新的监听端口;
  4. master 进程用新配置启动新 worker 子进程;
  5. master 进程向旧 worker 子进程发送 QUIT 信号;
  6. 旧 worker 子进程关闭监听句柄,处理完当前连接后结束。

2.3 worker 进程关闭流程

  1. 设置定时器 worker_shutdown_timeout ,这个属性在 conf 文件里设置;
  2. 关闭监听句柄;
  3. 关闭空闲连接;
  4. 在循环中等待全部连接关闭;
  5. 退出进程。

2.4 热更新流程

  1. 旧二进制文件备份,新二进制文件代替旧二进制文件。
  2. 给旧二进制文件对应的 master 进程发送信号 kill -USR2 <old_pid>:
    1. 这个信号会新让旧 master 进程启动一个新 master 进程。
    2. 旧 master 进程不会关闭,而且它虽然在继续监听 80 443 等端口,但因为旧进程的 fd 已经从 epoll 中移出,所以实际上它并不处理新连接。
    3. 旧 master 是新 master 的父进程,所以新 master 能共享打开的监听端口。
  3. 给旧的 master 进程发送信号 kill -WINCH <old_pid>:
    1. 通知旧的 master 进程平滑关闭自己的 worker 进程。
    2. 保留旧版本的 master 是为了方便回滚,也可以发信号 QUIT 或者直接杀掉进程。
  4. 更新完成。

2.5 版本回滚流程

  1. 用旧的二进制文件覆盖新的二进制文件。
  2. 向旧 master 进程发信号 kill -HUP <old_pid>:
    1. 相当与 nginx -s reload 指令的作用,把旧 nginx 的worker进程拉起来,但这里并不直接使用 reload 的方式执行。
    2. 它会在没有旧 worker 进程时启动 worker 进程,这些进程属于旧 master 进程的子进程。
  3. 给新 master 进程发送信号 kill -USR2 <new_pid>:
    1. 此时,接收用户请求的是旧版本的nginx进程。
    2. 新版本的nginx进程不再接受用户请求。
  4. 回滚完成。可以关闭新的 master 进程。

2.6 日志切分

直接备份原本的日志文件后,执行 nginx -s reopen 会使 nginx 重新生成日志文件。

2.7 证书相关

SSL 证书更新后,需要执行 nginx -s reload 让 nginx 重新读取配置文件才能使用新的证书。

3 内置模块

时间配置参数:

  • ms milliseconds
  • s seconds
  • m minutes
  • h hours
  • d days
  • w weeks
  • M months, 30 days
  • y years, 365 days

3.1 http_core 模块 location 路径匹配

nginx 的 location 块的语法有两种写法:

location [ = | ~ | ~* | ^~ ] <uri> { ... }
location @name { ... }

第一部分参数根据检索顺序进行说明:

Search-Order Modifier Description Match-Type Stops-search-on-match
1st = The URI must match the specified pattern exactly Simple-string Yes
2nd ^~ The URI must begin with the specified pattern Simple-string Yes
3rd (None) The URI must begin with the specified pattern Simple-string No
4th ~ The URI must be a case-sensitive match to the specified Rx Perl-Compatible-Rx Yes (first match)
5th ~* The URI must be a case-insensitive match to the specified Rx Perl-Compatible-Rx Yes (first match)
N/A @ Defines a named location block. Simple-string Yes

location 块的 uri pattern 也支持 Rx Capturing-group ( 正则表达式捕获组 ),且 Rx 内部的 () 默认为捕获组模式,使用 (?:) 可以关闭捕获组模式。比如 (?:a|b) 意为以非捕获组模式对 a|b 进行匹配。

比如 location ~ ^/(?:index|update)$ 可以匹配 example.com/indexexample.com/update

# -------------------------------------------------------------------------------------
#  ()  : Group/Capturing-group, capturing mean match and retain/output/use what matched
#        the patern inside (). the default bracket mode is "capturing group" while (?:)
#        is a non capturing group. example (?:a|b) match a or b in a non capturing mode
# -------------------------------------------------------------------------------------
#  ?:  : Non capturing group
#  ?=  : Positive look ahead
#  ?!  : is for negative look ahead (do not match the following...)
#  ?<= : is for positive look behind
#  ?<! : is for negative look behind
# -------------------------------------------------------------------------------------

正向斜杠 / 在 nginx 中没有特殊含义,比如, location / 可以匹配任以路径。而反斜杠 \ 为转译字符。

# -------------------------------------------------------------------------------------
#   /  : It doesn't actually do anything. In Javascript, Perl and some other languages,
#        it is used as a delimiter character explicitly for regular expressions.
#        Some languages like PHP use it as a delimiter inside a string,
#        with additional options passed at the end, just like Javascript and Perl.
#        Nginx does not use delimiter, / can be escaped with \/ for code portability
#        purpose BUT this is not required for nginx / are handled literally
#        (don't have other meaning than /)
# -------------------------------------------------------------------------------------

nginx 支持 Perl-Compatible-Rx:

# -------------------------------------------------------------------------------------
#   ~   : Enable regex mode for location (in regex ~ mean case-sensitive match)
#   ~*  : case-insensitive match
#   |   : Or
#   ()  : Match group or evaluate the content of ()
#   $   : the expression must be at the end of the evaluated text
#         (no char/text after the match) $ is usually used at the end of a regex
#         location expression.
#   ?   : Check for zero or one occurrence of the previous char ex jpe?g
#   ^~  : The match must be at the beginning of the text, note that nginx will not perform
#         any further regular expression match even if an other match is available
#         (check the table above); ^ indicate that the match must be at the start of
#         the uri text, while ~ indicates a regular expression match mode.
#         example (location ^~ /realestate/.*)
#         Nginx evaluation exactly this as don't check regexp locations if this
#         location is longest prefix match.
#   =   : Exact match, no sub folders (location = /)
#   ^   : Match the beginning of the text (opposite of $). By itself, ^ is a
#         shortcut for all paths (since they all have a beginning).
#   .*  : Match zero, one or more occurrence of any char
#   \   : Escape the next char
#   .   : Any char
#   *   : Match zero, one or more occurrence of the previous char
#   !   : Not (negative look ahead)
#   {}  : Match a specific number of occurrence ex. [0-9]{3} match 342 but not 32
#         {2,4} match length of 2, 3 and 4
#   +   : Match one or more occurrence of the previous char
#   []  : Match any char inside
# ------------------------------------------------------------------------------------

3.2 http_limit 限制访问频率

ngx_http_limit_req_module 限制 HTTP 请求频率,采用漏桶算法。

ngx_http_limit_conn_module 限制 TCP 并发连接数。

两个模块都基于 IP 来限制访问频率。

3.2.1 限制 HTTP 请求频率

ngx_http_limit_req_module 使用 limit_req_zonelimit_req 配合达到频率限制效果。一段时间内的,来自单个 IP 的 HTTP 请求如果超过指定数量,nginx 就返回 503 状态码 ( 通常会改为 429 Too Many Requests )。

ngx_http_limit_req_module 限制某段时间内同一 IP 访问频率:

http{
    ...
    limit_req_zone $binary_remote_addr zone=httplimit:10m rate=20r/s;
    limit_req_status 429;
    ...
    server{
        ...
        limit_req zone=httplimit burst=10 nodelay;
        ...
    }
    ...
}
  • limit_req_zone $binary_remote_addr zone=httplimit:10m rate=20r/s; 定义一个名为 httplimitzone ,分给它 10M 内存来存储 session ( 1M 能存 16000 个左右 ),以 $binary_remote_addr ( IP 地址的二进制表示 ) 为 key,IP 地址当前的并发连接数为 value。限制请求频率为每秒 20 个 ( r/s 可改为 r/m 即以分钟为单位时间的频率 ),rate 的值必须为整数。
  • limit_req_status 429; 频率过限时返回 429 状态码 ( 默认值 503 )。
  • limit_req zone=httplimit burst=10 nodelay;:
    • httplimit 这个 zone 里限定了请求频率 rate 可以处理每 IP 每秒不大于 20 个请求。
    • burst 定义了最大突发请求限制 ( 这里限制最大突发请求为 10 个 ):
      • 假设某 IP 的请求频率为 rateIP ,zone 中定义的请求频率为 rate
      • 如果 rate * 1s + burst < rateIP * 1s ,那么对于超出限制 ( 即 burst 队列之外 ) 的请求,nginx 返回 429 状态。
      • 如果 rate * 1s < rateIP * 1s < rate * 1s + burst ,而且设置 nodely,那么 rate = rate + burst / 1s ,相当于变相地提高了 rate ,提升量为 burst 指定的值。
      • 如果 rate * 1s < rateIP * 1s < rate * 1s + burst ,而且没有设置 nodely,那么超出限制的请求会被暂存到 buster 队列,队列长度为 burst 定义的值。此时,nginx 处理完先来的请求后,再去处理 burst 队列。

注意: 限速 20r/s 的意义是,每 50ms 只处理一个请求。即,假设 1s 内只有两个请求,而这两个请求都在 50ms 内到达,那么第二个到来的请求会被丢弃 ( 或者进入 burst 队列 )。

给一个例子帮助理解。假设 1s 内只有 3 个请求,而这 3 个请求都在 50ms 内到达。那么,第一个到达的请求被正常处理,第二三个到达的请求进入 buster 队列,如果:

  • 设置 nodelay ,第一个请求被处理完成后,不论用时多久,立即处理 buster 队列。
  • 未设置 nodelay ,保持限速。如果处理第一个请求的用时小于 50ms,那么等到第 50ms 过了之后,处理 buster 队列的第一个请求,等到第 100ms 过了之后,再处理 buster 队列中的第二个请求。

3.2.2 限制 TCP 并发连接数

ngx_http_limit_conn_module 限制单个 IP 的 TCP 并发连接数:

http{
    ...
    limit_conn_zone $binary_remote_addr zone=tcplimit:10m;
    limit_conn_status 429;
    ...
    server{
        ...
        limit_conn tcplimit 40;
        limit_rate 500K;
        ...
    }
    ...
}
  • limit_conn_zone $binary_remote_addr zone=tcplimit:10m; 定义一个名为 tcplimitzone ,分给它 10M 内存来存储 session,以 $binary_remote_addr ( IP 地址的二进制表示 ) 为 key,IP 地址当前的并发连接数为 value。
  • limit_conn_status 429; 频率过限时返回 429 状态码 ( 默认值 503 )。
  • limit_conn tcplimit 40; 连接数限制为 40。
  • limit_rate 500K; 单连接带宽限制为 500k。

3.3 http_gzip 压缩数据包大小

gzip 配置的常用参数:

  • gzip on|off: 是否开启 gzip。默认不开
  • gzip_buffers 32 4K| 16 8K: 压缩在内存中的缓冲区数量,每个缓冲区多大。推荐使用 32 4K 配置。
  • gzip_comp_level [1-9]: 级别越高,压缩比率越大,越耗 CPU。随着压缩级别的升高,压缩比有所提高,但到了级别 6 后,很难再提高。
  • gzip_disable: 正则匹配到的 Uri 不进行 gzip 压缩。
  • gzip_min_length 200: 开始压缩的最小字节数。
  • gzip_http_version 1.0|1.1: 识别启用压缩的最低的 http 版本:
    • 默认协议版本小于 http/1.1 不开启gzip压缩。
    • 为了兼容不支持压缩的早期浏览器加了此选项。
  • gzip_proxied: 设置请求者代理服务器如何缓存内容:
    • off: 不压缩所有代理结果数据。
    • any: 压缩所有结果数据。
    • expired: 如果 header 中包含 Expires 头信息,启用压缩。
    • no-cache: 如果 header 中包含 Cache-Control:no-cache 头信息,启用压缩。
    • no-store: 如果 header 中包含 Cache-Control:no-store 头信息,启用压缩。
    • private: 如果 header 中包含 Cache-Control:private 头信息,启用压缩。
    • no_last_modified: 如果 header 中包含 Last_Modified 头信息,启用压缩。
    • no_etag: 如果 header 中包含 ETag 头信息,启用压缩。
    • auth: 如果 header 中包含 Authorization 头信息,启用压缩。
  • gzip_types text/plain application/xml: 针对文件类型压缩,如 txt xml html css 等。
  • gzip_vary on|off: 是否传输 gzip 压缩标志。

给一个常用配置:

# gzip
gzip                on;
gzip_buffers        16 8K;
gzip_comp_level     6;
gzip_min_length     100;
gzip_types          *; # compress all MIME type files
gzip_disable        "MSIE [1-6]\."; # ie6 and earlier version do not suport gzip
gzip_vary           on;

注: 图片或者 mp3 这样的二进制文件的压缩率较小,耗费 CPU 资源较多,所以这类文件也可以不压缩。

3.4 http_proxy 转发与代理

3.4.1 proxy_cache 缓存

3.4.1.1 缓存的不活跃和过期
  • proxy_cache_path 配置缓存路径以及路径对应的配置,这个配置中的 inactive 参数可以设置不活跃缓存被删除的时间。
  • proxy_cache_valid 设置各个状态码所对应的响应缓存的过期时间。

需要注意: inactive 计时器走完,缓存会被删除,而 proxy_cache_valid 计时器走完后,缓存不被删除。

只要有请求出现,inactive 计时器就被刷新,重新开始计时。而不论有没有请求进入, proxy_cache_valid 计时器都不会被刷新,计时不会被打断。而一直没请求出现的话, inactive 和 proxy_cache_valid 的计时器都不会被刷新。

如果这两个配置项同时被启用,则会有如下的情况:

  • inactive 计时 1m,proxy_cache_valid 计时 1h,请求进来,cache 出现,各自的计时器启动:
    1. 情况一: 不断请求这个 cache,inactive 计时器不断刷新,请求总是能在计时器结束之前到来,持续到 1h 时间点。此时 proxy_cache_valid 计时器结束,inactive 计时器未结束,这意味着此时 cache 不可用,但没有被删除。如果在 inactive 计时器结束前又来了一个请求,nginx 检测到有缓存存在,然后发现缓存过期 ( 失效 ),就重新去读取服务器数据,刷新缓存和 proxy_cache_valid 计时器。如果直到 inactive 计时器结束还没有请求进来,那么 inactive 计时结束以后缓存被删除。
    2. 情况二: 在 cache 出现后,下一次请求在 1m 之后到来 ( 和第一次请求的间隔超过了 1m )。而在 1m 时间点后,inactive 计时器结束,cache 被删除。1m 后到来的请求就没有 cache 可用了,此时 nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。
  • inactive 计时 1m,proxy_cache_valid 计时 1m,请求进来,cache 出现,各自的计时器启动:
    1. 情况一: cache 出现的 1m 内没有请求进入。此时 inactive 计时器结束,cache 被删除。在 1m 之后,有新请求进入,此时 nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。
    2. 情况二: cache 出现的 1m 内有新请求进入。此时 inactive 计时器刷新,而 proxy_cache_valid 计时器还在运行。所以 1m 时间点之后,inactive 计时器还在运行,但由于 proxy_cache_valid 计时器运行结束导致 cache 过期,此时 cache 不可用,但没有被删除。所以 1m 时间点之后,如果有新请求到来,nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。如果 1m 时间点之后,直到 inactive 第一次被刷新后的计时器运行结束,一直都没有新请求进入,那么此时由于 inactive 计时器运行结束导致 cache 过期,并且被删除。
  • inactive 计时 1h,proxy_cache_valid 计时 1m,请求进来,cache 出现,各自的计时器启动:
    1. 情况一: cache 出现后的 1m 内没有新请求进入。由于 proxy_cache_valid 计时器运行结束导致 cache 过期,此时,cache 不可用,但没有被删除。在 1m 之后,如果有请求进来,nginx 检测到有缓存存在,然后发现缓存过期 ( 失效 ),就重新去服务器拿数据,删除旧 cache 建立新 cache,proxy_cache_valid 计时器重新启动。
    2. 情况二: cache 出现后的 1h 内没有新请求进入。在 1h 内的 1m 时间点上,由于 proxy_cache_valid 计时器运行结束导致 cache 过期,此时,cache 不可用,但没有被删除。一直没有请求进入,持续到 1h 时间点上,由于 inactive 计时器运行结束导致 cache 过期,并且被删除。在 1h 时间点之后,有新的请求进入,nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。

根据上面的分析可以得出一个结论: 使用 proxy_cache_valid 的目的就在于设置一个强制刷新缓存的频率

在不配置 proxy_cache_valid 的情况下,如果某个缓存被频繁访问,那么就会导致 inactive 计时器不断被刷新,而 inactive 计时器不结束的话,nginx 就不会更新这个被频繁访问的缓存。如果被缓存的数据已经被更新了,由于 inactive 计时器一直没有结束,新数据无法进入缓存,那么更新后的数据无法被任何人访问到。

4 第三方模块

可以静态编译进 Nginx 二进制文件,也可以编译成库文件由 Nginx 挂载。

推荐把第三方模块编译成库文件,这样可以正常从包管理软件更新 Nginx 版本。

4.1 Brotli 压缩

Brotli 是谷歌开源的比 gzip 更高效的压缩算法。

4.1.1 Linux 系统

git clone https://github.com/google/ngx_brotli.git --recursive 从 Github 下载 ngx_brotli 源代码。

nginx -v 查看 Nginx 的版本,去官网下载对应的版本。解压到 ngx_brotli 同级的文件夹。

进入 Nginx 源码文件夹,执行:

./configure --with-compat --add-dynamic-module=../ngx_brotli

按照提示,安装上缺少的依赖。然后执行 make modules 开始编译库文件。

代码跑完后,Nginx 源码文件夹下会出现一个 objs 文件夹,里面有 ngx_http_brotli_static_module.songx_http_brotli_filter_module.so 两个库文件。

此时需要注意: 如果使用 Debian 11 及以上系统,系统默认装了 libbrotli1 这个 shared library ,它包含了 ngx_http_brotli_filter_module 。Debian 10 及以下系统默认没装这个包。

所以 Debian 11 及以上版本系统只需要在 Nginx 配置文件里写明加载 ngx_http_brotli_static_module.so 即可。Debian 10 及以下需要加载两个 .so 库文件。

4.1.2 BSD 系统

如果系统是 FreeBSD,可以在 /usr/ports/www/nginx 使用 Ports 编译。编译后可以在 /usr/ports/www/nginx/work 文件夹中找到编译好的源码文件夹。

在编译好源码之后,把在源码包的 objs 文件夹中找到的以 .so 结尾的库文件复制到 Nginx 的动态库文件夹,再在配置文件里加载模块即可。

给一个常用配置:

# brotli
brotli              on;
brotli_comp_level   6;
brotli_buffers      16 8k;
brotli_min_length   1k;
brotli_types        *;


Last Update: 2023-05-18 Thu 08:34

Contact: [email protected]     Generated by: Emacs 27.1 (Org mode 9.3)

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议