MySQL升级WRITE_SET后的一次死锁分析[通俗易懂]

MySQL升级WRITE_SET后的一次死锁分析[通俗易懂]背景 MySQL在推出MGR的时候使用了WRITE_SET, 借用这个思想, MySQL在5.7.22版本引入了基于WRITE_SET的并行复制方案[1]。在原先的主从复制技术中,同一批次的事物能进…

MySQL升级WRITE_SET后的一次死锁分析

导致我们死锁的现象是: 我们发现开启了write_set并行回放的实例从库上死锁的概率比以前高了不少, 并且发生死锁的实例都是在进行xtrabackup备份。本文主要分析这些数据库实例上发生死锁的原因。

场景

我们知道MySQL事物会设计到很多的锁,比如MDL锁,innodb的行锁,意向锁,latch 锁等等。不同的隔离级别锁的行为也有很多的差异。从死锁理论的角度:死锁就是有向图中存在环,从而造成相互等待。要解决死锁只要简单的破坏任何一条边,来打破环行等待。当然实际的实现可能会因各个环节点的权重不同而有所优化,选择代价最小的。但之前的重点肯定是找出这个“环”。而这些锁有些是运维的时候可以看到有些是看不到的。比如latch锁一般对用户看不到。因为性能原因,我们的MDL锁和INNODB锁的详细信息并未收集。如果开启了,就可以通过performance_schema.metadata_lock这个表来查询MDL锁的相关信息,通过show engine innodb status来查看详细innodb的加锁信息。

通过简单的分析,我们锁定是MDL死锁。所以在这样的场景下,我们只能通过show full processlist来查看到当时的状态,如下图:

case1: photo 1 图1

case2: photo 2-1 图2-1

photo 2-2 图2-2

===

为了方便大家理解, 我画了一个示意图[图3]来解释这两个case的死锁情况: photo 3 图3

case1 死锁分析:

可以看到在work线程组中,有一个work处理的事物先到达了事物的提交状态, 但是事物在提交前需要进行 order_commit判断,因为我们设置了slave_preserve_commit_order ,要保证事物是按照主库上的提交顺序来提交的。所以这个时候必须等待之前的事物要提交才可以进行。所以看到这个线程的状态是: “Waiting for preceding transaction to commit”。当那个”靠前”的事物准备提交的时候要去拿mdl::commit_lock这把锁,发现要不到。形成如上的“环等待”。

通过分析可以知道,这个时候同时执行了 FTWRL (flush table with read lock), 而这个操作会获取到MDL的一个共享锁。但是同样没有版本获取mdl::commit_lock 而等待。这个等待会造成新来的更新请求被阻塞,因为更新的语句是排他类型的锁。由于篇幅的原因,不细说MDL锁兼容细节。这里只给出结论,会阻塞部分更新的语句,进而会影响到业务。

=== photo 4 图4

case2 死锁分析:

顺便提一句: 同样可以看到,这种情况下新的请求被阻塞主。注意,这也正是备份的核心思想。阻塞新来的请求,阻塞同批次的提交。保证在备份的时候没有新的数据插入

一开始一个比较”靠后”的事物获取了mdl::commit_lock,在准备提交的时候,发现系统配置了slave_preserve_commit_order,同时该事物的前面还有事物未提交,需要等待前面的事物先执行完成后才能继续。然后FTWRL先获取了mdl::global_read_lock锁,但是没有办法获取mdl::commit_lock锁。

这个时候如果这个“前面的事物”是更新操作,那么就跟mdl::global_read_lock锁互斥,故而形成上面的死锁。

验证

由于这样的死锁,是概率出现的。为了高效的复现问题,我们打算使用mysql的测试框架来验证. 第一个步骤是:通过上面的分析,修改内核源码加大死锁的概率。证明我们的猜想确实能够出现死锁。但是这个出现的死锁并不一定就是线上真是环境的死锁。故而需要我们把修改的源码在实际场景下面验证。当然我们没有办法在生产环境来验证。我们可以通过第一步修改的源码,然后使用备份的数据来模拟。如果使用备份的数据 + 我们修改的源码数据库实例复现了,才能客观的判断我们的死锁研判。当然读者可能说我们修改源码破坏了之前的环境,这里当然是有前提的。这个前提就是:只修改并行回放线程组中的某一个线程,不改变原有逻辑,只是单纯的让它支持慢一点来提高死锁的概率,作证我们的死锁研判。

首先我们的第一步就是要:在主库上产生两个事物(当然我们也可以使用蛮力,循环,不过可能效果差,甚至可能无法复现),使用MySQL的测试框架,祥见如下的代码:

57 #  ===========================
58 # 在master上创建两个链接master和master1
59 --source include/rpl_connection_master.inc
60 send SET DEBUG_SYNC="waiting_in_the_middle_of_flush_stage SIGNAL w WAIT_FOR b";
61
62 --source include/rpl_connection_master1.inc
63 send SET DEBUG_SYNC= "now WAIT_FOR w";
64
65 --source include/rpl_connection_master.inc
66 --reap
67 show master status;
68 send insert into test.t1 values(1);
69
70 --source include/rpl_connection_master1.inc
71 --reap
72 SET DEBUG_SYNC= "bgc_after_enrolling_for_flush_stage SIGNAL b";
73 insert into test.t1 values(1000);  

代码100分

如何验证我们的主库上这两个事物属于同一个批次呢?当然是binlog啦。结果如下:

代码100分show master status;
File	Position	Binlog_Do_DB	Binlog_Ignore_DB	Executed_Gtid_Set
master-bin.000001	849
#200107  9:26:14 server id 1  end_log_pos 219 CRC32 0x059fa77a 	Anonymous_GTID	last_committed=0	sequence_number=1	rbr_only=no
#200107  9:26:24 server id 1  end_log_pos 408 CRC32 0xa1a6ea99 	Anonymous_GTID	last_committed=1	sequence_number=2
	rbr_only=yes
#200107  9:26:24 server id 1  end_log_pos 661 CRC32 0x2b0fc8a5 	Anonymous_GTID	last_committed=1	sequence_number=3	rbr_only=yes

可以看到last_commit这个字段我们一共产生了两组binlog, 一个是0 这里是create table 语句。另外一个是1, 就是我们上面的两条insert 语句。

接下来就是就是要修改MySQL的源代码了,这里主要是要考虑到MTS的并行复制逻辑。因为我们在主库上通过DEBUG_SYNC让大的事物先执行,所以比如是大的事物先分配到woker线程组中的第一个。所以我们在binlog回放的关键路径上: Xid_apply_log_event::do_apply_event_worker 这个函数中让第一个worker sleep足够多的时间让我们执行FTWRL。

直接修改源代码编译需要来回的编译,我们这边使用systemstap 这个工具,JIT在运行时注入一段代码来改变某些worker的行为。在执行注入前先执行脚本验证下能否注入:

41 --exec sudo stap -L "process("$MYSQLD").function("pop_jobs_item")"
42 --exec sudo stap -L "process("$MYSQLD").function("*Xid_apply_log_event::do_apply_event_worker")"

需要注意的是,因为stap的架构原理的原因,详细可参考下面的链接[3],需要root权限。下面是注入的代码:

代码100分stap -v -g -d $MYSQLD --ldd -e "probe process($server_pid).function("Xid_apply_log_event    ::Xid_apply_log_event
") {printf("hit in do_apply_log_event
") if ($w->id ==0) { mdelay(30000)} }"
stap -v -g -d $MYSQLD --ldd -e "probe process($server_pid).function("pop_jobs_item") { printf("hit in
pop_jobs_item") if ($worker->id == 0) { mdelay(3000)} }"

大致的意思就是: 让复制线程组的第一个线程sleep 3s。这样有足够的时间来运行FTWRL。最终的执行结果:

show full processlist;
Id	User	Host	db	Command	Time	State	Info
3	root	localhost:10868	test	Sleep	83		NULL
4	root	localhost:10870	test	Sleep	84		NULL
7	root	localhost:10922	test	Query	61	Waiting for commit lock	flush table with read lock
8	root	localhost:10926	test	Query	0	starting	show full processlist
9	system user		NULL	Connect	82	Waiting for master to send event	NULL
10	system user		NULL	Connect	61	Slave has read all relay log; waiting for more updates	NULL
11	system user		NULL	Connect	71	Waiting for global read lock	NULL
12	system user		NULL	Connect	71	Waiting for preceding transaction to commit	NULL
13	system user		NULL	Connect	82	Waiting for an event from Coordinator	NULL
14	system user		NULL	Connect	81	Waiting for an event from Coordinator	NULL

可以看到,我们的猜想完整的复现了死锁。大致解释下:

我们在构造这个死锁的时候,因为我们控制 的worker会sleep 3s。故而我们可以查询worker的状态,当worker处于 Waiting for preceding transaction to commit 这个状态的时候,立马执行FTWRL。然后可以看到FTWRL会block在commit_lock。然后另外一个更新自然是要等待: global read lock, 而形成死锁。

总结

首先对于不太理解备份原理的同学,应该可以从这两个死锁等待图中清楚的看到FTWRL的作用。它是通过两把GLOBAL READ LOCK 和COMMIT_LOCK锁来控制备份的一致性。这里不详细讨论。 解决死锁问题,通过死锁理论,肯定是要打破有向图中的环。在我们的这个死锁case中通过分析可以知道可以操作的两条边只有: 1. slave_preserve_commit_order
2. FTWRL 显然:对于那些可以接受在从库上事物的提交可以“乱序”的,我们只要关闭这个配置选项就可以解除死锁

而如果是要强制要求有序的,那么我们只能关闭备份的线程(图中的节点,及相关的边) 同样可以激活成功教程死锁。在死锁出现的时候,个人觉得关闭备份线程代码是更小的。如果关闭worker线程的话,从库复制会出错误。

参考

  1. https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-22.html
  2. http://mysqlhighavailability.com/improving-the-parallel-applier-with-writeset-based-dependency-tracking/
  3. https://sourceware.org/systemtap/langref/

作者:龙利剑

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/10003.html

(0)
上一篇 2023-01-27
下一篇 2023-01-27

相关推荐

  • 用Python编程,学习如何使用lower()函数

    用Python编程,学习如何使用lower()函数Python是一种高级编程语言,被广泛应用于各种领域,特别是数据分析和人工智能领域。在Python编程中,经常需要将字符串进行一些处理,比如大小写转化等操作,这时候就需要使用一些字符串函数。本文将介绍如何使用lower()函数,将字符串转化为小写字母。

    2024-06-05
    58
  • Python os.path.join函数使用指南

    Python os.path.join函数使用指南Python是一种高级编程语言,由于其简洁、易读易写以及广泛的应用,已成为程序员最受欢迎的编程语言之一。而os.path.join()函数是Python中常用的字符串拼接方法之一,可以将多个路径段连接在一起,并且会自动根据操作系统的不同使用正确的路径分隔符。因此,掌握这个函数的使用方法对Python编程非常重要。

    2024-05-07
    95
  • ORDER BY导致索引使用不理想「建议收藏」

    ORDER BY导致索引使用不理想「建议收藏」在MySQL中经常出现未按照理想情况使用索引的情况,今天记录一种Order by语句的使用导致未按预期使用索引的情况。 1. 问题现象 1.1 SQL语句: SELECT DISTINCT p.* F

    2022-12-21
    145
  • 流处理引擎(SPE)中的的分布式一致性语义之Exactly-Once和Effectively

    流处理引擎(SPE)中的的分布式一致性语义之Exactly-Once和EffectivelyAt most Onece:最多一次,如果算子处理事件失败,事件将不再尝试该事件。 At Least Onece:至少一次,如果算子处理事件失败,算子会再次尝试该处理事件,直到有一次成功。 Exact

    2023-01-30
    152
  • python中退出for循环的命令

    python中退出for循环的命令a href=”https://beian.miit.gov.cn/”苏ICP备2023018380号-1/a Copyright www.python100.com .Some Rights Reserved.

    2024-04-29
    75
  • MySQL的IP地址与数字互转原理[通俗易懂]

    MySQL的IP地址与数字互转原理[通俗易懂]一、inet_aton与inet_ntoa inet_aton是把ip地址转为数字的函数,记忆小技巧,inet表示网络相关,在c语言中a习惯性代表字符串,to就是转换的,n代表数字,aton就是字符…

    2022-12-20
    137
  • 使用CMD执行Python脚本

    使用CMD执行Python脚本现在Python被广泛应用于数据处理、Web开发、人工智能等领域。在日常开发工作中,我们经常需要使用Python脚本完成一些常规任务。比如,我们要对一个文件夹中的所有文件进行批量处理,我们可以编写一个Python脚本,通过运行脚本来实现批量处理。但是,如何在Windows下方便地运行Python脚本呢?这时候,CMD(命令提示符)就是一个很好的选择。

    2024-09-14
    23
  • 如何通过 ShardingSphere-Proxy 落地分表分库?

    如何通过 ShardingSphere-Proxy 落地分表分库?参考:Sharding-Proxy的基本功能使用 1. 环境准备 MySql 5.7 apache-shardingsphere-4.1.1-sharding-proxy-bin.tar.gz jdk

    2023-04-10
    234

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注