首页 » PHP教程 » 百格运动php技巧_MySQL80从库MTS并发的准时炸弹

百格运动php技巧_MySQL80从库MTS并发的准时炸弹

访客 2024-12-15 0

扫一扫用手机浏览

文章目录 [+]

8.0.28以下的MTS可能在运行一段韶光后涌现hang去世的情形,大概如下(仿照的):

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

百格运动php技巧_MySQL80从库MTS并发的准时炸弹

https://bugs.mysql.com/bug.php?id=103636

百格运动php技巧_MySQL80从库MTS并发的准时炸弹
(图片来自网络侵删)

这个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线程的id

MDL_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现成的实现。

2.一个提交行列步队,这个很显然,当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个条件来剖断是否唤醒:

1)当前有worker线程

2)通过提交行列步队头部获取的下一个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。
那么当前等待和实行的图如下:

当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类型。

作者丨八怪(高鹏)

来源丨"大众年夜众号: MySQL学习(ID:MySQL_case)

dbaplus社群欢迎广大技能职员投稿,投稿邮箱:editor@dbaplus.cn

最新活动丨XCOPS智能运维管理人年会

报名地址:2023 XCOPS智能运维管理人年会-广州站 - 百格活动

标签:

相关文章