解之路——

舒肤佳洗手液2022-07-06  16

转载:

前言证明了阅读Linux内核的源代码是大有裨益的,尤其是在处理问题的时候。当你看到错误时,你可以在脑海中闪现现象/原因/解决方法。甚至一些棱角也能很快反映出为什么。笔者看过一些Linux TCP协议栈的源代码,在解决下面这个问题的时候有一种非常流畅的感觉。

首先,这个问题并不难解决,但是这个问题引发的现象还是挺有意思的。先描述一下现象。我想测试一下自研的dubbo协议隧道网关(这个网关的设计也挺有意思的,我准备放在下面的博客里)。让我们先来看看压力测量的拓扑结构:

为了测试作者网关的单机性能,每端只预留一个网关,即gateway1和gateway2。当压力达到一定水平时,将报告错误,这将导致压力测量停止。很自然的认为网关承受不了。

在Gateway2的机器上检查网关的情况,没有错误。Gateway1有大量的502错误。02是坏网关的经典错误,Nginx。首先想到的是Gateway2在上游被Nginx压倒并踢开。

然后,先看看Gateway2的负载情况,查一下监控,发现Gateway2在4核8G的机器上只用了一个核,根本不存在瓶颈。木卫一有问题吗?看着那点可怜的网卡流量打消了这个猜想。

Nginx所在机器的CPU利用率接近100%。这时,发现一个有趣的现象。Nginx确实CPU满满的!

再次压力测试,上了Nginx的机器顶,发现Nginx的四个Worker占了一个核,把CPU吃光了-_-!

什么,号称性能强大的Nginx这么弱,说好的事件驱动\epoll edge trigger \是纯C做的?肯定是姿势不对!

摆脱Nginx直联毫无压力。既然投机是Nginx的瓶颈,那就把Nginx去掉吧。Gateway1和Gateway2是直连的,压力测试时TPS飙升。而且Gateway2的CPU最多吃了2核,没什么压力。

去Nginx看一下日志。因为Nginx机器的权限不在作者手里,所以一开始我并没有关注它的日志。现在联系相应的运维部门看一下。在accesslog中发现了大量的502错误,这些错误确实是Nginx的。再次查看错误日志,我发现了大量的

没有分配请求的地址因为我看过TCP源码,我能在一瞬间反应过来,端口号用完了!由于Nginx上游和后端后端后台默认为短连接,当大量请求流量进来时会产生大量TIME_WAIT连接。

这些TIME_WAIT占用端口号,内核恢复需要1分钟左右。

cat/proc/sys/net/IP v4/IP _ local _ port _ range 32768 61000也就是说一分钟内只要生成28232(61000-32768)个TIME_WAIT的socket,端口号就会耗尽,也就是470.5TPS(28232/60),这只是一个事实上这个限制是在客户端,在服务器端没有这个限制,因为服务器端口号只是一个有名的在上游,Nginx扮演客户端的角色,Gateway2扮演Nginx的角色。

为什么Nginx的CPU是100%,笔者很快就想明白了为什么Nginx会把机器的CPU吃光。问题是端口号的搜索过程。

让我们来看看性能最密集的函数:

Int __inet_hash_connect(...){ //注意,这里是静态变量static u32提示;// hint帮助从下一个要分配的端口号开始搜索u32 offset = hint+port_offset,而不是从0开始;.....inet_get_local_port_range(low, high);//这里的剩余是61000-32768剩余=(高低)+1...for(I = 1;我 lt=剩余;i++) { port = low + (i + offset) %剩余;/*端口是否占用检查*/...gotook} .......ok:提示+= I;.......}看看上面的代码。如果始终没有可用的端口号,则需要循环剩余次数来声明端口号耗尽,即28232次。按照正常情况,因为hint的存在,每次搜索都是从下一个要分配的端口号开始,通过个位数搜索就能找到端口号。如下图所示:

所以当端口号耗尽时,Nginx的工作进程就沉浸在上述for循环中,把CPU吃光了。

很简单为什么Gateway1调用Nginx时没有问题,因为作者在Gateway1调用Nginx时设置了Keepalived,所以使用的是长连接,所以不存在这个端口号会被耗尽的限制。

如果Nginx后面有多台机器,CPU会100%因为端口号搜索,而每当有可用端口号的时候,搜索次数可能因为提示是1到28232的差。

因为端口号限制是针对特定的远程服务器:port。所以只要Nginx的后端有多台机器,甚至同一台机器上有多个不同的端口号,只要不超过临界点,Nginx就不会有任何压力。

扩大端口号范围的无脑方案当然是扩大端口号的范围,这样可以抵抗更多的TIME_WAIT。同时关小tcp_max_tw_bucket。tcp_max_tw_bucket是内核中最大的TIME_WAIT数。只要端口范围-tcp_max_tw_bucket大于某个值,总会有端口端口可用,避免再次调高临界值时进一步击穿临界点。

cat/proc/sys/net/IP v4/IP _ local _ port _ range 2768 61000 cat/proc/sys/net/IP v4/TCP _ Max _ TW _ Buckets 20000 Open tcp_tw_reuse Linux对于这个问题其实有一个由来已久的解决方案,那就是参数TCP _ TW _ reuse。

回声 # 39;1#39; gt/proc/sys/net/IP v4/TCP _ TW _ Reuse其实TIME_WAIT过大的原因是它的恢复时间实际需要1min,这实际上是TCP协议中规定的2MSL时间,但在Linux中固定为1min。

#定义TCP _ TIME WAIT _ LEN (60 * Hz)/*等待多长时间销毁time-wait * state,大约60秒*/2 MSL原因是为了排除网络上仍然残留的数据包影响同一个五元组的新套接字,也就是说在2MSL(1min)内重用这个五元组会有风险。为了解决这个问题,Linux采取了一些措施来防止这种情况,使得1s以内的TIME_WAIT在大多数情况下可以重用。下面的代码是检查这个TIME_WAIT是否重用。

_ _ inet _ hash _ connect |- gt;_ _ inet _ check _ established static int _ _ inet _ check _ established(......){ ....../*首先检查时间等待套接字。*/ sk_nulls_for_each(sk2,node, head- gt;tw chain){ tw = inet _ twsk(sk2);//如果在time_wait中找到匹配端口,判断if (inet _ tw _ match (sk2,net,hash,acocookie,saddr,daddr,ports,dif)) {if (twsk _ unique (sk,sk2,twp))是否为唯一;else goto not _ unique}} ......}而核心函数是twsk_unique,其判断逻辑如下:

int tcp_twsk_unique(......){ ......if(TCP tw- gt;tw _ ts _ recent _ stamp (twp = = NULL | |(sysctl _ TCP _ tw _ reuse get _ seconds()-TCP tw- gt;tw _ ts _ recent _ stamp gt1))) {//将write_seq设置为snd_nxt+65536+2 //这样可以保证数据传输速率< =80Mbit/s,不会绕到TP->:write _ seq = TCP tw- gt;tw_snd_nxt + 65535 + 2......返回1;}返回0;}以上代码逻辑如下:

在tcp_timestamp和tcp_tw_reuse开启的情况下,Connect搜索一个端口时,只需要在这个端口的TIME_WAIT状态下用Socket记录最新的时间戳>:1s,就可以复用这个端口,也就是把之前的1分钟缩短为1s。同时为了防止潜在的序列号冲突,直接在65537中加入write_seq,这样在单个Socket的传输速率小于80mbit/s的情况下就不会造成序列号重叠(冲突),同时设置这个tw_ts_recent_stamp的时序如下图所示:

所以,如果Socket进入TIME_WAIT状态,如果一直有对应的数据包发送,就会影响到这个TIME_WAIT对应的端口可用的时间。开启该参数后,由于从1min缩短到1s,Nginx从单上行到单上行可承受的TPS从原来的470.5TPS(28232/60)提高到28232TPS,提高了60倍。如果性能不够,可以配合上面的端口号范围扩大和tcp_max_tw_bucket缩减继续提高tps。但如果减少tcp_max_tw_bucket,可能会有序列号重叠的风险。毕竟插座是复用的,不经过2MSL阶段。

不要打开tcp_tw_recycle。在NAT环境下开启tcp_tw_recyle会有很大影响,建议不要开启。

Nginx上游改为长连接。其实以上问题都是NGX和后端连接过短造成的。从1.1.4开始,Nginx实现了对后端机的长连接支持功能。在上游,此配置可以打开长连接功能:

上游后端{服务器127 . 0 . 0 . 1:8080;#需要特别注意的是,keepalive指令并不限制nginx工作进程可以打开的上游服务器连接的总数。应该将connections参数设置为一个足够小的数字,以便上游服务器也能处理新的传入连接。keepalive 32keepalive _ timeout 30s#将后端连接的最大空闲时间设置为30s}这样前端和后端都是长连接,大家又可以玩得开心了。

由此产生的风险点会导致由于单个远程ip:port耗尽而导致CPU将满的现象。因此,在Nginx中配置上游时,您需要格外小心。我们假设一个情况,PE扩展了一个Nginx。为了防止出现问题,它应该先装一个后端看看情况。这时候如果体量比较大,突破临界点会产生大量错误(而且应用本身没有压力,毕竟临界值是470.5TPS(28232/60))。即使是这个域名之外的同一个Nginx上的请求,也会因为CPU耗尽而得不到响应。也许多一些后端/开放tcp_tw_reuse是个不错的选择。

应用再强大,也还是承载在内核上,逃不出Linux内核的牢笼。所以对Linux内核本身的参数进行调优是非常有意义的。如果你看过一些内核源代码,无疑可以帮助我们在线排查问题,同时也可以引导我们避开一些坑!

转载请注明原文地址:https://juke.outofmemory.cn/read/620103.html
最新回复(0)