龙空技术网

MySQL 8.0不讲武德:MTS这个定时炸弹,速来排坑!

IT168企业级 332

前言:

而今各位老铁们对“php队列mysql”都比较关切,姐妹们都想要分析一些“php队列mysql”的相关知识。那么小编也在网摘上网罗了一些对于“php队列mysql””的相关资讯,希望姐妹们能喜欢,姐妹们快快来了解一下吧!

一、问题描述

8.0.28以下的MTS可能在运行一段时间后出现hang死的情况,大概如下(模拟的):

这个问题因此有一个2的31次方事务后触发的界限,实际上就是int类型的溢出导致,因此为定时炸弹,BUG如下:

这个BUG由印风提交,可以看出来跑了很久才触发,着实费劲,感谢大佬,否则真的很难知道为什么。

二、关于slave_preserve_commit_order的实现

实际上本参数主要控制的是从库的提交顺序和主库一致,其实现大概分为两个步骤

首先是SQL线程分发事务的时候对顺序进行记录,因为分发是按照主库的binlog的顺序分发的因此可以理解为就是seqnumber的顺序。worker线程选择事务执行是无序的,但是在提交的时候就需要保证顺序,这个时候根据SQL线程分发的顺序进行提交即可。

在8.0中主要通过一个全局的Commit_order_manager结构进行记录,其中有2个部分比较重要:

1. 一个全部worker线程信息的vecoter容器,有多少个worker线程容器中就有多少个元素,其中每个元素代表一个worker,叫做node,包含一些重点的成员变量如下:

m_commit_sequence_nr{NO_SEQUENCE_NR} 提交序列信息value_type m_worker_id worker线程的idMDL_context *m_mdl_context 包含MDL LOCK信息memory::Aligned_atomic<Commit_order_queue::enum_worker_stage> m_stage 状态信息

其中m_commit_sequence_nr和m_mdl_context是为唤醒worker线程准备的,这点和5.7不同,5.7中为全部唤醒然后每个worker循环判断,

while (queue_front() != worker->id)

8.0为精准唤醒,每个节点的m_commit_sequence_nr信息来自全局的m_commit_sequence_generator生成器。而m_stage主要由如下一些不同的状态,标记不同的截断。

FINISHED_APPLYING:事务已经应用,但可能没有提交,如果需要等待提交顺序转为REQUESTED_GRANT状态 REQUESTED_GRANT:事务不能提交,需要等到当前运行worker授予MDL LOCK REGISTERED :事务已经注册也就是SQL线程分发了。

另外对于唤醒操作用的应该是MDL LOCK现成的实现。

一个提交队列,这个很显然,当worker线程第一次发现不是自己提交顺序的时候,就是根据这个提交队列来的,其为m_commit_queue,其为一个无锁化队列,暂未对无锁化进行研究,但是对其方法和push,pop,<<,>>等函数的注释可以看出是一个先进先出的队列,比如:

  /**    Retrieves the value at the virtual index pointed by the head of the    queue, clears that position, updates the virtual index stored in the    head and clears the value returned by `get_state()`, setting it to    `SUCCESS`.

其中Retrieves the value at the virtual index pointed by the head of the queue, clears that position,说明了其>>操作的作用,也就是从头部取信息,并且清理这个位置。

其包含的重点步骤有:

SQL线程分发分配注册事务

Commit_order_manager::register_trx    ->cs::apply::Commit_order_queue::push

其中主要完成的步骤为将分配的worker的信息记录到node中,同时根据m_commit_sequence_generator生成器生成一个提交序列(本bug就是和这个生成器有关,重启实际上是重置了这个内存计数器),每个事务都会让这个计数器加1。并且将这个worker的work id 放入m_commit_queue队列中。并且将node的状态置为REGISTERED。

worker线程等待者等待自己的队列到来

Commit_order_manager::wait ->Commit_order_manager::wait_on_graph

当worker现成准备提交的时候,也就是在进入flush队列之前,需要判断自己是否可以提交,这个时候判定的原则是当前worker 的node信息的worker id是否和m_commit_queue队列的头部worker id相同,

 if (this->m_workers.front() != worker->id)

因为前面我们说了,m_commit_queue队列实际上就是记录的根据分发顺序记录的worker id。如果不能提交则进入等待,状态改为REQUESTED_GRANT,这个时候这个worker是需要别的worker唤醒的。

worker线程执行者唤醒等待者

MYSQL_BIN_LOG::change_stage (Commit_stage_manager::BINLOG_FLUSH_STAGE) ->Commit_stage_manager::enroll_for   ->Commit_order_manager::finish_one

注意这里是BINLOG_FLUSH_STAGE,也就是在保证flush队列的顺序,实际上也保证了commit队列的顺序,最终完成唤醒的函数是Commit_order_manager::finish_one,这个唤醒过程主要完成的任务就是唤醒下一个正在等待的worker线程,其重要方式为从m_commit_queue队列的头部拿一个worker id,实际上就是要唤醒的worker,然后通过下面3个条件来判定是否唤醒:

A. 当前有worker线程

B. 通过提交队列头部获取的下一个worker线程,其节点状态为FINISHED_APPLYING或者REQUESTED_GRANT

C. 通过提交队列头部获取的下一个worker线程,其节点的m_commit_sequence_nr信息必须和当前提交事务的worker id的m_commit_sequence_nr+1 相同

A&&B&&C 同时满足才能唤醒下一个应该唤醒worker线程,实际上从现有的分析来看,这个过程不太可能出问题。但是BUG就出现C条件上。

现在我们来画一个图描述这种唤醒方式。假设,当前worker有4个,worker id分别为 0,1,2,3。当前全局计数器来到了2147483640,并且能够并发的GTID的seq number分别为 4147483647,2147483648,2147483649,2147483650,而4147483647为较大的事务,因此在他提交之前其他3个事务不能提交。

然后其中4147483647分配给了worker 2,4147483648分配给了worker 0,4147483649 分配给了 worker 1,4147483650分配给了worker 3。那么当前等待和执行的图如下:

image.png

当GTID 4147483647事务执行完成后,需要唤醒GTID 4147483648事务,那么从m_commit_queue中取下一个workid:0就可以了,但是这里要比如2147483641+1 是否等于2147483642,否则唤醒,也就是C条件,当然这里没有问题。

四、BUG产生

前面我们看到一个2147483641+1 是否等于2147483642是否成立的条件,但是在代码中虽然worker线程的m_commit_sequence_nr和m_commit_sequence_generator都是unsigned long long类型也就是8字节不带符号位的,但是自动推导的变量当worker的m_commit_sequence_nr的取出来后确实保存在一个int类型的变量中,也就是如下:auto this_seq_nr{0};

然后 this_seq_nr+1,如果这里this_seq_nr是2147483647,加1后者会发生溢出,如下

(gdb) p this_seq_nr$4 = 2147483647(gdb) ptype this_seq_nrtype = int(gdb) p next_seq_nr$3 = -2147483648(gdb) ptype next_seq_nrtype = int

其中就是next_seq_nr发生了溢出,来到了负数。这个时候会比对-2147483648是否等于2147483648,如果等于才会唤醒,显然这里就不满足了,因此BUG产生,BUG产生后任何worker都不能唤醒,参考上面的图。-2147483648实际上就是

1000 0000 0000 0000 0000 0000 0000 0000 

也就是2147483647+1

0111 1111 1111 1111 1111 1111 1111 1111 

加了一个1因为符号位为1了,因此显示了最大的负数,也就是- 2的31次方,实际就是溢出了。这里可以用auto this_seq_nr{0ull}; 让自动推导为unsigned long long类型,则不会溢出。

五、BUG模拟

因为所有的事务每次提交都会获取一个提交序列m_commit_sequence_nr,其来自全局生成器m_commit_sequence_generator,而m_commit_sequence_generator初始化的时候为1,我们直接修改其初始化值为2147483640,这样很快就会出现溢出的情况,否则模拟2147483647个事务不太现实。同时我们主库设置writeset,同时使用多核CPU,从库设置8个并行线程,意为最大限度的加大并发。

mysql> set global transaction_write_set_extraction=XXHASH64;Query OK, 0 rows affected (0.00 sec)mysql> set global  binlog_transaction_dependency_tracking=WRITESET;Query OK, 0 rows affected (0.00 sec)

经过编译后,从库稳定重现,

重现后的DEBUG方式,这里选用条件断点,并且打到Commit_order_manager::finish_one的286行左右,由于断路原则,A&&B条件需要满足才会跑这个条件,也就是需要唤醒比较提交序列的时候,代码的this->m_workers[next_worker].freeze_commit_sequence_nr这行。

break rpl_slave_commit_order_manager.cc:286 if next_seq_nr < 0

观察

(gdb) p *(this->m_workers[next_worker].m_commit_sequence_nr.m_underlying)$11 = {<std::__atomic_base<unsigned long long>> = {static _S_alignment = 8, _M_i = 2147483648}, <No data fields>}(gdb) p next_seq_nr$12 = -2147483648

显然worker自生的m_commit_sequence_nr没有问题为2147483648,但是取出来后就是-2147483648并不相等,因此条件C不满足不做唤醒。

需要注意的是本问题和slave_preserve_commit_order参数有关,如果关闭则不会,因为关闭后整个Commit_order_manager结构将不会初始化,也就没有提交顺序一说了。

六、总结这个问题是MTS多线程并发下的一个问题且和参数slave_preserve_commit_order有关,并且无法自动解锁,因为已经无法唤醒等待的worker线程。如果MTS 压力不大,一直是单线程woker在并发,因为没有需要唤醒的worker,那么可能永远也遇不到。这个问题触发在自从库启动依赖执行了2147483647事务之后,一旦超过并且有需要唤醒的worker则不能唤醒,也就是说不一定是刚好在2147483647个事务上,可能稍微多一点,因为2147483647事务过后可能没有需要唤醒的worker,但是一旦压力上来就要可能不能唤醒。也就是说hang住发生的时间点大于等于从库启动依赖执行了2147483647事务。重启主从可以重置全局计数器,因此可以恢复运行。8028修复了这个问题,修复代码主要如下,

其中cs::apply::Commit_order_queue::sequence_type为一个unsigned long long类型。

标签: #php队列mysql #mysql安装教程8028