项目亮点:集群部署挑战!深入理解TCP!
问题抛出
当我们在服务发布的时候,一上线有少量的请求会报失败,毛估在发布的这一个短暂的时间,可能也就几秒钟,有10%左右的失败率,问题的关键是过了几秒钟之后,失败率又下来了,所以给人感觉就是在发布的过程中,并不能保证一个发布的平顺性,在一些流量不是很大的系统,这个失败率可能不是很大,但如果系统的流量非常的大,每秒有上万qps,而且如果是写入的流量的话,并且没有做这种分布式事务的控制,那就有可能造成两边数据的不一致,这也就是我们面试中常问到的问题:就是请求超时的话,会有什么问题,其实很多时候就是因为发布引起的这个原因。所以当时我负责的这个系统就不得不去解决这个问题。
不知道大家是否了解“惊群效应”,在某一个瞬间,流量突然过来之后会造成一些资源的争抢,并不是说这个机器不能承载这样的流量,而是流量来得很快,才会导致这样的现象。造成这样的原因其实有很多,比如:Java的即时编译,或者缓存没有预热,或者有一些其他的资源本来就是懒加载的,最开始几个请求过来的话,是非常消耗资源的,导致上游就请求超时了。
我当时排查出来的原因跟底层的TCP有关系。
问题排查
一般我们可以通过查应用程序的日志,甚至Linux的日志,根据报错的时间点去看了一下Dubbo的日志
意思是通道关闭, 一般一个RPC框架服务有三个模块,第一个是注册中心,第二个是Consumer,第三个provider,我们这个日志是从consumer报出来的。
通道关闭之后它就去重连,重连之后就可以去正常的传输请求了,查到这里我就比较疑惑了:为什么会报通道关闭?为什么重连之后就可以正常的请求呢?
搜索Dubbo的日志是在哪一个类里面输出的
又去参考了dubbo的架构图
发现这个类是在TCP这一层去做一个连接的通道,那竟然都涉及到TCP这一层了,那么抛开Dubbo这个日志,在Linux底层,TCP的连接是有日志的。所以我们找到Linux当时打印的日志,发现了一个很重要的信息:
意思就是在某个端口上它发现有这种洪水攻击,大家应该都听说过DDOS,就是分布式拒绝服务式攻击,这个攻击的原理其实也很简单,可能黑客就是用很多这种木马病毒传播出去之后再做成服务的跳板或者代理,朝某一个服务发送请求包,然后导致正常的请求不能响应,但是这里的情况可能不太一样,我们看到SYN
,SYN是什么,我就想到了TCP三次握手的过程中的syn
我们来复习一下tcp的三次握手:
- 客户端–发送带有 SYN 标志的数据包–一次握手–服务端
- 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
- 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手,中间握手失败了会怎么样?具体可参考:http://www.linjsblog.top/archives/161
我相信大家对上面的内容一定非常熟悉,但是针对于这种问题的原因是什么呢?我们详细的来看一下:
整个服务端其实是有两个队列的,一个叫同步队列(半连接队列),另外一个叫accept 队列(全连接队列)
什么是半连接队列:就是这个请求还没有完成建立连接,只是做了一个收发,三次握手就是要保证服务端和客户端它的收发功能都是正常的,第二次握手之后其实服务端不知道自己发送的是否正常,所以服务端先将客户端的连接放到一个半连接队列里面,只有第三次握手结束后,才能保证连接是一个全双工的正常状态,然后再把半连接队列移动到全连接队列(Accept queue)里面.
如果这个时候全连接队列已经满了,该怎么办呢?有两种策略:让客户端重试(RST),或者说告诉客户端请求已经满了,你不用再请求了,你可以通过负载均衡去调到其他节点,就不要掉我这个机器了。
所以问题的关键就两个:1. 这两个队列的长度是怎么去设置的 2. 队列满的时候,应该采取怎样的策略
其实这个都是有参数的,而且这几个参数设计的还比较复杂,在Linux 不同的版本,甚至它的默认值也不太一样
它会优先使用应用框架传进去的长度: TCP 全连接队列的长度取 net.core.somaxconn
及业务进程调用 listen 时传入的 backlog 参数,两者中的较小值
当这个队列满的时候具体采用哪些策略,我后面也去看了下Linux内核TCP这一块的一些源代码,发现是从客户端这边直接把请求移除掉还是发送RST这种请求让它去重视,这个逻辑还是比较复杂的
解决方案
首先把Netty的代码更新了一下,默认参数是50,把它调大,然后把Linux的几个参数也调大,并且半连接和全连接的长度设置成一样的
// 设置全连接队列,大小为80
new ServerBootstrap().option(ChannelOption.SO_BACKLOG, 80);
之后就再也没有发现这个问题了。 所以,一般网络服务程序(tomcat、redis、mysql等)都会有一个backlog的配置,在springboot内置的tomcat中,backlog配置方法如下:
server:
port: 8080
tomcat:
accept-count: 8192 # 内置tomcat的全连接队列大小配置