每日一博 | 解 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_range 32768 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_range 22768 61000 cat /proc/sys/net/ipv4/tcp_max_tw_buckets 20000
开启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_established static 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; 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内核本身参数的调优还是非常有意义的。如果读过一些内核源码,无疑对我们排查线上问题有着很大的助力,同时也能指导我们避过一些坑!
公众号
关注笔者公众号,获取更多干货文章:
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
picbed 1.8.0 发布
picbed是一款基于Python Flask的简洁不小气的web图床,本次1.8发布完成标准化、规范化的新版,更完善的第三方扩展加载器和文档。 更新如下: 功能: 全站公告 忘记/重置密码 钩子支持appversion元数据 钩子的模板扩展点增加adminscript、userscript、nav 钩子路由方法 用户设置标签(分组)及按标签设置分组上传所用后端 用户审核拒绝提示,拒绝后重新提交申请 修复: 解决textarea类型多行文本造成的页面错误 设置默认SecretKey解决正式环境多workers状态紊乱 解决首页上传设置相册时粘贴文字出现的提示 更改: 删除用户时一并删除用户产生的数据 删除图片时删除数据 正式环境脚本采用-c方式读取picbed.py 打印config便于调试 钩子加载时检测版本是否符号语义化2.0规范 安装第三方包时使用upgrade方式 钩子扩展操作按钮改为图标 内置钩子up2oss、up2cos移除,可无缝改为第三方 钩子管理器call方法args、kwargs已经废弃 优化: 用户管理显示细节增强 用户邮箱验证 设置首页上传区域提示内容时进行HT...
- 下一篇
Fedora 33 桌面版本默认使用 Btrfs 最新进展
虽然 Fedora 工程和指导委员会(FESCo) 已批准 Fedora 33 桌面变体(版本)默认使用 Btrfs 文件系统,但为了在今年秋季 Fedora 33 发布时顺利实现这一目标,还有很多工作需要完成。 现在来看一下 Fedora 团队针对这项即将到来的新变更提供的进度报告: 更新 Anaconda 安装程序,使其默认使用 Btrfs 来对所有桌面安装媒介进行自动分区 新添加 Libvirtd 补丁,该补丁如果在 Btrfs 上运行,将在包含目录的 VM 镜像上设置"nodatacow"。这是为了禁用具有虚拟机镜像的目录的 Copy-on-Write 功能,因为这对性能有影响,而 CoW 功能的用处并不大 Fedora 33 内核已经加入了多个 Btrfs 内核补丁,相关 PR 已提交到上游的 linux-btrfs@,包括新的 'rescue=all'挂载选项 Btrfs 文件系统驱动现在已经内置于内核中,而不是作为一个模块 Red Hat CKI(Red Hat Continuous Kernel Integration,红帽持续内核集成项目)已经将 Btrfs 添加到其...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS关闭SELinux安全模块
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Linux系统CentOS6、CentOS7手动修改IP地址