Nginx Bad Gateway和no live upstreams错误分析
最近项目的生产环境中客户端出现大量的Nginx 502 Bad Gateway错误,逐步排查最终定位到是由于被ddos攻击造成服务器资源耗尽无法响应造成的问题。遂整理过程著文以记之。 场景 线上4个节点,每个节点都有两个相同服务通过Nginx作负载均衡,均采用Nginx默认值,未做过多配置,配置类似: 1upstream test-server { 2 server 127.0.0.1:8002; 3 server 127.0.0.1:8001; 4} 客户端出现大量的 502 Bad Gateway 信息,查看Nginx错误日志,信息如下: no live upstreams while connecting to upstream 初步定位问题,发现后台出现了很多莫名其妙的错误,查看Nginx错误日志发现也打印了很多上述错误。怀疑是后台某个接口请求出错导致返回了 500,导致Nginx将这个服务下线,后经过排查,后台确实存在一些错误信息,但是出错上述错误的时间不匹配。 后整理思路并认真思考,发现可能思路存在偏差:后台http状态码怎么会影响Nginx将服务下线呢? 因为Http状态码表示http响应的状态,也就是表示响应结果的正确与否,比如 2xx 表示服务端正确处理并响应了数据,4xx 表示客户端存在错误妨碍了服务端的处理,5xx 表示服务端错误导致处理失败了。但是,能够返回这些状态码说明服务端连接正常,只是由于特定原因导致服务端响应错误了。返回了这些状态码Nginx当真会将服务下线吗?试想一下,某一个客户端请求查询一条不存在的数据导致服务端处理时抛出异常并响应 500 状态码,然后Nginx认为服务不可用并将其下线,导致一定时间内所有的请求都返回上述错误,那么罪魁祸首到底是服务端还是客户端?Nginx这么处理是不是太过了呢?Nginx肯定不会这么来设计。 所以,我猜测Nginx不可能如此是非不分,应该是别的原因导致的。 查阅upstream官方文档,看到这么两个参数配置: fail_timeout=time: 设置多少时间内不能与下游服务成功通信 max_fails 次后可以认为下游服务不可用,这是一个周期时间,即每隔 fail_timeout 时间都会进行统计,默认为 10 秒。 max_fails=number: 设置在 fail_timeout 时间内与下游服务通信失败的次数,达到该次数后认为服务不可用,默认为1。 按照Nginx的默认设置,也就是说每个10秒统计下有服务的通信失败次数,达到1次就认为服务不可用,此时Ngingx会将其踢下线,后续所有转发到该服务的请求都会返回 no live upstreams while connecting to upstream,直到下一个10秒再重新处理(max_fails 为0又重新将服务上线)。 关键在于这个 通信失败 的理解。通信失败,表示Nginx转发请求给服务,但是服务没有任何响应,而不是一开始怀疑的 HTTP 状态不是200,能成功响应,不论是什么状态码,都应该认为与服务通信成功。 实践才能出真知,为了验证我的猜想,必须进行实验。 实验 使用golang编写一个http服务,代码如下: 1var inErr bool 2func main() { 3 port := os.Args[1] 4 node := os.Args[2] 5 r := gin.Default() 6 go func() { 7 for { 8 if node == "node1" { 9 break 10 } 11 inErr = !inErr 12 time.Sleep(3 * time.Second) 13 } 14 }() 15 r.GET("/", func(ctx *gin.Context) { 16 if inErr { 17 ctx.String(http.StatusInternalServerError, "error: "+node) 18 } else { 19 ctx.JSON(http.StatusOK, gin.H{"msg": "ok, " + node}) 20 } 21 }) 22 r.GET("/err", func(ctx *gin.Context) { 23 ctx.String(http.StatusInternalServerError, "error: "+node) 24 ctx.Abort() 25 }) 26 r.GET("/timeout", func(ctx *gin.Context) { 27 time.Sleep(time.Second * 10) 28 ctx.String(http.StatusOK, "after 10s responsed") 29 }) 30 _ = r.Run(":" + port) // 参数0为执行文件本身信息,真正的参数下标为1 31} 上述代码使用了 gin 框架,大致的逻辑: ...