Cobar死锁问题
Posted 点我达技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Cobar死锁问题相关的知识,希望对你有一定的参考价值。
题外话
使用Cobar
将近一年了,但对其原理仍旧不是很了解,更没阅读过源码,说起来也是惭愧。趁着最近线上的一次故障,总算说服自己花时间来看看Cobar
的真面目。
线上故障
故障现场的Cobar
完全就处于一个近似于僵死的状态,并且在底层mysql
层面捕捉到了锁:多个session
长时间的在等待某个行级锁(row-level locking)直到超时(锁超时时间是50s)。从该SQL我们定位到某个业务接口,并且发现该接口会被并发调用,这个并发调用会更新同一个人的账户余额,简化成SQL
类似于
update `user` set balance = balance - 5 where id = 1;
当时的并发度大概在30+左右,应用有大量更新这条记录的等待锁超时异常爆出。并且当时的现象是,其余访问cobar
的线程也被阻塞。
临时方案
线上故障在无法立即找出原因并解决的情况下,我们必须要有紧急预案或者说是临时方案。当时已经定位到业务接口,该接口是由于某个定时任务去并行得对某个用户的账号做扣罚。于是我们将该扣罚金额参数调整成了0,至此,Cobar
恢复正常。
抛出疑问
当然,临时方案只是为了暂时先恢复正常运营。下面的工作就要找出问题真正的原因。
针对上面的故障场景,当时产生了几点疑问:
- 为什么某条Sql会占用id为1的行锁超过50s甚至更长?
- 为什么id为1的行锁占用会影响到整个
Cobar
的服务?
我们留着这两点疑问先慢慢往下看。
场景还原
故障之后有同事在重现该场景并寻找原因,根据数据源和事务提交方式的不同,在并发度为500的情况下,分别测试了如下几种场景:
只有在使用Cobar
并采用手动提交事务的情况下,才会出现Cobar
僵死的情况。这个测试模拟了一个最简单并且足以说明问题的场景,单表一条主键id为1的记录,对其做update
操作。更不用说并发500了,30+的情况也完全能把Cobar
给搞挂。
原理探究
到这里大家可能就开始质疑Cobar
了,真的是Cobar
的并发低到只有30+?
带着这样的质疑,我踏上了一条为Cobar
正名的“不归路”。通过官方文档以及Cobar
的源代码,梳理出了Cobar
的大致结构,下面是简化后的结构图:
可以看到,Cobar
实现了Mysql
协议,伪装成Mysql
服务端与我们的应用进行通讯,这样我们的应用就可以像直连Mysql
一样操作Cobar
了。应用与Cobar
建立连接,然后通过Mysql
协议将请求发到Cobar
,Cobar
解析报文然后根据命令的不同执行不同的操作。其中涉及到两个线程池,如上图所示,在Cobar
中命名这两个线程池为:Handler
和Executor
:Handler
的主要工作是读取数据流,解析报文,处理对应命令,路由计算等。而具体要和底层的Mysql
打交道的工作就交给Executor
来完成了。我们来举个简单的例子,就拿前面的update
语句来简单分析一下(建立连接这块先不说):
update `user` set balance = balance - 5 where id = 1;
假设应用和Cobar
之间已经建立起了连接,那么Cobar
就开始从应用读取数据流,一旦读到数据流,那么Cobar
会简单处理数据包,待获得一个完整的数据包之后将此数据包打包成一个任务丢给Handler
去执行。该任务的内容包含:
- 根据
Mysql
协议来解析数据包(枯燥的过程,例如包头4个字节,第五个字节代表具体的命令类型,诸如此类的东西) - 通过第一步可以解析出命令类型以及具体的SQL,下面以Query这种命令类型为例,这也是最常用的(这里的查询不局限select,crud都属于Query)
- 根据SQL进行路由计算,针对于单节点和多节点处理略有不同
最后将携带了路由信息的数据包打包成任务丢到Executor
中去执行,这块任务要做的是:
- 根据路由信息关联
Mysql
通道 - 针对该会话绑定
Mysql
通道,主要是为了关联事务 - 发送数据包到
Mysql
并等待返回结果
提出猜想
通过上面的分析,再来想想之前提出过的两点疑问:
- 为什么某条Sql会占用id为1的行锁超过50s甚至更长?
- 为什么id为1的行锁占用会影响到整个
Cobar
的服务?
这边先来简单普及一个知识点,一般的手动事务需要三个步骤:
- set autocommit = 0;
- Query Command
- commit/rollback
在正常情况下根据主键update
肯定不可能超过50s,那么只有一种情况,那就是update
操作之后没有commit
。
为什么会没有commit
呢,是不是Executor
被挤满了?
为什么Executor
满了?因为堵满了update
这看起来像不像一个死锁(DeadLock)问题?Executor
中的某条线程(ThreadA)获取了锁,其余大多数线程都在等待该锁。假设此时update
线程足够多并且都因为等待锁而阻塞,进而堵满了Executor
。那么那条获得了锁的线程也就没有空余线程来释放锁了(commit/rollback)。DeadLock!
验证猜想
首先,我们来重现场景,和上面写的场景还原一样,采用500个线程来并发更新一条记录:
update `user` set balance = balance - 5 where id = 1;
不出意料,场景又再现了。此时,我们通过Cobar
提供的管理节点来监控线程池,发现Executor
跑满了,并且队列中还堆积了好多请求:
此时再去底层的Mysql
看看,发现此时大量锁超时:
为了证明线程池里堵得全都是update
,又通过修改Cobar
源码打印出了Executor
中任务执行的日志。至此,问题已经非常清晰,并且同时也解释了为什么在自动提交事务的场景下不会发生堵塞。
那么可以开始考虑解决方案了。
解决方案
加大
Executor
的线程池大小,这应该也是最容易想到的方式。Cobar
会根据CPU
核心数创建N个Executor
,假设将Executor
的线程池大小调整到256,那么理论上可支持对同一条记录的并发操作数可达到 N 256。
优点:改起来非常方便,本身Cobar
配置文件就有暴露该配置项
*缺点:总觉得有点治标不治本的味道,并发量过高还是会有阻塞的危机
当然可以搭配死锁检查或对于线程中执行时间过长的SQL直接Kill并报警等机制调整线程池策略,如配置超大maxSize,也就是保证线程资源管够,这涉及到修改源码,因为Cobar在线程池上并没有暴露扩展点。
再定义一种线程池,单独用来执行commit/rollback命令
优点:将资源隔离,可以从本质上来解决死锁问题
缺点:需要改动Cobar
源代码,可能需要经过一轮全面的测试才能使用到生产环境
以上三种方式都可以达到想要的效果。个人倾向第三种,因为将资源隔离,才是从本质上解决问题。另外,修改的代码成功通过了Cobar 168个单测用例,并且很荣幸,这部分代码已经被合并至官方仓库。所以,放心大胆的使用吧。
结束语
感觉这个问题算是Cobar
隐藏的一个BUG吧。可能在设计之初并没有考虑到一些对同一条记录高并发的更新场景。网上也没有太多关于这方面的文章。这篇文章算是一个探路者,为我之后研究开源中间件开一个好头~
以上是关于Cobar死锁问题的主要内容,如果未能解决你的问题,请参考以下文章