Nginx upstream 失效转移机制研究

结论

经过多次模拟线上的环境测试,Nginx 负载均衡技术默认情况下已经对于 connect refused(状态码表现为 502)和 time out(状态码表现为 504)做了失效转移,使用的是 upstream 模块的 proxy_next_upstream 指令(这个选项默认是启动的)来实现。

对于 http GET 请求,当这个请求转发到上游服务器发生断路,或者读取响应超时则会将同样的请求转发到其他上游服务器来处理,如果所有服务器都超时或者断路,则会返回 502 或者 504 错误。

对于http POST 请求,当这个请求转发到上游服务器发生断路,则会将请求转发到其他上游服务器来处理,但是如果这个请求发生了读取超时,则不会做失效转移,会返回 504 错误,Nginx 之所以这么做应该是为了防止同一个请求发送两次,比如涉及到银行的充值等操作就会发生很严重的 bug。以下是模拟线上的场景测试得出的结论:

  1. 上游服务器有两台,一台处于 down 状态,另一台处于正常服务状态,那么来自客户端的 GETPOST 请求都会通过 Nginx 的失效转移机制路由到正常状态的机器,返回 200 状态码,并不会返回给客户端 502 错误;
  2. 上游服务器有两台,两台都 down 了,那么会不管是 GET 还是 POST 请求都会直接返回给客户端 502 错误;
  3. 上游服务器有两台,一台机器的 http GETPOST 接口都正常 return,另一台相同的接口死循环,模拟超时。
    这种情况下如果客户端的请求路由到了正常机器,那么直接返回 200
    如果请求路由到了死循环的接口,并且是 GET 请求,那么会等待 Nginx 设置的超时时间过后,然后将请求转发到另一台机器的正常接口。
    如果请求路由到了死循环的接口,并且是 POST 请求,那么等待 nginx 设置的超时时间过后直接返回 504,没有进行失效转移,防止请求的重复发送;
  4. 上游服务器有两台,两台机器的 http GETPOST 接口都死循环,模拟超时,那么对于 GET 请求会进行请求转发到另一台尝试,对于 POST 请求直接返回 504,不会进行进一步尝试;

论证环境及工具

  • 一台前端 Nginx 服务器;
  • 两台上游服务器;
  • Nginx 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 443;
server_name ngxfailover.xxx.me;
ssl on;
ssl_certificate xxx/xxx.crt;
ssl_certificate_key xxx/xxx.key;
location / {
proxy_pass http://py_web_upstream;
}
}

upstream py_web_upstream{
server upstream_server1:5000;
server upstream_server2:5000;
}
  • 第一台上游服务器正常代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from flask import Flask
app = Flask(__name__)


@app.route('/a/<name>')
def failover_get_method(name):
print name
return '<h1>I am Server2, My name is %s </h1>' % name

@app.route('/b/<name>', methods=["POST"])
def failover_post_method(name):
print name
return '<h1>I am Server2, My name is %s </h1>' % name


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
  • 第二台上游服务器超时代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
from flask import Flask
app = Flask(__name__)


@app.route('/a/<name>')
def failover_get_method(name):
print name
while True:
time.sleep(256)
return '<h1>I am Server2, My name is %s </h1>' % name

@app.route('/b/<name>', methods=["POST"])
def failover_post_method(name):
print name
while True:
time.sleep(256)
return '<h1>I am Server2, my name is %s </h1>' % name


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

论证过程

以下为前面 4 种案例的论证过程:

案例 1

上游服务器有两台,一台处于 down 状态,另一台处于正常服务状态。

在这种情况下,通过 curl 多次发送 GETPOST 请求,发现不管怎么请求,返回都是正常状态,如果 Nginx 发生了失败尝试操作,那么会在 Nginx access 日志中的 upstream 字段看到有两个服务器的地址。

发送 GET 和 POST 请求:

1
2
curl -XGET https://ngxfailover.xxx.me/a/hello
curl -XPOST https://ngxfailover.xxx.me/b/hello

观察日志:

可以看出所有请求都成功了,红框框圈起来的请求表示发生了失效转移,并且请求成功。

案例 2

上游服务器有两台,两台服务器都处于 down 状态。

在这种情况下不管是 GET 还是 POST 请求都会直接返回给客户端 502 错误。

发送 GET 和 POST 请求:

1
2
curl -XGET https://ngxfailover.xxx.me/a/hello
curl -XPOST https://ngxfailover.xxx.me/b/hello

观察日志:
可以看出所有请求全部返回 502 错误,红框框圈起来的请求表示发生了失效转移,但是还是失败了。

案例 3

上游服务器有两台,一台机器的 http GETPOST 接口都正常 return,另一台相同的接口死循环,模拟超时。

这种情况下如果客户端的请求路由到了正常机器,那么直接返回 200

如果请求路由到了死循环的接口,并且是 GET 请求,那么会等待 Nginx 设置的超时时间过后,然后将请求转发到另一台机器的正常接口。

如果请求路由到了死循环的接口,并且是 POST 请求,那么等待 Nginx 设置的超时时间过后直接返回客户端 504 错误,没有进行失效转移,防止请求的重复发送。

发送 GET 请求:

1
curl -XGET https://ngxfailover.xxx.me/a/hello

观察日志:

可以看到对于 GET 请求全部成功,红框框圈起来的表示发生了失效转移,第一台超时后会是继续尝试第二台,最终成功。

发送 POST 请求:

1
curl -XPOST https://ngxfailover.xxx.me/b/hello

观察日志:

可以看到对于 POST 请求,如果 Nginx 等待上游服务器处理请求超时,并不会发生失效转移,直接返回给客户端 504 错误。

案例 4

上游服务器有两台,两台机器的 http GETPOST 接口都死循环,模拟超时。

这种情况下对于 GET 请求会将请求转发到另一台尝试,对于 POST 请求直接返回 504 错误,不会进行进一步尝试。

发送 GET 请求:

1
curl -XGET https://ngxfailover.xxx.me/a/hello

观察日志:

可以看出对于 GET 请求,Nginx 在等待超时会继续进行尝试,两台都尝试失败后返回了 504 错误。

发送 POST 请求:

1
curl -XPOST https://ngxfailover.xxx.me/b/hello

观察日志:

可以看出对于 POST 请求,Nginx 在等待超时会不继续进行尝试其他上游服务器,直接返回 504 错误。

总结

总体来看 Nginx 的失效转移技术已经非常成熟,Nginx 默认情况下对于 connect refused(状态码表现为 502)和 time out(状态码表现为 504)已经做了失效转移,并且 Nginx 根据请求的类型不同,对失效转移的策略也不同。对于服务器后台状态没有改变的请求(比如 GET 请求)会进行失效转移,对于服务后台状态有改变的请求(比如 POST 请求),有失效转移机制,这也符合 Rest API 的冪等性标准。如果要强行加其他状态码的失效转移,比如 500503 等,需要考量下业务请求是否能容忍请求的重复发送。