Nginx学习:代理模块(二)缓存与错误处理
在基本的配置学习之后,其实大部分的业务场景就已经够用了,没错,就那一个 proxy_pass 指令,真的就够了。但是,对于许多更复杂的业务场景来说,Nginx 的代理模块还是提供了更多的功能,做为每个想成为架构师的码农来说,这一部分不说多精通,至少也都得有些了解。今天学习的代理模块缓存与错误处理和 FastCGI 模块非常类似,很多内容我们照搬之前的测试方式就可以了。
今天的配置指令大部分都是可以在 http、server、location 下配置的,仅有 proxy_cache_path 是只能在 http 模块下配置的,我们马上就会看到。
Proxy缓存
代理的缓存也是在获得代理的响应之后,对响应的结果进行缓存,也可以进行不同的配置来实现是否需要走缓存,同样地,清理缓存的指令也是商业版的,如果需要相应的功能,需要第三方的插件。
还是先来看相关的配置指令,最后再一起进行简单地测试。
proxy_cache_path
设置缓存的路径和其他参数。
go
proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [min_free=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time];
没有默认值,需要配置在 http 模块下配置。缓存数据存储在文件中。缓存中的文件名是对缓存键应用 MD5 函数的结果。 levels 参数定义缓存的层次级别:从 1 到 3,每个级别接受值 1 或 2。
缓存的响应会首先写入临时文件,然后重命名该文件。从 0.8.9 版本开始,临时文件和缓存可以放在不同的文件系统上。但是,请注意,在这种情况下,文件是跨两个文件系统复制的,而不是廉价的重命名操作。因此,建议对于任何给定位置,缓存和保存临时文件的目录都放在同一个文件系统上。临时文件的目录是根据 use_temp_path 参数 (1.7.10) 设置的。如果省略此参数或将其设置为值 on,则将使用 proxy_temp_path 指令为给定位置设置的目录。如果该值设置为 off,则临时文件将直接放在缓存目录中。
此外,所有活动密钥和有关数据的信息都存储在共享内存区域中,其名称和大小由 keys_zone 参数配置。一兆字节的区域可以存储大约 8000 个密钥。
在 inactive 参数指定的时间内未访问的缓存数据将从缓存中删除,无论其新鲜度如何。默认情况下,非活动设置为 10 分钟。特殊的"缓存管理器"进程监控由 max_size 参数设置的最大缓存大小,以及由 min_free (1.19.1) 参数设置的带缓存文件系统上的最小可用空间量。当超出大小或没有足够的可用空间时,它会删除最近最少使用的数据。数据在 manager_files、manager_threshold 和 manager_sleep 参数 (1.11.5) 配置的迭代中被删除。在一次迭代中,最多删除 manager_files 个项目(默认为 100)。一次迭代的持续时间受 manager_threshold 参数的限制(默认为 200 毫秒)。在迭代之间,由 manager_sleep 参数配置的暂停(默认为 50 毫秒)。
启动后一分钟,特殊的"缓存加载器"进程被激活。它将有关存储在文件系统上的先前缓存数据的信息加载到缓存区域中。加载也是在迭代中完成的。在一次迭代中,最多加载 loader_files 个项目(默认情况下,100 个)。此外,一次迭代的持续时间受 loader_threshold 参数的限制(默认为 200 毫秒)。在迭代之间,由 loader_sleep 参数配置的暂停(默认为 50 毫秒)。
proxy_cache
定义用于缓存的共享内存区域。
go
proxy_cache zone | off;
默认值是 off ,其实就是开启代理缓存功能的开关。同一个区域可以在多个地方使用,参数值可以包含变量 (1.7.9)。 off 参数禁用从先前配置级别继承的缓存。
proxy_cache_background_update
允许启动后台子请求以更新过期的缓存项,同时将过时的缓存响应返回给客户端。
go
proxy_cache_background_update on | off;
默认值是 off ,请注意,有必要在更新时允许使用陈旧的缓存响应。
proxy_cache_bypass
定义不从缓存中获取响应的条件。
go
proxy_cache_bypass string ...;
没有默认值,如果字符串参数中至少有一个值不为空且不等于"0",则不会从缓存中获取响应:
go
proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
proxy_cache_bypass $http_pragma $http_authorization;
上面的意思就是 cookie 中有 nocache 字段 ,或者 Get 请求参数中有 nocache 字段和 comment 字段,并且这些字段都不为空;或者请求头有 pragma 或 authorization 字段,那么这个请求就不会走缓存。它可以与 proxy_no_cache 指令一起使用。
proxy_cache_convert_head
启用或禁用将"HEAD"方法转换为"GET"以进行缓存。
go
proxy_cache_convert_head on | off;
默认值是 on ,禁用转换时,应将缓存键配置为包含 $request_method
。
proxy_cache_key
定义一个用于缓存的键。
go
proxy_cache_key string;
默认值是 $scheme$proxy_host$request_uri
,其实就是我们缓存这条请求时,定义的那个 key ,就像我们在做数据库缓存时,会通过 where 或完整的 SQL 语句进行 md5 来当缓存 key 一样,例如可以这么设置:
go
proxy_cache_key "$host$request_uri $cookie_user";
默认情况下,它更接近于下面这个字符串。
go
proxy_cache_key $scheme$proxy_host$uri$is_args$args;
另外,如果有特殊的需要,比如说要缓存 Post 之类的修改型的请求,则应该把 $request_method
也加上。不过对于缓存设计来说,这类请求通常不会去缓存。
proxy_cache_lock
启用后,一次只允许一个请求通过将请求传递给代理服务器来填充根据 proxy_cache_key 指令标识的新缓存元素。
go
proxy_cache_lock on | off;
默认 off ,相同缓存元素的其他请求要么等待响应出现在缓存中,要么等待释放该元素的缓存锁,直到 proxy_cache_lock_timeout 指令设置的时间。
就是在缓存失效时,如果有多个请求过来,那么只能有一个请求可以去进行缓存操作,解决缓存击穿的问题。关于缓存击穿的问题,如果大家不记得了,可以去 Redis 系列的文章中查看哦。Redis进阶:缓存穿透、击穿与雪崩 https://mp.weixin.qq.com/s/298VajkPwGRlTQGxRtPI8g
proxy_cache_lock_age
如果传递给代理服务器以填充新缓存元素的最后一个请求在指定时间内没有完成,则可以将另一个请求传递给代理服务器。
go
proxy_cache_lock_age time;
默认 5s 。同样是缓存过期时,如果一个请求在更新时超时了,那么其它请求就直接传递到代理服务器。
proxy_cache_lock_timeout
为 proxy_cache_lock 设置超时。
go
proxy_cache_lock_timeout time;
默认值是 5s ,当时间到期时,请求将被传递到代理服务器,但是,响应不会被缓存。在 1.7.8 之前,可以缓存响应。
proxy_cache_max_range_offset
为字节范围请求设置字节偏移量。
go
proxy_cache_max_range_offset number;
没有默认值,如果范围超出偏移量,范围请求将被传递到代理服务器,并且响应不会被缓存。没有测,也不知道咋测,有了解的小伙伴记得留言哦。
proxy_cache_methods
如果此指令中列出了客户端请求方法,则响应将被缓存。
go
proxy_cache_methods GET | HEAD | POST ...;
"GET"和"HEAD"方法总是添加到列表中,但建议明确指定它们。另请参见 proxy_no_cache 指令。
比如说默认情况下,POST 请求是不会被缓存的,如果想要缓存 POST 或者 PUT、DELETE 之类的请求,就需要在这里配置。同时要注意,上面的 proxy_cache_key 配置最好也要加上 $request_method
,否则如果 GET 和 POST 的 URL 一致的话,可能就会出问题。
proxy_cache_min_uses
设置将缓存响应的请求数。
go
proxy_cache_min_uses number;
默认值是 1 ,就是只要有一条请求来了,就缓存,一般不用动它。
proxy_cache_purge
定义将请求视为缓存清除请求的条件。
go
proxy_cache_purge string ...;
没有默认值,如果字符串参数的至少一个值不为空且不等于"0",则删除具有相应缓存键的缓存条目。返回 204(No Content)响应表示操作成功的结果。
如果清除请求的缓存键以星号("*")结尾,则所有与通配符键匹配的缓存条目都将从缓存中删除。但是,这些条目将保留在磁盘上,直到它们因不活动而被删除,或者被缓存清除器 (1.7.12) 处理,或者客户端尝试访问它们。
商业版的才提供,不多说了,大家可以使用开源的第三方库。
proxy_cache_revalidate
用带有"If-Modified-Since"和"If-None-Match"标头字段的条件请求启用过期缓存项的重新验证。
go
proxy_cache_revalidate on | off;
默认值是 off ,通过请求头中的 HTTP 缓存相关字段来做为缓存的更新依据,需要我们 PHP 代码中添加响应头及处理。
proxy_cache_use_stale
确定在哪些情况下可以在与代理服务器通信期间使用过时的缓存响应。
go
proxy_cache_use_stale error | timeout | invalid_header | updating | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | http_429 | off ...;
该指令的参数与 proxy_next_upstream 指令的参数相匹配。
如果无法选择代理服务器来处理请求,则错误参数还允许使用过时的缓存响应。此外,如果当前正在更新,更新参数允许使用陈旧的缓存响应。这允许在更新缓存数据时最小化对代理服务器的访问次数。在响应过时 (1.11.10) 后的指定秒数内,也可以直接在响应标头中启用使用过时的缓存响应。这比使用指令参数的优先级低。
-
如果当前正在更新,则"Cache-Control"标头字段的"stale-while-revalidate"扩展允许使用过时的缓存响应。
-
"Cache-Control"标头字段的"stale-if-error"扩展允许在发生错误时使用过时的缓存响应。
为了在填充新缓存元素时最小化对代理服务器的访问次数,可以使用 proxy_cache_lock 指令。
和 FastCGI 相关的配置功能也是类似的,当使用服务器组做负载均衡时,如果某一个后端服务器出现问题了,比如报 500 错误了,那么在这里加上 http_500 之后,就会将请求转移到下一个后端服务器上。
proxy_cache_valid
为不同的响应代码设置缓存时间。
go
proxy_cache_valid [code ...] time;
为代码为 200 和 302 的响应设置 10 分钟的缓存时间,为代码为 404 的响应设置 1 分钟的缓存时间。
如果只指定缓存时间 ,则只有 200, 301, 和 302 的响应会被缓存,此外,可以指定 any 参数来缓存任何响应
缓存的参数也可以直接在响应头中设置。这比使用指令设置缓存时间具有更高的优先级。
-
"X-Accel-Expires"标头字段设置响应的缓存时间(以秒为单位)。零值禁用响应缓存。如果该值以 @ 前缀开头,则它设置自 Epoch 以来的绝对时间(以秒为单位),直到可以缓存响应。
-
如果头部不包含"X-Accel-Expires"字段,可以在头部字段"Expires"或"Cache-Control"中设置缓存参数。
-
如果标头包含"Set-Cookie"字段,则不会缓存此类响应。
-
如果标头包含具有特殊值"*"的"Vary"字段,则不会缓存此类响应(1.7.7)。如果标头包含具有另一个值的"Vary"字段,则将考虑相应的请求标头字段(1.7.7)缓存此类响应。
可以使用 proxy_ignore_headers 指令禁用对这些响应头字段中的一个或多个的处理。
proxy_no_cache
定义不将响应保存到缓存的条件。
go
proxy_no_cache string ...;
如果字符串参数中至少有一个值不为空且不等于"0",则不会保存响应。可以与 proxy_cache_bypass 指令一起使用。
Proxy 缓存测试
好了,上面的配置指令都看完了,那么咱们就来挑一些进行简单地测试。为了测试方便,咱们直接使用 PHP 文件来进行测试,因为可以方便地返回随机数。
go
vim /usr/local/nginx/html/fastcgi1/proxy/1.cache.php
<?php
if($_GET['code']){
http_response_code((int)$_GET['code']);
}
echo rand();
代码很简单吧,就是打印了一个随机数。另外我们还根据不同的 GET 参数 code ,返回不同的响应状态码,比如我们要返回 500 状态码,就直接加上一个 code=500
这样的 GET 参数就好了。
接下来就简单配置几个缓存参数吧。
go
// http 下
proxy_cache_path proxycache levels=1:2 keys_zone=cacheone:10m;
// server 下
location ~ /cache/(.*) {
proxy_pass http://192.168.56.88:80/$1?$args;
proxy_cache cacheone;
proxy_cache_bypass $arg_nocache;
proxy_cache_valid 200 3s;
proxy_cache_valid 201 10s;
}
注意 proxy_cache_path 是要在 http 下配置的哦,不能在 server 或 location 下面配置。
然后我们就简单配置了 proxy_cache、proxy_cache_bypass 和 proxy_cache_valid 这三个指令。其中 proxy_cache_bypass 指定如果有 GET 参数 nocache ,就不走缓存;proxy_cache_valid 则分别指定 200 状态码时缓存 3s ,201 状态码时缓存 10s 。
go
curl -v 'http://192.168.56.88:8027/cache/fastcgi1/proxy/1.cache.php'
直接使用 CURL 进行测试,加上 -v
参数可以看到请求头和响应头的信息,不加也可以,然后看返回的随机数结果。
go
* Rebuilt URL to: GET/
* Could not resolve host: GET
* Closing connection 0
curl: (6) Could not resolve host: GET
* Trying 192.168.56.88...
* TCP_NODELAY set
* Connected to 192.168.56.88 (192.168.56.88) port 8027 (#1)
> GET /cache/fastcgi1/proxy/1.cache.php HTTP/1.1
> Host: 192.168.56.88:8027
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.23.0
< Date: Wed, 14 Sep 2022 01:45:11 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/7.2.24
<
* Connection #1 to host 192.168.56.88 left intact
235692549
默认情况下是 200 状态码,因此在三秒内多次发送,返回的随机数会是一样的。三秒后随机数才会更新,我们也可以加上 nocache 参数,不走缓存,这样每次都会走后端的代理请求。
go
curl 'http://192.168.56.88:8027/cache/fastcgi1/proxy/1.cache.php?nocache=1'
最后,大家还可以加上 code 参数,测试一下 201 是不是会缓存 10s 。
go
curl 'http://192.168.56.88:8027/cache/fastcgi1/proxy/1.cache.php?code=201'
其它的测试大家可以自己试下哦,篇幅有限,这里就不一一测试了。用好缓存可以极大地提高我们的运行效率,缓存让请求不需要通过代理转发直接就在第一层 Nginx 就进行处理了。
关于缓存文件的查看,大家直接在 Nginx 程序运行目录下的 proxycache 目录下就可以看到了,内容和 FastCGI 生成的缓存文件内容是一样的。
Proxy错误处理
还是熟悉的配方和熟悉的味道,这里的错误处理最主要的就是对于服务器组来说,当某一个后端服务出现问题时,代理模块将如何处理。另外就是如果只有一个后端代理,那么错误是由 Nginx 来处理还是让后端来进行处理。这里和 FastCGI 部分也没什么区别,咱们边看指令,边简单演示一下就好。
proxy_next_upstream
指定在哪些情况下应将请求传递给下一个服务器。
go
proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | http_429 | non_idempotent | off ...;
Default:
默认值 error timeout ,参数的意义是:
-
error 与服务器建立连接、向其传递请求或读取响应标头时发生错误
-
timeout 在与服务器建立连接、向其传递请求或读取响应标头时发生超时
-
invalid_header 服务器返回空响应或无效响应
-
http_500、http_502、http_503、http_504、http_403、http_404、http_429 代理服务器返回对应的状态码时
-
non_idempotent 通常,如果请求已发送到上游服务器(1.9.13),则使用非幂等方法(POST、LOCK、PATCH)的请求不会传递到下一个服务器,显式启用此选项允许重试此类请求
-
off 禁止将请求传递到下一个服务器
应该记住,只有在尚未向客户端发送任何内容的情况下,才有可能将请求传递给下一个服务器。也就是说,如果在传输响应的过程中发生错误或超时,则无法解决此问题。
该指令还定义了与服务器通信的不成功尝试。错误、超时和 invalid_header 的情况总是被认为是不成功的尝试,即使它们没有在指令中指定。 http_500、http_502、http_503、http_504 和 http_429 的情况仅在指令中指定时才被视为不成功尝试。 http_403 和 http_404 的情况永远不会被认为是不成功的尝试。
将请求传递到下一个服务器可能会受到尝试次数和时间的限制,也就是后面两个配置的内容,咱们先来测试这个配置指令的效果。
首先我们在 89 这台服务器上写一个 PHP 文件,直接抛出 500 异常。
go
// 192.168.56.89 /usr/local/nginx/html/1.php
<?php
throw new Exception('出错了');
然后添加 proxy_next_upstream ,指定 http_500 的处理。
go
proxy_next_upstream error timeout http_500;
服务器组还是使用之前的 proxy1 那个服务器组,不断地刷新,页面会一直显示 88 服务器上的内容。注释掉 proxy_next_upstream 或者去掉 http_500 这个配置,那么错误页面就会显示出来。
proxy_next_upstream_timeout
限制可以将请求传递到下一个服务器的时间。
go
proxy_next_upstream_timeout time;
默认是 0 值,表示关闭此限制,也就是无限制。
proxy_next_upstream_tries
限制将请求传递到下一个服务器的可能尝试次数。
go
proxy_next_upstream_tries number;
默认是 0 值,表示关闭此限制,也就是无限制。
proxy_intercept_errors
确定代码大于或等于 300 的代理响应是否应传递给客户端或被拦截并重定向到 nginx 以使用 error_page 指令进行处理。
go
proxy_intercept_errors on | off;
默认 off ,当后端代理报错时,直接显示的是后端服务的错误信息,通过这个配置,可以拦截并显示当前的代理服务器的错误信息页面。
go
location ~ /errors/(.*) {
proxy_pass http://proxy1/$1?$args;
proxy_intercept_errors on;
error_page 500 /50x.html;
# proxy_next_upstream error timeout http_500;
}
这时访问的结果就会是当前 Nginx 代理指定的 50x.html 页面了。
go
curl 'http://192.168.56.88:8027/errors/1.php'
如果打开下面的 proxy_next_upstream 的注释,它们一起运行会是什么结果呢?以 proxy_next_upstream 为准,如果所有的服务器组都有问题呢?最后会到 proxy_intercept_errors ,大家可以自己试试哦。
总结
内容看着比较多,但其实这些配置指令我们并不陌生,毕竟之前有过 FastCGI 的学习了,还是比较好理解的。主要还是需要大家一起动手测试一下,看看效果是不是和我们想像中的一样。话又说回来,代理模块还是有些特有的配置,我们下篇文章就会看到一个,一步一个脚印,继续加油吧。
参考文档: