记一次连接池错误

背景

最近有一个服务出现了特别诡异的问题,在并发高的时候,客户端线程T-A发出请求Req-A,线程T-B发出请求Req-B,最后可能是T-B收到响应Res-A。在并发很低的时候,不会出现这个问题。

服务端和客户端信息如下:

服务端:

语言:java
协议:thrift
服务:THsHaServer
部署环境:docker
高峰QPS:3w

客户端:

语言:java
部署环境:docker
访问方式:连接池来保持多个长连接
并发:多线程

问题排查

当时发现了返回的结果和预期不符合之后,首先认为是服务端的问题,通过打印(入参,执行结果)来验证服务端逻辑是否正确,但是发现服务端计算逻辑没有任何问题。

然后客户端打印(请求参数,结果),写了一个python脚本统计了一下结果,发现了开头的现象。这就很诡异了,一时没有什么思路。

我们组另外一位大哥就逐步浏览代码了,然后发现请求在发生超时异常时,并没有将这个链接销毁,觉得问题可能是这里导致的,然后验证了下,果然是的。。。

我们通过下图看下这是如何导致问题发生的。

  1. t1时刻发送了ReqA
  2. t2时刻由于ReqA超时,而放弃了tcp连接,但这个时候还响应A还没被客户端接收到
  3. t3时刻另外一个线程从连接池里获取了这个被放弃的tcp连接,发送出请求ReqB
  4. t4时刻ResA先于ResB被client接收到,问题发生
  5. t5时刻ResB回到家,发现物是人非

当时没有根据问题的现象来往这方面想,是因为对tcp全双工和半双工不熟悉导致的。

解决方案

找到问题根源,解决起来就非常简单了。在等待结果的时候,如果发生异常,就销毁这个连接。

引申

这个问题还能再引申到tcp的全双工和半双工问题上。

  • 全双工指的是同一个tcp连接其中的任何一端均可以同时的发送请求和接收结果,且不会发生本篇文章中的问题。http2使用的是这个协议。

  • 半双工指的是tcp连接在任何一个时刻,其中一端在发出请求之后,必须等到结果之后才能再重新发送请求。大部分的网络协议均使用的是半双工。

评论