Nginx 笔记
1 踩坑
1.1 缺少模块
FreeBSD 上 ( 截止到 2021.9.13 FreeBSD 13 ) 没有 ngx_stream_proxy_module 这个模块,所以 proxy protocol 这个特性没法在 FreeBSD 上用。
2 基础
3 个主要应用场景:
- 静态资源服务。
- 反向代理: 缓存和负载均衡。
- API 服务: OpenResty。
4 个主要组成部分:
- 二进制可执行文件。
- nginx.conf 配置文件。
- access.log 访问日志。
- error.log 错误日志。
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 命令执行流程
- 向 master 进程发送 HUP 信号;
- master 进程校验配置文件语法是否正确;
- master 进程打开新的监听端口;
- master 进程用新配置启动新 worker 子进程;
- master 进程向旧 worker 子进程发送 QUIT 信号;
- 旧 worker 子进程关闭监听句柄,处理完当前连接后结束。
2.3 worker 进程关闭流程
- 设置定时器 worker_shutdown_timeout ,这个属性在 conf 文件里设置;
- 关闭监听句柄;
- 关闭空闲连接;
- 在循环中等待全部连接关闭;
- 退出进程。
2.4 热更新流程
- 旧二进制文件备份,新二进制文件代替旧二进制文件。
- 给旧二进制文件对应的 master 进程发送信号
kill -USR2 <old_pid>
:- 这个信号会新让旧 master 进程启动一个新 master 进程。
- 旧 master 进程不会关闭,而且它虽然在继续监听 80 443 等端口,但因为旧进程的 fd 已经从 epoll 中移出,所以实际上它并不处理新连接。
- 旧 master 是新 master 的父进程,所以新 master 能共享打开的监听端口。
- 给旧的 master 进程发送信号
kill -WINCH <old_pid>
:- 通知旧的 master 进程平滑关闭自己的 worker 进程。
- 保留旧版本的 master 是为了方便回滚,也可以发信号 QUIT 或者直接杀掉进程。
- 更新完成。
2.5 版本回滚流程
- 用旧的二进制文件覆盖新的二进制文件。
- 向旧 master 进程发信号
kill -HUP <old_pid>
:- 相当与
nginx -s reload
指令的作用,把旧 nginx 的worker进程拉起来,但这里并不直接使用 reload 的方式执行。 - 它会在没有旧 worker 进程时启动 worker 进程,这些进程属于旧 master 进程的子进程。
- 相当与
- 给新 master 进程发送信号
kill -USR2 <new_pid>
:- 此时,接收用户请求的是旧版本的nginx进程。
- 新版本的nginx进程不再接受用户请求。
- 回滚完成。可以关闭新的 master 进程。
2.6 日志切分
直接备份原本的日志文件后,执行 nginx -s reopen
会使 nginx 重新生成日志文件。
2.7 证书相关
SSL 证书更新后,需要执行 nginx -s reload
让 nginx 重新读取配置文件才能使用新的证书。
3 内置模块
时间配置参数:
ms
millisecondss
secondsm
minutesh
hoursd
daysw
weeksM
months, 30 daysy
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/index
和 example.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_zone 和 limit_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;
定义一个名为httplimit
的 zone ,分给它 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;
定义一个名为tcplimit
的 zone ,分给它 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 出现,各自的计时器启动:
- 情况一: 不断请求这个 cache,inactive 计时器不断刷新,请求总是能在计时器结束之前到来,持续到 1h 时间点。此时 proxy_cache_valid 计时器结束,inactive 计时器未结束,这意味着此时 cache 不可用,但没有被删除。如果在 inactive 计时器结束前又来了一个请求,nginx 检测到有缓存存在,然后发现缓存过期 ( 失效 ),就重新去读取服务器数据,刷新缓存和 proxy_cache_valid 计时器。如果直到 inactive 计时器结束还没有请求进来,那么 inactive 计时结束以后缓存被删除。
- 情况二: 在 cache 出现后,下一次请求在 1m 之后到来 ( 和第一次请求的间隔超过了 1m )。而在 1m 时间点后,inactive 计时器结束,cache 被删除。1m 后到来的请求就没有 cache 可用了,此时 nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。
- inactive 计时 1m,proxy_cache_valid 计时 1m,请求进来,cache 出现,各自的计时器启动:
- 情况一: cache 出现的 1m 内没有请求进入。此时 inactive 计时器结束,cache 被删除。在 1m 之后,有新请求进入,此时 nginx 要重新去服务器拿一次数据。数据拿到后,cache 被重新建立,inactive 和 proxy_cache_valid 的计时器被刷新,各自重新开始计时。
- 情况二: 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 出现,各自的计时器启动:
- 情况一: cache 出现后的 1m 内没有新请求进入。由于 proxy_cache_valid 计时器运行结束导致 cache 过期,此时,cache 不可用,但没有被删除。在 1m 之后,如果有请求进来,nginx 检测到有缓存存在,然后发现缓存过期 ( 失效 ),就重新去服务器拿数据,删除旧 cache 建立新 cache,proxy_cache_valid 计时器重新启动。
- 情况二: 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.so 和 ngx_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 *;