记一次连接池错误
背景
最近有一个服务出现了特别诡异的问题,在并发高的时候,客户端线程T-A发出请求Req-A,线程T-B发出请求Req-B,最后可能是T-B收到响应Res-A。在并发很低的时候,不会出现这个问题。
服务端和客户端信息如下:
服务端:
语言:java
协议:thrift
服务:THsHaServer
部署环境:docker
高峰QPS:3w
客户端:
语言:java
部署环境:docker
访问方式:连接池来保持多个长连接
并发:多线程
问题排查
当时发现了返回的结果和预期不符合之后,首先认为是服务端的问题,通过打印(入参,执行结果)来验证服务端逻辑是否正确,但是发现服务端计算逻辑没有任何问题。
然后客户端打印(请求参数,结果),写了一个python脚本统计了一下结果,发现了开头的现象。这就很诡异了,一时没有什么思路。
我们组另外一位大哥就逐步浏览代码了,然后发现请求在发生超时异常时,并没有将这个链接销毁,觉得问题可能是这里导致的,然后验证了下,果然是的。。。
我们通过下图看下这是如何导致问题发生的。
- t1时刻发送了ReqA
- t2时刻由于ReqA超时,而放弃了tcp连接,但这个时候还响应A还没被客户端接收到
- t3时刻另外一个线程从连接池里获取了这个被放弃的tcp连接,发送出请求ReqB
- t4时刻ResA先于ResB被client接收到,问题发生
- t5时刻ResB回到家,发现物是人非
当时没有根据问题的现象来往这方面想,是因为对tcp全双工和半双工不熟悉导致的。
解决方案
找到问题根源,解决起来就非常简单了。在等待结果的时候,如果发生异常,就销毁这个连接。
引申
这个问题还能再引申到tcp的全双工和半双工问题上。
全双工指的是同一个tcp连接其中的任何一端均可以同时的发送请求和接收结果,且不会发生本篇文章中的问题。http2使用的是这个协议。
半双工指的是tcp连接在任何一个时刻,其中一端在发出请求之后,必须等到结果之后才能再重新发送请求。大部分的网络协议均使用的是半双工。