懂得三境界-使用dubbo时请求超过问题

2022年01月16日 阅读数:3
这篇文章主要向大家介绍懂得三境界-使用dubbo时请求超过问题,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

引子css

刚才下班回家路上,无心中听到大街上放的歌,歌词有这么一句:“毡房外又有驼铃声声响起,我知道那必定不是你”。这一句我彷佛听懂了歌者的魂牵梦绕和绝望,若是在十年前我大概只能感觉出悠扬的声调里溢出的悲凉吧。linux

在工做上,我没有十年的时间来把思考能力上升一个等级。对于一个问题,须要在很短的时间反复思考,深层次的弄懂。懂得,有三个初级境界,对应三个方法:nginx

1>字面理解-what、why、how
redis

2>来龙去脉-5why
算法

3>选择最优-SMARTapache

想了解what、why、how黄金圈法则或者5why分析法可参考我以前的文章:《代码荣辱观-以运用风格为荣,以随意编码为耻》;想了解SMART原则可参考我以前的文章:《知名互联网公司须要什么样的人才》编程

回到主线:为何是初级境界?我本身也不知道更高级别的境界是有什么,由于本身境界没有达到。可是至少有:刻入骨髓 这一境界。服务器

举个例子:十二年前,有次在大街上走,我走过一个【北京银行】的大门,银行二楼的玻璃哗啦啦掉下来。我知道身后有危险,有不少玻璃落到了距离我身后不到十公分的地方。我当时很镇定的继续向前走。等过了危险区,我很想神经质的大叫。由于走路时再多犹豫2秒,可能脑壳上被扎的全是玻璃。因此走路时时时刻刻都会想着离楼房远一些,而且好像感觉到了本身脑壳被玻璃扎。我理解这能够算做把楼房玻璃很危险的理解刻入骨髓。并发

 

字面理解框架

今天举的这个例子纯粹是技术问题,终于不须要用蹩脚的比喻把事情描述的更难理解来达到脱敏的效果。

what

咱们采用的是dubbo服务,这是个稳定成熟的RPC框架。可是咱们在某些应用中会发现,只要这个应用一发布(或者重启),就会出现请求超时的问题,以下图所示:

并且都是第一笔请求会报错,以后就再也没有问题了。

why

我当时很快就定位了问题,由于在内网wiki上、技术博客上,不少人都写了这个坑。因此不讲排查思路了,直接讲结论:

在server端链接数过多, linux系统有个链接队列溢出了。溢出的链接被丢弃,可是client端不知道,仍然给此server发送消息。链接没有创建天然发送不成功。client发第一笔消息超时,至关于探活失败,client端因而从新创建链接。链接成功创建后开始正常的通讯,因此后面都成功了。

how

怎么来解决这个问题呢?四个思路。

第一个是队列溢出了,那就说明队列过小。能够把队列值改大。dubbo使用的是一个写死的默认值:50。能够修改dubbo源码把值改大或者干脆动态获取队列值。

第二个是队列数不变,实际链接数减小。减小server端的链接方,好比有些client端其实没有实际业务调用这个server端了,就双方聊聊把无用的依赖去掉。

第三个是可让服务端在丢弃链接的同时给client端通知一下,linux有个系统参数/proc/sys/net/ipv4/tcp_abort_on_overflow,默认为0。不会给client端发通知,可是设置为1时会给server端发一个reset请求,客户端收到会重连。

第四个是让client端定时心跳探测。探测发现超时了立刻重连,超时的那笔只是探测请求,不影响业务。

 

来龙去脉

做为软件工程师,重要的一个软素质是批判性思惟。多问几个问题,找到答案,理解就能更进一步。

Q1: 提到溢出的队列究竟是什么队列?

A: 下图是TCP链接三次握手的示意图。

 

一次握手:

一开始client端和server端都处于closed状态(未创建链接状态)。client端主动向server端发起syn请求创建链接请求,server端收到后将与client端的链接设置为listen状态(半链接状态)。问题来了,server端怎么保存与client端的状态呢?总须要有地方存呀,存的地方就是队列。链接队列又叫backlog队列。到这里,server端与client端的半链接创建了。这里的backlog队列也叫半链接队列。

二次握手:

server端返回ack应答+syn请求给client,意思是:ack我收到了你的请求,syn你收到个人了没?client端收到server端响应,将本身的状态设置为established状态(链接状态)。

三次握手:

client向server端发送一个ack响应,告诉server端收到。而后server端收到后将与client端的链接设置为established状态(全链接状态)。一样,全链接状态在server端也须要一个backlog队列存储。这里的backlog队列也叫全链接队列。

Q2: backlog队列究竟是全链接队列仍是半链接队列?

A: 这个问题让我想起别的事情。我大学是东北大学,有次看到校内论坛上有个帖子:“东北大学和东南大学谁更有资格叫东大?”最终没啥结论,东北大学内网再论证本身该叫东大,东南大学内网确定不认。

可是backlog的问题仍是有达成共识的可能的。backlog实际上是一个链接队列,在Linux内核2.2以前,backlog包括半链接状态和全链接状态两种队列。在Linux内核2.2以后,分离为两个backlog来分别限制半链接(SYN_RCVD状态)队列大小和全链接(ESTABLISHED状态)队列大小。

半链接队列:

队列长度由/proc/sys/net/ipv4/tcp_max_syn_backlog指定,默认为2048。

全链接队列:

队列长度由/proc/sys/net/core/somaxconn和使用listen函数时传入的参数,两者取最小值。默认为128。

在Linux内核2.4.25以前,是写死在代码常量 SOMAXCONN ,在Linux内核2.4.25以后,在配置文件/proc/sys/net/core/somaxconn中直接修改,或者在 /etc/sysctl.conf 中配置 net.core.somaxconn = 128 。

想到这里我恍然大悟,东北大学、东方大学、东南大学在本身的地盘都有资格简称东大(这里讲这个插曲是为了澄清一件事情:我昨天下午4点发的文章里标题是一个北大妹子,那篇文章是帮朋友的忙,北大妹子不是我,我是东大妹子)。

Q3: 究竟是全链接队列仍是半链接队列溢出致使了超时?

A: server端与client端进行二次握手的前提是server端认为本身与client创建链接是没有任何问题的。若是server端半链接队列溢出了,本身这边都没有处于半链接状态,天然不会发送ack+syn给client端。client端作的应该是从新尝试创建链接,不是发送数据。请求会发送到已经创建好链接的server端(server端是多机器多活部署的)不会形成请求超时。

而二次握手一旦完成,进行三次握手时,若是全链接队列已满,服务器收到客户端发来的ACK, 不会将该链接的状态从SYN_RCVD变为ESTABLISHED。可是客户端已经认为链接创建好了开始发送数据了,这时候是有可能形成超时的。

Q4: 全链接队列满了以后server端是怎么处理的呢?

当全链接队列已满时,则根据 tcp_abort_on_overflow 的值来执行相应动做。

tcp_abort_on_overflow = 0 处理:

则服务器创建该链接的定时器,这个定时器是一个服务器的规则是重新发送syn+ack的时间间隔成倍的增长,好比重新了第二次握手,进行了5次,这五次的时间分别是 1s, 2s,4s,8s,16s,这种倍数规则叫“二进制指数退让”(binary exponential backoff)。

给客户端定时重新发回SYN+ACK即从新进行第二次握手,(若是客户端设定的超时时间比较短就很容易出现异常)服务器从新进行第二次握手的次数由/proc/sys/net/ipv4/tcp_synack_retries 这个linux系统参数决定。

tcp_abort_on_overflow = 1 处理:

当 tcp_abort_on_overflow 等于1 时,发送一个reset请求重置链接。客户端收到能够尝试再次从第一次握手开始创建链接或者其余处理。

Q5: 怎么验证确实是backlog队列溢出呢?

ss 是 Socket Statistics 的缩写。ss 命令能够用来获取 socket 统计信息。ss -l 是显示listen状态的数据,以下所示:

[root@localhost ~]# ss -lState       Recv-Q Send-Q        Local Address:Port Peer Address:Port LISTEN      0      128                       *:http                         *:*       LISTEN      0      128                       :::ssh                        :::*       LISTEN      0      128                        *:ssh                         *:*       LISTEN      0      100                     ::1:smtp                        :::*       LISTEN      0      100               127.0.0.1:smtp                         *:*       

在LISTEN状态,其中 Send-Q 即为全链接队列的最大值,Recv-Q 则表示全链接队列中等待被server段处理的数量。数量为0,说明处理能力很够;Send-Q =Recv-Q ,满了,再来就丢弃掉了。

可是这是一个实时的数据,一段时间有拥塞,过一下子就行了怎么查呢?

可使用netstat -s 能够查看被全链接队列丢弃的数据。

[root@localhost ~]# netstat -s | grep "times the listen queue of a socket overflowed"35552 times the listen queue of a socket overflowed

补充说明: 半链接队列不少文章叫作SYN QUEUE队列。全链接队列不少文章叫作ACCEPT QUEUE队列。这是一些研究linux源码的同窗根据源码的命名来叫的。

 

选择最优

马云说:“选择比努力重要” 。懂得三境界,第三境界的重点不是懂,而是得。最终要根据懂了的内容决策出最优方案。

除了字面理解里提到的四种思路,来龙去脉里还提到了从新进行第二次握手的次数由/proc/sys/net/ipv4/tcp_synack_retries 这个linux系统参数决定。

分别来分析一下各个方案的可行性和优缺点:

方案1:把队列值调大

这个队列值是指全链接队列,调大以后,client端的二次握手就在这个队列里排队等待server端真正创建链接。假设队列值调到上限65535。第65535号请求在排队的过程,client端是established状态,数据可能会发送过来,服务端尚未established状态,还不能处理。

到何时能处理呢?65535个请求所有处理完须要13s的样子。对通常的服务来讲妥妥的超时。因此nginx和redis都是使用的511,让响应时间在100ms内完成。

方案2:减小链接数

只要能减小的下来,这是理想的法子。如今server端都过载了,可想而知,接入的client端再也不少数,推进他们一个个去梳理和改造,就算你们执行力很强,把改下的下了。可想而知,废弃的也通常不会有多少。不展开了啊,如今已经三千多字了,争取五千字内结束。

还有没有别的方法减小链接数呢?最简单的就是使用分治法。

划分子集

跟同事讨论请教的时候,他给我提供了一个划分子集的思路。让client端只和server端一部分服务器创建链接。有两种分配谁跟谁链接的算法,一个是随机算法。可是server端服务器我最常见过几千台组成一个集群的。对随机(虽然链接数是服务器台数的n倍)来讲,样本是不多的,会很不均匀;另一个是肯定性算法,思路也很简单。链接的client端及数量是肯定的,那就排个序,按照server端数量分配一下。这样链接数是均匀的,可是就没办法作到请求级别的流量均匀。

粘滞链接

尽量让客户端老是向同一提供者发起调用,除非该提供者挂了,再连另外一台。<dubbo:protocol name="dubbo" sticky="true" />。若是每一个client端都只和一个server端创建链接。那server端压力就是原来的(1/机器台数)。不够加机器就好了,横向可扩展。

这种作法最大的问题是高可用和并发请求的问题,对于可用性要求不高、请求量不高的服务(好比后台定时任务定时拉取可重试)实际上是能够用的。可是这须要client端的自觉性,而对维护这个client端的人员来说,他们自身是没有好处的,由于本来也就是只是重启时发生一次超时嘛。因此客户端在能够的状况下愿不肯意这样作就看格局了。

方案3:服务端通知

服务端通知上面来龙去脉中有提到能够设置

/proc/sys/net/ipv4/tcp_synack_retries

从新进行几回进行第二次握手。可是这个阶段,client端可能会发数据包过来形成超时;另外,能够设置

/proc/sys/net/ipv4/tcp_abort_on_overflow=1

整个握手直接断掉,client端是closed状态,它会找其余established状态的链接进行数据包发送,不会形成超时。事实上,调研了一些大厂,

tcp_abort_on_overflow=1是做为默认配置的。

方案4:客户端探测

客户端探测想本身作的话比较麻烦,好比说把,客户端调了n个服务,每一个服务创建了n个链接。资源开销大,还必需要复用这些已经创建的链接,复杂度高。

其实provider 和consumer 有双向心跳(探测)的,那为何没检测出并进行重连?

这个首先面临的问题:client端认为链接成功了,但server端认为没有成功。那么server端 是不会发送心跳给 client端的。

client端是否是应该发心跳给server端呢?是的,原来使用dubbo2.5.3版本时3分钟client端会发送一个探测,以后把问题链接closed掉。只是dubbo 2.6.9使用了netty4。他们强强联手搞出来一个bug,探测机制楞没生效!

心跳有个条件,就是lastRead 和 lastWrite 不为空。那就须要看哪里设置了这两个参数。经过代码查到client端链接成功和server端链接成功的时候都会设置。这里只考虑client端状况,对比netty3发现netty4里少了

NettyServerHandler的handler链处理。这个handler链处理就是用来初始化那两个值的。

除了改client端源码,有没有别的方法让client端探测生效呢?其实什么都不用TCP就有keepalive(探活)机制。默认是7200秒,也就是2小时。能够修改:

/proc/sys/net/ipv4/tcp_keepalive_time 

单位是秒

好了,解决问题的方法就讲到这里,完结撒花~~

 

咦,说好的SMART原则呢?

S表明具体的(Specific)

M表明可衡量的(Measurable)

A表明可达到的(Attainable)

R表明与最终目标是相关的(Relevant) 

T表明有明确的截止期限(Time-bound)

编程一辈子,公众号:编程一辈子知名互联网公司须要什么样的人才

方案这么多,哪一种是最好的呢?看场景。方案4提到了问题实际上是开源组件有bug致使。但改开源组件,看公司规划、开源社区支持,A可行性上有制约;

方案2涉及不少整改和推进,T时效上有制约。方案虽多,排除法排除一下能剩下一个就不错了。