解Bug之路-Nginx 502 Bad Gateway

news2024/9/22 4:19:58

前言

事实证明,读过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环境下造成很大的影响,建议不开启。

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内核本身参数的调优还是非常有意义的。如果读过一些内核源码,无疑对我们排查线上问题有着很大的助力,同时也能指导我们避过一些坑!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/401317.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宁盾目录成功对接Coremail邮箱,为其提供LDAP统一认证和双因子认证

近日&#xff0c;宁盾与 Coremail 完成兼容适配&#xff0c;在 LDAP 目录用户同步、统一身份认证及双因子认证等模块成功对接。借此机会&#xff0c;双方将加深在产品、解决方案等多个领域的合作&#xff0c;携手共建信创合作生态&#xff0c;打造信创 LDAP 身份目录服务新样本…

第15章 局部波动率的影响

这学期会时不时更新一下伊曼纽尔德曼&#xff08;Emanuel Derman&#xff09; 教授与迈克尔B.米勒&#xff08;Michael B. Miller&#xff09;的《The Volatility Smile》这本书&#xff0c;本意是协助导师课程需要&#xff0c;发在这里有意的朋友们可以学习一下&#xff0c;思…

Java基础-2023.3.08-Java入门

Java入门 1.人机交互 CMD&#xff08;K1-K5&#xff09; 1.CMD&#xff1a; 1&#xff09;在Windows中&#xff0c;利用命令行的方式操作计算机 2&#xff09;可以打开文件&#xff0c;打开文件夹&#xff0c;创建文件夹等 注&#xff1a; 2.打开CMD …

GEE学习01--配置Python与Jupyter Notebook

1、查看自己电脑有哪些Python 首先&#xff0c;使用Arcgis Pro克隆了Python&#xff0c;原先的Pro自带和clone的Python都要确保在系统变量中设置了Path。 这里可以看到有一个WindowsApps&#xff0c;在Path中要将其至于自己的Python下面&#xff0c;否则的话&#xff0c;在Cm…

JavaScript语法

文章目录一、JavaScript是什么&#xff1f;JavaScript引入方式二、基础语法书写语法输出语句变量数据类型运算符流程控制语句数组函数JS变量作用域对象一、JavaScript是什么&#xff1f; JavaScript&#xff1a;是一门跨平台的脚本语言&#xff0c;用来控制网页行为&#xff0…

目标检测 pytorch复现R-CNN目标检测项目

目标检测 pytorch复现R-CNN目标检测项目1、R-CNN目标检测项目基本流程思路2、项目实现1 、数据集下载&#xff1a;2、车辆数据集抽取3、创建分类器数据集3、微调二分类网络模型4、分类器训练5、边界框回归器训练6、效果测试目标检测 R-CNN论文详细讲解1、R-CNN目标检测项目基本…

IO系统—相关数据结构初始化及调用详解(文件描述符表、文件结构表、文件节点表、设备节点表、内核驱动表等)

文章目录数据结构总述数据结构初始化流程概述各结构初始化及关联说明文件描述符表文件结构&#xff08;文件表&#xff09;文件节点设备节点内核驱动表数据结构总述 本文基于NEW_1 型内核数据结构展开&#xff0c;通过上图可以看出了使用三种数据结构&#xff08;文件描述符表项…

【Linux系统编程】03:文件夹操作

文件夹操作 OVERVIEW文件夹操作一、文件夹操作1.修改目录chdir2.打开目录opendir3.关闭目录closedir4.读取目录readdir5.文件信息获取stat二、案例使用1.ls实现2.stat文件信息获取3.ls -la主要实现4.ls -la重点修改5.ls -la文件字典序6.ls -la文件颜色显示三、补充操作1.文件定…

【SPSS】交叉设计方差分析和协方差分析详细操作教程(附案例实战)

🤵‍♂️ 个人主页:@艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞👍🏻 收藏 📂加关注+ 目录 方差分析概述 交叉设计方差分析

hibernate学习(三)

session&#xff1a;类似connection对象是连接对象 一、session&#xff1a; 二、session中的API 三、get和load的区别 四、修改方法&#xff1a; void update&#xff08;object obj&#xff09; 五、删除方法&#xff1a; void delete&#xff08;object obj&#xff0…

案例14-代码结构逻辑混乱,页面设计不美观

目录 目录 一&#xff1a;背景介绍 二&#xff1a;思路&方案 三&#xff1a;过程 问题1&#xff1a;代码可读性差&#xff0c;代码结构混乱 问题2&#xff1a; 代码逻辑混乱&#xff0c;缺乏封装的意识 问题3&#xff1a;美观问题&#xff1a;问题和图标没有对应上 四…

最短路专题——Dijkstra、Floyd、Bellman-Ford、SPFA

目录前言一、全源最短路1.1 Floyd二、单源最短路2.1 Dijkstra2.1.1 堆优化版的Dijkstra2.2 Bellman-Ford2.2.1 队列优化版的Bellman-Ford&#xff1a;SPFA前言 BFS是一种朴素的最短路算法&#xff0c;它可以找到无权图或边权都相同的图的最短路&#xff0c;但是对于边权不完全…

AbstractQueuedSynchronizer从入门到踹门

概念设计初衷&#xff1a;该类利用 状态队列 实现了一个同步器&#xff0c;更多的是提供一些模板方法&#xff08;子类必须重写&#xff0c;不然会抛错&#xff09;。 设计功能&#xff1a;独占、共享模式两个核心&#xff0c;state、Queue2.1 statesetState、compareAndSetSta…

LayerNormalization

目录 1.BN的问题 1.1 BN与batch size 1.2 BN与RNN 2.LN详解 2.1 MLP中的LN 2.2 RNN中的LN 2.3 LN与ICS和损失平面平滑 BN不适用于RNN等动态网络和batchsize较小的时候效果不好。LayerNormalization的提出有效的解决BN的这两个问题。LN和BN不同点是归一化的维度是互相垂直…

SQL总结-排名的使用

##一、通过排名或者范围条件连表筛选特殊行 第一行最后一行区间&#xff08;第一行到第二行或者连续区间&#xff09;找中位数通过排名进行分组或者连续区间 ######1.使用条件筛选连表找区间 Employee 表保存了一年内的薪水信息。 请你编写 SQL 语句&#xff0c;来查询每个…

基于ChatRWKV智能问答和内容创作

ChatRWKV是对标ChatGPT的开源项目,希望做大规模语言模型的Stable Diffusion,测试很一段时间确实很像ChatGPT,从使用方法和内容结果上都很相似,但是还有一些差异。 文章目录 准备工作环境配置创建虚拟环境激活虚拟环境pip安装匹配版本ChatRWKV 使用模型替换常用参数设置使用…

手机磁吸背夹散热器制冷快速方案

手机散热器是什么&#xff1f;手机散热器分为几种类型&#xff1f;手机散热的方式都有哪些&#xff1f; 因为经常玩游戏&#xff0c;手机发热得厉害&#xff0c;都可以煎鸡蛋了&#xff0c;心想着要买个东西给手机散散热&#xff0c;没想到还真的有手机散热器。 不知道手机散…

mysql锁分类大全

前言 为什么会出现锁 MySQL中的锁是为了保证并发操作的正确性和一致性而存在的。 当多个用户同时对同一份数据进行操作时&#xff0c;如果不加控制地进行读写操作&#xff0c;就可能导致数据不一致的问题。例如&#xff0c;当多个用户同时对同一行数据进行写操作时&#xff…

uniapp使用webview嵌入vue页面及通信

最近刚做的一个需求&#xff0c;web端&#xff08;Vue&#xff09;使用了FormMaking库&#xff0c;FormMaking是一个拖拉拽的动态设计表单、快速开发的一个东西&#xff0c;拖拽完之后最终可以导出一个很长的json&#xff0c;然后通过json再进行回显&#xff0c;快速开发&#…

【Spring Boot】Spring Boot经典面试题分享

文章目录1. SpringBoot 简介2. SpringBoot 的优缺点3. SpringBoot 固定版本4. SpringBoot 的使用方式5. SpringBoot 自动配置原理6. PropertySource7. ImportResource8. springboot 的 profile 加载9. SpringBoot 激活指定 profile 的几种方式10. SpringBoot 项目内部配置文件加…