写点什么

解 Bug 之路 -Nginx 502 Bad Gateway

发布于: 2020 年 09 月 09 日
解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 的。又看了下错误日志,发现有大量的


Cannot assign requested address
复制代码


由于笔者读过 TCP 源码,一瞬间就反应过来,是端口号耗尽了!由于 Nginx upstream 和后端 Backend 默认是短连接,所以在大量请求流量进来的时候回产生大量 TIME_WAIT 的连接。



而这些 TIME_WAIT 是占据端口号的,而且基本要 1 分钟左右才能被 Kernel 回收。



cat /proc/sys/net/ipv4/ip_local_port_range32768    61000
复制代码


也就是说,只要一分钟之内产生 28232(61000-32768)个 TIME_WAIT 的 socket 就会造成端口号耗尽,也即 470.5TPS(28232/60),只是一个很容易达到的压测值。事实上这个限制是 Client 端的,Server 端没有这样的限制,因为 Server 端口号只有一个 8080 这样的有名端口号。而在

upstream 中 Nginx 扮演的就是 Client,而 Gateway2 就扮演的是 Nginx



为什么 Nginx 的 CPU 是 100%


而笔者也很快想明白了 Nginx 为什么吃满了机器的 CPU,问题就出来端口号的搜索过程。



让我们看下最耗性能的一段函数:


int __inet_hash_connect(...){        // 注意,这边是static变量        static u32 hint;        // hint有助于不从0开始搜索,而是从下一个待分配的端口号搜索        u32 offset = hint + port_offset;        .....        inet_get_local_port_range(&low, &high);        // 这边remaining就是61000 - 32768        remaining = (high - low) + 1        ......        for (i = 1; i <= remaining; i++) {            port = low + (i + offset) % remaining;            /* port是否占用check */            ....            goto ok;        }        .......ok:        hint += i;        ......}
复制代码


看上面那段代码,如果一直没有端口号可用的话,则需要循环 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 端口可用,这样就可以避免再次到调大临界值得时候继续击穿临界点。


cat /proc/sys/net/ipv4/ip_local_port_range22768    61000cat /proc/sys/net/ipv4/tcp_max_tw_buckets20000
复制代码


开启 tcp_tw_reuse


这个问题 Linux 其实早就有了解决方案,那就是 tcp_tw_reuse 这个参数。


echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse
复制代码


事实上 TIME_WAIT 过多的原因是其回收时间竟然需要 1min,这个 1min 其实是 TCP 协议中规定的 2MSL 时间,而 Linux 中就固定为 1min。


#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT                  * state, about 60 seconds    */
复制代码


2MSL 的原因就是排除网络上还残留的包对新的同样的五元组的 Socket 产生影响,也就是说在 2MSL(1min)之内重用这个五元组会有风险。为了解决这个问题,Linux 就采取了一些列措施防止这样的情况,使得在大部分情况下 1s 之内的 TIME_WAIT 就可以重用。下面这段代码,就是检测此 TIME_WAIT 是否重用。


__inet_hash_connect    |->__inet_check_establishedstatic int __inet_check_established(......){    ......        /* Check TIME-WAIT sockets first. */    sk_nulls_for_each(sk2, node, &head->twchain) {        tw = inet_twsk(sk2);        // 如果在time_wait中找到一个match的port,就判断是否可重用        if (INET_TW_MATCH(sk2, net, hash, acookie,                    saddr, daddr, ports, dif)) {            if (twsk_unique(sk, sk2, twp))                goto unique;            else                goto not_unique;        }    }    ......}
复制代码


而其中的核心函数就是 twsk_unique,它的判断逻辑如下:


int tcp_twsk_unique(......){    ......    if (tcptw->tw_ts_recent_stamp &&        (twp == NULL || (sysctl_tcp_tw_reuse &&                 get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {       // 对write_seq设置为snd_nxt+65536+2       // 这样能够确保在数据传输速率<=80Mbit/s的情况下不会被回绕              tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2        ......        return 1;    }    return 0;    }
复制代码


上面这段代码逻辑如下所示:



在开启了 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 环境下造成很大的影响,建议不开启,具体见笔者的另一篇博客:


https://my.oschina.net/alchemystar/blog/3119992
复制代码


Nginx upstream 改成长连接


事实上,上面的一系列问题都是由于 Nginx 对 Backend 是短连接导致。

Nginx 从 1.1.4 开始,实现了对后端机器的长连接支持功能。在 Upstream 中这样配置可以开启长连接的功能:


upstream backend {    server 127.0.0.1:8080;# It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker             process can open. The connections parameter should be set to a number small enough to let upstream servers process new incoming connections as     well.    keepalive 32;     keepalive_timeout 30s; # 设置后端连接的最大idle时间为30s}
复制代码


这样前端和后端都是长连接,大家又可以愉快的玩耍了。



由此产生的风险点


由于对单个远端 ip:port 耗尽会导致 CPU 吃满这种现象。所以在 Nginx 在配置 Upstream 时候需要格外小心。假设一种情况,PE 扩容了一台 Nginx,为防止有问题,就先配一台 Backend 看看情况,这时候如果量比较大的话击穿临界点就会造成大量报错(而应用本身确毫无压力,毕竟临界值是 470.5TPS(28232/60)),甚至在同 Nginx 上的非此域名的请求也会因为 CPU 被耗尽而得不到响应。多配几台 Backend/开启 tcp_tw_reuse 或许是不错的选择。


总结


应用再强大也还是承载在内核之上,始终逃不出 Linux 内核的樊笼。所以对于 Linux 内核本身参数的调优还是非常有意义的。如果读过一些内核源码,无疑对我们排查线上问题有着很大的助力,同时也能指导我们避过一些坑!


公众号


关注笔者公众号,获取更多干货文章:



发布于: 2020 年 09 月 09 日阅读数: 1229
用户头像

公众号: <<解Bug之路>> 2019.02.11 加入

无论多么艰苦的时刻,都不要忘记。辉煌的未来,在你的眼中闪耀!

评论 (3 条评论)

发布
用户头像
厉害。。 可以讲讲TCP吗,我只知道三次握手; 源码方面几乎没看过
2020 年 09 月 10 日 08:27
回复
可以
2020 年 09 月 23 日 08:44
回复
用户头像
自己顶
2020 年 09 月 09 日 10:16
回复
没有更多了
解Bug之路-Nginx 502 Bad Gateway