拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 懂得三境界-使用dubbo时请求超过问题

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

白鹭 - 2022-03-11 2169 0 0

引子

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

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

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

2>前因后果-5why

3>选择最优-SMART

想了解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时效上有制约,方案虽多,排除法排除一下能剩下一个就不错了,

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *