解 Bug 之路 -Nginx 502 Bad Gateway
解 Bug 之路-Nginx 502 Bad Gateway
前言
事实证明,读过 Linux 内核源码确实有很大的好处,尤其在处理问题的时刻。当你看到报错的那一瞬间,就能把现象/原因/以及解决方案一股脑的在脑中闪现。甚至一些边边角角的现象都能很快的反应过来是为何。笔者读过一些 Linux TCP 协议栈的源码,就在解决下面这个问题的时候有一种非常流畅的感觉。
Bug 现场
首先,这个问题其实并不难解决,但是这个问题引发的现象倒是挺有意思。先描述一下现象吧,
笔者要对自研的 dubbo 协议隧道网关进行压测(这个网关的设计也挺有意思,准备放到后面的博客里面)。先看下压测的拓扑吧:
为了压测笔者 gateway 的单机性能,两端仅仅各保留一台网关,即 gateway1 和 gateway2。压到一定程度就开始报错,导致压测停止。很自然的就想到,网关扛不住了。
网关的情况
去 Gateway2 的机器上看了一下,没有任何报错。而 Gateway1 则有大量的 502 报错。502 是 Bad Gateway,Nginx 的经典报错,首先想到的就是 Gateway2 不堪重负被 Nginx 在 Upstream 中踢掉。
那么,就先看看 Gateway2 的负载情况把,查了下监控,发现 Gateway2 在 4 核 8G 的机器上只用了一个核,完全看不出来有瓶颈的样子,难道是 IO 有问题?看了下小的可怜的网卡流量打消了这个猜想。
Nginx 所在机器 CPU 利用率接近 100%
这时候,发现一个有意思的现象,Nginx 确用满了 CPU!
再次压测,去 Nginx 所在机器上 top 了一下,发现 Nginx 的 4 个 Worker 分别占了一个核把 CPU 吃满-_-!
什么,号称性能强悍的 Nginx 竟然这么弱,说好的事件驱动\epoll 边沿触发\纯 C 打造的呢?一定是用的姿势不对!
去掉 Nginx 直接通信毫无压力
既然猜测是 Nginx 的瓶颈,就把 Nginx 去掉吧。Gateway1 和 Gateway2 直连,压测 TPS 里面就飙升了,而且 Gateway2 的 CPU 最多也就吃了 2 个核,毫无压力。
去 Nginx 上看下日志
由于 Nginx 机器权限并不在笔者手上,所以一开始没有关注其日志,现在就联系一下对应的运维去看一下吧。在 accesslog 里面发现了大量的 502 报错,确实是 Nginx 的。又看了下错误日志,发现有大量的
由于笔者读过 TCP 源码,一瞬间就反应过来,是端口号耗尽了!由于 Nginx upstream 和后端 Backend 默认是短连接,所以在大量请求流量进来的时候回产生大量 TIME_WAIT 的连接。
而这些 TIME_WAIT 是占据端口号的,而且基本要 1 分钟左右才能被 Kernel 回收。
也就是说,只要一分钟之内产生 28232(61000-32768)个 TIME_WAIT 的 socket 就会造成端口号耗尽,也即 470.5TPS(28232/60),只是一个很容易达到的压测值。事实上这个限制是 Client 端的,Server 端没有这样的限制,因为 Server 端口号只有一个 8080 这样的有名端口号。而在
upstream 中 Nginx 扮演的就是 Client,而 Gateway2 就扮演的是 Nginx
为什么 Nginx 的 CPU 是 100%
而笔者也很快想明白了 Nginx 为什么吃满了机器的 CPU,问题就出来端口号的搜索过程。
让我们看下最耗性能的一段函数:
看上面那段代码,如果一直没有端口号可用的话,则需要循环 remaining 次才能宣告端口号耗尽,也就是 28232 次。而如果按照正常的情况,因为有 hint 的存在,所以每次搜索从下一个待分配的端口号开始计算,以个位数的搜索就能找到端口号。如下图所示:
所以当端口号耗尽后,Nginx 的 Worker 进程就沉浸在上述 for 循环中不可自拔,把 CPU 吃满。
为什么 Gateway1 调用 Nginx 没有问题
很简单,因为笔者在 Gateway1 调用 Nginx 的时候设置了 Keepalived,所以采用的是长连接,就没有这个端口号耗尽的限制。
Nginx 后面有多台机器的话
由于是因为端口号搜索导致 CPU 100%,而且但凡有可用端口号,因为 hint 的原因,搜索次数可能就是 1 和 28232 的区别。
因为端口号限制是针对某个特定的远端 server:port 的。
所以,只要 Nginx 的 Backend 有多台机器,甚至同一个机器上的多个不同端口号,只要不超过临界点,Nginx 就不会有任何压力。
把端口号范围调大
比较无脑的方案当然是把端口号范围调大,这样就能抗更多的 TIME_WAIT。同时将 tcp_max_tw_bucket 调小,tcp_max_tw_bucket 是 kernel 中最多存在的 TIME_WAIT 数量,只要 port 范围 - tcp_max_tw_bucket 大于一定的值,那么就始终有 port 端口可用,这样就可以避免再次到调大临界值得时候继续击穿临界点。
开启 tcp_tw_reuse
这个问题 Linux 其实早就有了解决方案,那就是 tcp_tw_reuse 这个参数。
事实上 TIME_WAIT 过多的原因是其回收时间竟然需要 1min,这个 1min 其实是 TCP 协议中规定的 2MSL 时间,而 Linux 中就固定为 1min。
2MSL 的原因就是排除网络上还残留的包对新的同样的五元组的 Socket 产生影响,也就是说在 2MSL(1min)之内重用这个五元组会有风险。为了解决这个问题,Linux 就采取了一些列措施防止这样的情况,使得在大部分情况下 1s 之内的 TIME_WAIT 就可以重用。下面这段代码,就是检测此 TIME_WAIT 是否重用。
而其中的核心函数就是 twsk_unique,它的判断逻辑如下:
上面这段代码逻辑如下所示:
在开启了 tcp_timestamp 以及 tcp_tw_reuse 的情况下,在 Connect 搜索 port 时只要比之前用这个 port 的 TIME_WAIT 状态的 Socket 记录的最近时间戳>1s,就可以重用此 port,即将之前的 1 分钟缩短到 1s。同时为了防止潜在的序列号冲突,直接将 write_seq 加上在 65537,这样,在单 Socket 传输速率小于 80Mbit/s 的情况下,不会造成序列号重叠(冲突)。
同时这个 tw_ts_recent_stamp 设置的时机如下图所示:
所以如果 Socket 进入 TIME_WAIT 状态后,如果一直有对应的包发过来,那么会影响此 TIME_WAIT 对应的 port 是否可用的时间。
开启了这个参数之后,由于从 1min 缩短到 1s,那么 Nginx 单台对单 Upstream 可承受的 TPS 就从原来的 470.5TPS(28232/60)一跃提升为 28232TPS,增长了 60 倍。
如果还嫌性能不够,可以配上上面的端口号范围调大以及 tcp_max_tw_bucket 调小继续提升 tps,不过 tcp_max_tw_bucket 调小可能会有序列号重叠的风险,毕竟 Socket 不经过 2MSL 阶段就被重用了。
不要开启 tcp_tw_recycle
开启 tcp_tw_recyle 这个参数会在 NAT 环境下造成很大的影响,建议不开启,具体见笔者的另一篇博客:
Nginx upstream 改成长连接
事实上,上面的一系列问题都是由于 Nginx 对 Backend 是短连接导致。
Nginx 从 1.1.4 开始,实现了对后端机器的长连接支持功能。在 Upstream 中这样配置可以开启长连接的功能:
这样前端和后端都是长连接,大家又可以愉快的玩耍了。
由此产生的风险点
由于对单个远端 ip:port 耗尽会导致 CPU 吃满这种现象。所以在 Nginx 在配置 Upstream 时候需要格外小心。假设一种情况,PE 扩容了一台 Nginx,为防止有问题,就先配一台 Backend 看看情况,这时候如果量比较大的话击穿临界点就会造成大量报错(而应用本身确毫无压力,毕竟临界值是 470.5TPS(28232/60)),甚至在同 Nginx 上的非此域名的请求也会因为 CPU 被耗尽而得不到响应。多配几台 Backend/开启 tcp_tw_reuse 或许是不错的选择。
总结
应用再强大也还是承载在内核之上,始终逃不出 Linux 内核的樊笼。所以对于 Linux 内核本身参数的调优还是非常有意义的。如果读过一些内核源码,无疑对我们排查线上问题有着很大的助力,同时也能指导我们避过一些坑!
公众号
关注笔者公众号,获取更多干货文章:
版权声明: 本文为 InfoQ 作者【无毁的湖光】的原创文章。
原文链接:【http://xie.infoq.cn/article/bb730d7c3ef9f43a9244d6bc4】。文章转载请联系作者。
评论 (3 条评论)