破产码农
最近在生产上变更时遇到从机死锁的问题。表现的现象为:同一张的表的DROP TABLE操作进入了同一组。而由于并行复制,并且从机提交顺序需要保证与主机一致,因此产生了死锁。
从上图可以看到SQL语句DROP TABLE IF EXISTS z对应的二进制日志中的last_committed值都为3,也就是他们处于同一组。
若这时发生Worker Thread 2先执行,且要求从机提交顺序与主机一致,那么Worker Thread 2提交前会一直等待Worker Thread 1完成,而由于Worker Thread 2已经执行了DROP TABLE操作,Worker Thread 1无法获得元数据锁去执行删除同样的表z,因此产生经典的AB-BA死锁。
现在要解决这个问题,主要是思考:为什么DROP TABLE IF EXISTS z会进入了同一组提交队列中。
可以肯定的是,这的的确确是一个显而易见的Bug。而由于之前对组提交代码相对比较熟悉,个人第一时间的反应这应该不是组提交那块代码的Bug。
后来同事反馈是由于使用gh-ost进行表结构变更时,工具会执行类似执行LOCK TABLES t WRITE,z WRITE多表锁定语句,从而引起死锁。
在有了上述线索后,就比较好定位死锁问题了。
执行命令LOCK TABLES t WRITE,z WRITE会对表t、表z加上表级别的SHARED_NO_READ_WRITE的元数据锁。
这个元数据锁是在SQL引擎层的,对应源代码mdl.cc,并不是InnoDB引擎层的行记录锁。
接着,尝试测试在执行完上述SQL命令后,接着去执行DRO TABLE IF EXISTS z,这时发现对应表z上的表级别元数据锁已释放。
这也符合预期,毕竟删除表z,同时释放已持有的元数据锁,这是正常逻辑。
然而,在深入分析源码后,发现使用LOCK TABLES命令先去锁表,再去删除表,和普通的删除表操作,两者在释放元数据锁的时间是不一样的。
先来看普通的DROP TABLE操作,其内部的流程如下图所示:
正常的删除操作在完整提交后,才释放元数据锁。
但若通过命令LOCK TABLES现在去锁住两张表,接着再删除其中一张表,如我们演示的表z,则内部的流程如下图所示:
这时会发生在完整提交前就释放了表z的元数据锁。若这时有其他线程执行DROP TABLE IF EXISTS z的操作,则有可能进入到orderd_committed流程,从而导致两个DROP TABLE在同一提交组的可能。
若想要在自己的测试环境中更为容易的模拟出DROP TABLE在同提交一组的环境,同学们可以调整如下参数,:
binlog_group_commit_sync_delay = 1000000
binlog_group_commit_sync_no_delay_count = 8
若是源码调试,可以定位文件sql_table.cc,定位到函数mysql_rm_table设置断点。
发现这个Bug后,姜老师花时间研究看了下源码。其实从代码逻辑上看是没有问题的,只是在从机回放的时候存在问题。所以,从严格意义上看,这个并不算Bug。只能说是一种不完美的缺陷。
然而,对于业务来说,这是的的确确的Bug。从机死锁,主从延迟,会导致各种严重的问题。
若将参数slave_preserve_commit_order设置为0,即不要求主从二进制日志顺序一致,可以规避上述问题,主从数据依然保证一致。但这不是完美的解决方案。
这个Bug的修复还真有些棘手,尝试想了很多方法,总没有特别好的思路。
例如参考之前DROP USER IF EXISTS,如果没有实际表删除,则不记录二进制日志。然而,这个方法并不好,官方最近版本DROP USER IF EXISTS无论是否有删除用户操作,也都记录二进制日志。
同样,若将上述thd->mdl_context.release_all_locks_for_name这行注释掉,那么会存在DROP TABLE z后,表z的元数据锁依然存在,只有当执行完命令UNLOCK TABLES才完全释放元数据锁。
这种方式,虽能避免上述问题,但还是有些实现上的不完美。
这时看了下MySQL 8.0的源码mdl.cc,发现相对5.7做了比较大的重构。扫了下代码,无意中发现多了一种新类型的元数据锁——TABLESPACE元数据锁。
接着测试验证发现8.0并不存在5.7上述的Bug!!!因为DROP TABLE IF EXISTS只释放TABLE元数据锁,并不释放TABLESPACE元数据锁。
那么当线程2执行同样命令时,则由于TABLESPACE元数据存在,会触发等待,因此也不存在同一组中存在相同的DDL。
至于为什么要引入TABLESAPCE的元数据锁,可看官方的Worklog:https://dev.mysql.com/worklog/task/?id=7957
从数据库角度看,这并不是MySQL内核层的死锁Bug。
这是因为使用gh-osc工具,引发的从机并行复制潜在的Bug。从概率上看也并不一定100%可以复现。
在翻看gh-osc工具源码中,也发现已经有用户提出从机死锁问题。官方也计划在1.1版本中修复。主要是让已经删除的表,不再有多次删除的可能。
当然,非要说是内核Bug,其实也是。毕竟二进制日志中last_committed值是错误的,从而引发从机并行回放的死锁。
这个Bug并不好很直观的进行修复。不过在8.0中,由于引入了TABLESAPCE元数据锁,反而弯打正着地解决了这个Bug。
好在这个Bug并非致命,发生概率非常小。若不使用gh-ost,则99%的概率不会遇到。若你有遇到,不妨留言,看看同学们是怎么解决这个的呢?
《MySQL冲冲冲》分库分表 vs 中间件,哪个架构更好?
中国芯片崛起,指日可待
上海,终于有些开窍了......
为什么剩女远多于剩男?一线城市剩女图鉴
老兵不死,他们只会自相残杀?