DBProxy连接TCP死链重连问题
一.问题描述
我们经常会在线上遇到一些数据库抖动问题,现象很明显:
- 普遍报错信息携带 Socket Rest 关键字
- 报错后问题能快速恢复,但是报错总量与实际业务吞吐量程正相关
- 报错期间数据库连接池通常会打满,出现连接池满的错误日志
经过故障期间的线上操作时序分析,我们定位出这类问题与敝司内的一个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,错误就会消失 |