李守中

Nginx 笔记

Table of Contents

1. 踩坑

1.1. 缺少模块

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

1.2. HTTP 重定向到 HTTPS 的配置导致浏览器报重定向次数过多

如果用了 Cloudflare 的 DNS 解析服务的话,那出现这个问题的原因可能是 Cloudflare 的 SSL/TLS 配置没有选对。

打开 Cloudflare 域名配置的 SSL/TLS 配置下的 Overview 选项卡后,可以看到当前域名下的 SSL/TLS 加密配置,有以下几个选项:

  • Off 不加密
  • Flexible 只加密浏览器到 Cloudflare 的数据
  • Full 除了加密浏览器到 Cloudflare 的数据之外,Cloudflare 到 Nginx 的数据也被加密
  • Full (strict) 选项隐含了 Full 表示的全程加密之外,证书还必须是可信机构颁发的

浏览器报重定向次数过多的原因是重定向规则有环。如果 Cloudflare 的 SSL/TLS 加密配置用了 Flexible ,那么 Cloudflare 到 Nginx 的数据就不被加密。这意味着,Cloudflare 会使用 HTTP 请求 Nginx 的 80 端口。但是 Nginx 里面的配置又写了 80 端口的 HTTP 被重定向到 HTTPS,这时候 Cloudflare 解析出重定向规则后就会告诉浏览器重新用 HTTPS 请求该地址,而该地址本身就是以 HTTPS 请求发到 Cloudflare 的,就是这样的环:

        https://lishouzhong.com                http://lishouzhong.com
浏览器 --------------------------> Cloudflare -------------------------> lishouzhong.com
  ^----------------------------------------------------------------------------|
                             301/302 Redirect
                     Location: https://lishouzhong.com

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-07-22 Sat 16:51

Generated by: Emacs 28.2 (Org mode 9.5.5)   Contact: [email protected]

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