Nginx 失败重试机制
src
网易游戏 SRE,喜欢钻研与分享。
背景
Nginx 作为目前应用较广的反向代理服务,原生提供了一套失败重试机制,来保证服务的可用性。本文主要是通过一些简单例子来剖析 Nginx 失败重试机制,让读者能对该机制有一个基础的了解,避免在使用过程中踩坑。
本文中的结论在以下环境进行验证:
版本详情:nginx/1.16.0
安装方式:使用 apt 从 nginx 官方源安装
如何定义 fails
在了解 Nginx 的失败重试机制之前,需要先了解 Nginx 如何定义失败。
Nginx 通过
proxy_next_upstream
参数来定义什么情况下会被认为是 fails,从而触发失败重试机制。
fails 可以分成两类:
- 默认错误 ,包括 error、timeout
- 选择定义错误 ,包含 invalid_header 以及各种异常 http 状态码错误等
默认错误
关于 默认错误 ,我们再详细解析一下这两种错误。关于这两种错误的定义,官网文档已经描述的非常清楚了:
error : an error occurred while establishing a connection with the server, passing a request to it, or reading the response header
timeout : a timeout has occurred while establishing a connection with the server, passing a request to it, or reading the response header
出现
error
的场景,常见的是上游服务器的服务重启、停止,或者异常崩溃导致的无法提供正常服务。而
timeout
的情况,就是代理请求过程中达到对应的超时配置,主要包括了:
-
proxy_connect_timeout
,建立三次握手的时间 -
proxy_read_timeout
,建立连接后,等待上游服务器响应以及处理请求的时间 -
proxy_send_timeout
,数据回传的间隔时间(注意不是数据发送耗时)
选择定义错误
关于
选择定义错误
,异常状态码部分(也就是 4xx、5xx 错误)应该是比较好理解,这里主要说一下
invalid_header
invalid_header : a server returned an empty or invalid response;
这个场景就是上游服务器返回空响应或者非法响应头,包括且不仅限于:
- 上游服务器的业务使用了非标准的 HTTP 协议,nginx 校验不通过
- 因服务异常导致响应请求处理返回了异常 header(或者空 header)
NOTE
默认只有 error、timeout 会被认为是 fails,统计到健康检测的 max_fails 计数,如果通过proxy_next_upstream
定义了其他类型的 fails,那这部分 fails 也会被算到计数器。
在选择自定义错误的配置上,一定要十分慎重,必须要结合业务实际情况来调整配置,而不是直接复制网上或者其他站点的配置,否则可能踩坑:
- 配置了不合理的错误类型,可能导致一些非预期的所有节点被踢掉的情况
- 缺少对关键错误类型的定义,导致出问题的节点一直没有被踢掉,影响客户端访问
重试机制解析
Nginx 的失败重试,就是为了实现对客户端透明的服务器高可用。然而这部分失败重试机制比较复杂且官方文档没详细介绍,本文将对其解析,并配合实际场景例子使之更容易被理解。
基础失败重试
这部分介绍最常见、最基础的失败重试场景。
为了方便理解,使用了以下配置进行分析(
proxy_next_upstream
没有特殊配置):
upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
模拟后端异常的方式是直接将对应服务关闭,造成 connect refused 的情况,对应
error
错误。
在最初始阶段,所有服务器都正常,请求会按照轮询方式依次转发给 AB 两个 Server 处理。假设这时 A 节点服务崩溃,端口不通,则会出现这种情况:
- 请求 1 转到 A 异常,再重试到 B 正常处理,A fails +1
- 请求 2 转到 B 正常处理
- 请求 3 转到 A 异常,再重试到 B 正常处理,A fails +1 达到 max_fails 将被屏蔽 60s
- 屏蔽 A 的期间请求都只转给 B 处理,直到屏蔽到期后将 A 恢复重新加入存活列表,再按照这个逻辑执行
如果在 A 的屏蔽期还没结束时,B 节点的服务也崩溃,端口不通,则会出现:
- 请求 1 转到 B 异常,此时所有线上节点异常,会出现:
- AB 节点一次性恢复,都重新加入存活列表
- 请求转到 A 处理异常,再转到 B 处理异常
- 触发 no live upstreams 报错,返回 502 错误
- 所有节点再次一次性恢复,加入存活列表
-
请求 2 依次经过 AB 均无法正常处理, 触发
no live upstreams
报错,返回 502 错误
重试限制方式
默认配置是没有做重试机制进行限制的,也就是会尽可能去重试直至失败。
Nginx 提供了以下两个参数来控制重试次数以及重试超时时间:
-
proxy_next_upstream_tries
:设置重试次数,默认0
表示无限制,该参数包含所有请求 upstream server 的次数,包括第一次后之后所有重试之和; -
proxy_next_upstream_timeout
:设置重试最大超时时间,默认0
表示不限制,该参数指的是第一次连接时间加上后续重试连接时间,不包含连接上节点之后的处理时间
为了方便理解,使用以下配置进行说明(只列出关键配置):
proxy_connect_timeout 3s;
proxy_next_upstream_timeout 6s;
proxy_next_upstream_tries 3;
upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
server 127.0.0.1:8003 fail_timeout=60s max_fails=2; # Server C
第 2~3 行表示在 6 秒内允许重试 3 次,只要超过其中任意一个设置,Nginx 会结束重试并返回客户端响应(可能是错误码)。我们通过 iptables DROP 掉对 8001、8002 端口的请求来模拟 connect timeout 的情况:
iptables -I INPUT -p tcp -m tcp --dport 8001 -j DROP
iptables -I INPUT -p tcp -m tcp --dport 8002 -j DROP
则具体的请求处理情况如下:
- 请求 1 到达 Nginx,按照以下逻辑处理
- 先转到 A 处理,3s 后连接超时,A fails +1
- 重试到 B 处理,3s 后连接超时,B fails +1
- 到达设置的 6s 重试超时,直接返回 `504 Gateway Time-out` 到客户端,不会重试到 C
- 请求 2 转到 C 正常处理
- 请求 3 到达 Nginx
- 先转到 B 处理,3s 后连接超时,B 达到 max_fails 将被屏蔽 60s
- 转到 C 正常处理
- 请求 4 达到 Nginx:
- 先转到 A 处理,3s 后连接超时,A 达到 max_fails 将被屏蔽 60s
- 转到 C 正常处理
- 后续的请求将全部转到 C 处理直到 AB 屏蔽到期后重新加入服务器存活列表
从上面的例子,可以看出
proxy_next_upstream_timeout
配置项对重试机制的限制,重试次数的情况也是类似,这里就不展开细讲了。
关于 backup 服务器
Nginx 支持设置备用节点,当所有线上节点都异常时启用备用节点,同时备用节点也会影响到失败重试的逻辑,因此单独列出来介绍。
upstream 的配置中,可以通过
backup
指令来定义备用服务器,其含义如下:
- 正常情况下,请求不会转到到 backup 服务器,包括失败重试的场景
- 当所有正常节点全部不可用时,backup 服务器生效,开始处理请求
- 一旦有正常节点恢复,就使用已经恢复的正常节点
- backup 服务器生效期间, 不会存在所有正常节点一次性恢复的逻辑
- 如果全部 backup 服务器也异常,则会将所有节点一次性恢复,加入存活列表
- 如果全部节点(包括 backup)都异常了,则 Nginx 返回 502 错误
为了方便理解,使用了以下配置进行说明:
upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
server 127.0.0.1:8003 backup; # Server C
在最初始阶段,所有服务器都正常,请求会按照轮询方式依次转发给 AB 两个节点处理。当只有 A 异常的情况下,与上文没有 backup 服务器场景处理方式一致,这里就不重复介绍了。
假设在 A 的屏蔽期还没结束时,B 节点的服务也崩溃,端口不通,则会出现:
- 请求 1 转到 B 处理,异常,此时所有线上节点异常,会出现:
- AB 节点一次性恢复,都重新加入存活列表
- 请求转到 A 处理异常,再重试到 B 处理异常,两者 fails 都 +1
- 因 AB 都异常,启用 backup 节点正常处理,并且 AB 节点一次性恢复,加入存活列表
- 请求 2 再依次经过 A、B 节点异常,转到 backup 处理,两者 fails 都达到 max_fails:
- AB 节点都将会被屏蔽 60s,并且不会一次性恢复
- backup 节点正式生效,接下来所有请求直接转到 backup 处理
- 直到 AB 节点的屏蔽到期后,重新加入存活列表
假设 AB 的屏蔽期都还没结束时,C 节点的服务也崩溃,端口不通,则会出现
- 请求 1 转到 C 异常,此时所有节点(包括 backup)都异常,会出现:
- ABC 三个节点一次性恢复,加入存活列表
- 请求转到 A 处理异常,重试到 B 处理异常,最后重试到 C 处理异常
- 触发 `no live upstreams` 报错,返回 502 错误
- 所有节点再次一次性恢复,加入存活列表
- 请求 2 依次经过 AB 节点异常,重试到 C 异常,最终结果如上个步骤,返回 502 错误
踩坑集锦
如果不熟悉 HTTP 协议,以及 Nginx 的重试机制,很可能在使用过程中踩了各种各样的坑:
- 部分上游服务器出现异常却没有重试
- 一些订单创建接口,客户端只发了一次请求,后台却创建了多个订单,等等…
以下整理了一些常见的坑,以及应对策略。
需要重试却没有生效
接口的 POST 请求允许重试,但实际使用中却没有出现重试,直接报错。
从 1.9.13 版本,Nginx 不再会对一个非幂等的请求进行重试。如有需要,必须在
proxy_next_upstream
配置项中显式指定
non_idempotent
配置。参考 RFC-2616 的定义:
- 幂等 HTTP 方法:GET、HEAD、PUT、DELETE、OPTIONS、TRACE
- 非幂等 HTTP 方法:POST、LOCK、PATCH
如需要允许非幂等请求重试,配置参考如下(追加
non_idemponent
参数项):
proxy_next_upstream error timeout non_idemponent;