DBProxy连接TCP死链重连问题
5 min read

DBProxy连接TCP死链重连问题

一.问题描述

我们经常会在线上遇到一些数据库抖动问题,现象很明显:

  1. 普遍报错信息携带 Socket Rest 关键字
  2. 报错后问题能快速恢复,但是报错总量与实际业务吞吐量程正相关
  3. 报错期间数据库连接池通常会打满,出现连接池满的错误日志

经过故障期间的线上操作时序分析,我们定位出这类问题与敝司内的一个dbproxy产品有关,该产品具有一些比较渣男的惰性断链特性

二.问题剖析

该问题具体的交互时序如下:

当DBProxy进行配置relead(配置更新、白名单同步或者其他数据库连接信息更新)的时候;

1. 对应流程1→ 2: DBProxy会在当前请求RT回包之后进行连接重置,连接重置之后,DBProxy并没有执行TCP 四次挥手的操作(这里形成了一个死链);

2. 对应流程2→ 3: 连接重置后,由于客户端未感知到该连接断开,仍然继续发送请求(对应REQ2),这时的现象是REQ2会超时未回包,用户态就是一个超时未回包的Socket Timeout错误,然后被客户端组件包装为 Connection Rest异常;

3. 对应动作3: 当客户端感知到该异常时,会重置该连接(从连接池中移除该无效连接,然后创建一个新的数据库连接);

4. 对应流程3以后: 当连接重建后,数据库连接池就能够恢复正常工作了(得益于3的连接重置)

三.问题解法

1. 开启testOnBorrow

我们是否在每次请求前,可以向dbproxy咨询该链接是否为健康状态,这样我们就不会傻傻地像RT2一样等到超时才会重置连接了;

比较幸运的是,DataSource连接池标准实现中有这个testOnBorrow属性能帮助我们做到这个;

但是该属性开启会导致dbproxy的请求吞吐量翻倍(每次请求前都需要回包一个心跳响应)

这样的解决方案比较适合流量较小的服务,牺牲一部分性能来换取绝对无损的解法

Druid的链接可用性验证
从池中重新获取一个有效的连接过程,每次获取前都需要进行一次可用性验证

2. 在故障发生时开启testOnBorrow

这种解决思路关键点在于: 当断链发生(捕获到具体异常)时,我们假定当前应用链接是全部不可用的,因此需要开启心跳检测; 而只要第一时间开启了心跳检测,就能保障接下来一段时间内所有请求链接都是必定可用的(如果不可用会重建联后发送);

为了平衡性能损耗问题,我们很容易想到一个思路:

  • 在未出现故障的时候不进行链接校验
  • 在出现故障的第一时间开启校验,并在一切恢复后的一段时间后关闭校验
我们在 Druid Filter 中隐藏的惊喜策略

四.一些试验佐证

1.开启testOnBorrow的性能损耗

1wQPS的压测性能分析, 4c8g容器cpu损耗多出约20%

2.引入错误熔断切面的试验测试

引入拦截面后,整体耗时抖动影响较小,能很好地解决数据库连接池打满的故障

对照组

错误数量

最长持续时间

未引入熔断插件500依赖testWhileIdle设置值,testWhileIdle为3秒那么久会持续3秒钟
引入熔断插件22一瞬间,只要一瞬的错误值达到5,错误就会消失