码出高效:Java开发手册-第1章(6)

1.5.4 TCP 断开连接

TCP 是全双工通信,双方都能作为数据的发送方和接收方,但TCP 连接也会有断开的时候。所谓相爱容易分手难,建立连接只有三次,而挥手断开则需要四次,如图1-24 所示。A 机器想要关闭连接,则待本方数据发送完毕后,传递FIN 信号给B机器。B 机器应答ACK,告诉A 机器可以断开,但是需要等B 机器处理完数据,再主动给A 机器发送FIN 信号。这时,A 机器处于半关闭状态(FIN_WAIT_2),无法再发送新的数据。B 机器做好连接关闭前的准备工作后,发送FIN 给A 机器,此时B 机器也进入半关闭状态(CLOSE_WAIT)。A 机器发送针对B 机器FIN 的ACK 后,进入TIME_WAIT 状态,经过2MSL(Maximum Segment Lifetime)后,没有收到B机器传来的报文,则确定B 机器已经收到A 机器最后发送的ACK 指令,此时TCP连接正式释放。具体释放步骤如图1-24 所示。一般来说,MSL 大于TTL 衰减至0 的时间。在RFC793 中规定MSL 为2 分钟。

码出高效:Java开发手册-第1章(6)

图1-24 TCP 四次挥手断开连接

通过抓包分析,如图1-25 所示红色箭头表示B 机器已经清理好现场,并发送FIN+ACK。注意,B 机器主动发送的两次ACK 应答的都是81,第一次进入CLOSE_WAIT 状态,第二次应答进入LAST_ACK 状态,表示可以断开连接,在绿色箭头处,A 机器应答的就是Seq=81。

码出高效:Java开发手册-第1章(6)

图1-25 TCP 四次挥手抓包分析

四次挥手断开连接用通俗的说法可以形象化地这样描述。

男生:我们分手吧。

女生:好的,我的东西收拾完,发信息给你。(此时男生不能再拥抱女生了 。)

(1 个小时后)

女生:我收拾好了,分手吧。(此时,女生也不能再拥抱男生了。)

男生:好的。(此时,双方约定两个月过渡期后,才可以分别找新的对象。)

图1-24 中的红色字体所示的TIME_WAIT 和CLOSE_WAIT 分别表示主动关闭和被动关闭产生的阶段性状态,如果在线上服务器大量出现这两种状态,就会加重机器负载,也会影响有效连接的创建,因此需要进行有针对性的调优处理。

    • TIME_WAIT:主动要求关闭的机器表示收到了对方的 FIN 报文,并发送出了ACK 报文,进入TIME_WAIT 状态,等2MSL 后即可进入到CLOSED 状态。如果FIN_WAIT_1 状态下,同时收到带FIN 标志和ACK 标志的报文时,可以直接进入TIME_WAIT 状态,而无须经过FIN_WAIT_2 状态。
    • CLOSE_WAIT:被动要求关闭的机器收到对方请求关闭连接的 FIN 报文,在第一次ACK 应答后,马上进入CLOSE_WAIT 状态。这种状态其实表示在等待关闭,并且通知应用程序发送剩余数据,处理现场信息,关闭相关资源。

在TIME_WAIT 等待的2MSL 是报文在网络上生存的最长时间,超过阈值报文则被丢弃。但是在当前的高速网络中,2 分钟的等待时间会造成资源的极大浪费,在高并发服务器上通常会使用更小的值。既然TIME_WAIT 貌似是百害而无一利的,为何不直接关闭,进入CLOSED 状态呢?原因有如下两点。

第一,确认被动关闭方能够顺利进入CLOSED 状态。如图1-24 所示,假如最后一个ACK 由于网络原因导致无法到达B 机器,处于LAST_ACK 的B 机器通常“自信”地以为对方没有收到自己的FIN+ACK 报文,所以会重发。A 机器收到第二次的FIN+ACK 报文,会重发一次ACK,并且重新计时。如果A 机器收到B 机器的FIN+ACK 报文后,发送一个ACK 给B 机器,就“自私”地立马进入CLOSED 状态,可能会导致B 机器无法确保收到最后的ACK 指令,也无法进入CLOSED 状态。这是A 机器不负责任的表现。

第二,防止失效请求。这样做是为了防止已失效连接的请求数据包与正常连接的请求数据包混淆而发生异常。

因为TIME_WAIT 状态无法真正释放句柄资源,在此期间,Socket 中使用的本地端口在默认情况下不能再被使用。该限制对于客户端机器来说是无所谓的,但对于高并发服务器来说,会极大地限制有效连接的创建数量,成为性能瓶颈。所以,建议将高并发服务器TIME_WAIT 超时时间调小。

在服务器上通过变更/etc/sysctl.conf 文件来修改该省略值( 秒):net.ipv4.tcp_fin_timeout = 30(建议小于30 秒为宜)。

修改完之后执行/sbin/sysctl -p 让参数生效即可。可以通过如下命令:

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a,S[a]}'​查看各连接状态的计数情况,为了使数据快速生效,2MSL 从240 秒更改为5 秒。参数生效后如图1-26 所示,TIME_WAIT 很快从75 个降为1 个。

码出高效:Java开发手册-第1章(6)

图1-26 各种TCP 状态的计数

在sysctl.conf 中还有其他连接参数也用来不断地调优服务器TCP 连接能力,以提升服务器的有效利用率。毕竟现代网络和路由处理能力越来越强,跨国时延通常也在1 秒钟以内,丢包率极低。如何快速地使连接资源被释放和复用,参数的优化往往可以取得事半功倍的效果。记得某大公司在大型购物节时,系统宕机,老总下令要加一倍服务器来解决问题。事实上,如果是参数配置错误导致的系统宕机,即使增加硬件资源,也无法达到好的效果。硬件的增加与性能的提升绝对不是线性相关的,更多的时候是对数曲线关系。

TIME_WAIT 是挥手四次断开连接的尾声,如果此状态连接过多,则可以通过优化服务器参数得到解决。如果不是对方连接的异常,一般不会出现连接无法关闭的情况。但是CLOSE_WAIT 过多很可能是程序自身的问题,比如在对方关闭连接后,程序没有检测到,或者忘记自己关闭连接。在某次故障中,外部请求出现超时的情况,当时的Apache 服务器使用的是默认的配置方式,通过命令:netstat -ant|grep-i "443"|grep CLOSE_WAIT|wc -l 发现在HTTPS 的443 端口上堆积了2.1 万个左右的CLOSE_WAIT 状态。经排查发现,原来是某程序处理完业务逻辑之后没有释放流操作,但程序一直运行正常,直到运营活动时才大量触发该业务逻辑,最终导致故障的产生。

1.5.5 连接池

我们使用连接来进行系统间的交互,如何管理成千上万的连接呢?服务器可以快速创建和断开连接,但对于高并发的后台服务器而言,连接的频繁创建与断开,是非常重的负担。就好像我们正在紧急处理线上故障,给同事打电话一起定位问题时,一般情况下都不会挂断电话,直到问题解决。在时间极度紧张的情况下,频繁地拨打和接听电话会降低处理问题的效率。在客户端与服务端之间可以事先创建若干连接并提前放置在连接池中,需要时可以从连接池直接获取,数据传输完成后,将连接归还至连接池中,从而减少频繁创建和释放连接所造成的开销。例如,RPC 服务集群的注册中心与服务提供方、消费方之间,消息服务集群的缓存服务器和消费者服务器之间,应用后台服务器和数据库之间,都会使用连接池来提升性能。

重点提一下数据库连接池,连接资源在数据库端是一种非常关键且有限的系统资源。连接过多往往会严重影响数据库性能。数据库连接池负责分配、管理和释放连接,这是一种以内存空间换取时间的策略,能够明显地提升数据库操作的性能。但如果数据库连接管理不善,也会影响到整个应用集群的吞吐量。连接池配置错误加上慢SQL,就像屋漏偏逢连夜雨,可以瞬间让一个系统进入服务超时假死宕机状态。

如何合理地创建、管理、断开连接呢?以Druid 为例,Druid 是阿里巴巴的一个数据库连接池开源框架,准确来说它不仅仅包括数据库连接池,还提供了强大的监控和扩展功能。当应用启动时,连接池初始化最小连接数(MIN);当外部请求到达时,直接使用空闲连接即可。假如并发数达到最大(MAX),则需要等待,直到超时。如果一直未拿到连接,就会抛出异常。

如果MIN 过小,可能会出现过多请求排队等待获取连接;如果MIN 过大,会造成资源浪费。如果MAX 过小,则峰值情况下仍有很多请求处于等待状态;如果MAX 过大,可能导致数据库连接被占满,大量请求超时,进而影响其他应用,引发服务器连环雪崩。在实际业务中,假如数据库配置的MAX 是100,一个请求10ms,则最大能够处理10000QPS。增大连接数,有可能会超过单台服务器的正常负载能力。另外,连接数的创建是受到服务器操作系统的fd(文件描述符)数量限制的。创建更多的活跃连接,就需要消耗更多的fd,系统默认单个进程可同时拥有1024 个fd,该值虽然可以适当调整,但如果无限制地增加,会导致服务器在fd 的维护和切换上消耗过多的精力,从而降低应用吞吐量。

懒惰是人的天性,有时候开发工程师为了图省事还会不依不饶地要求调长Timeout 时间,如果这个数值过大,对于调用端来说也是不可接受的。如果应用服务器超时,前台已经失败返回,但是后台仍然在没有意义地重试,并且还有新的处理请求不断堆积,最终导致服务器崩溃。这明显是不合理的。所以在双十一的场景里,应用服务器的全链路上不论是连接池的峰值处理,还是应用之间的调用频率,都会有相关的限流措施和降级预案。

图1-27 所示的是某连接池的监控图。图中连接池最小的连接数是2,一个线程就是一个活跃连接。一般可以把连接池的最大连接数设置在30 个左右,理论上还可以设置更大的值,但是DBA 一般不会允许,因为往往只有出现了慢SQL,才需要使用更多的连接数。这时候通常需要优化应用层逻辑或者创建数据库索引,而不是一味地采用加大连接数这种治标不治本的做法。极端情况下甚至会导致数据库服务不响应,进而影响其他业务。

码出高效:Java开发手册-第1章(6)

图1-27 连接池的监控图

从经验上来看,在数据库层面的请求应答时间必须在100ms 以内,秒级的SQL查询通常存在巨大的性能提升空间,有如下应对方案:

(1)建立高效且合适的索引。索引谁都可以建,但要想建好难度极大。因为索引既有数据特征,又有业务特征,数据量的变化会影响索引的选择,业务特点不一样,索引的优化思路也不一样。通常某个字段平时不用,但是某种触发场景下命中“索引缺失”的字段会导致查询瞬间变慢。所以,要事先明确业务场景,建立合适的索引。

(2)排查连接资源未显式关闭的情形。要特别注意在ThreadLocal 或流式计算中使用数据库连接的地方。

(3)合并短的请求。根据CPU 的空间局部性原理,对于相近的数据,CPU 会一起提取到内存中。另外,合并请求也可以有效减少连接的次数。

(4)合理拆分多个表join 的SQL,若是超过三个表则禁止join。如果表结构建得不合理,应用逻辑处理不当,业务模型抽象有问题,那么三表join 的数据量由于笛卡儿积操作会呈几何级数增加,所以不推荐这样的做法。另外,对于需要join 的字段,数据类型应保持绝对一致。多表关联查询时,应确保被关联的字段要有索引。

(5)使用临时表。某种情况下,该方法是一种比较好的选择。曾经遇到一个场景不使用临时表需要执行1 个多小时,使用临时表可以降低至2 分钟以内。因为在不断的嵌套查询中,已经无法很好地利用现有的索引提升查询效率,所以把中间结果保存到临时表,然后重建索引,再通过临时表进行后续的数据操作。

(6)应用层优化。包括进行数据结构优化、并发多线程改造等。

(7)改用其他数据库。因为不同数据库针对的业务场景是不同的,比如Cassandra、MongoDB。

上一篇:阿里巴巴 JAVA 开发手册


下一篇:JAVA学习收获