MySQL · 答疑解惑 · 物理备份死锁分析

简介: 背景 本文对 5.6 主备场景下,在备库做物理备份遇到死锁的case进行分析,希望对大家有所帮助。 这里用的的物理备份工具是 Percona-XtraBackup(PXB),有的同学可能不清楚其备份流程,所以这里先简单说下,PXB的备份步骤是这样的: 拷贝 InnoDB redo log,这是

背景

本文对 5.6 主备场景下,在备库做物理备份遇到死锁的case进行分析,希望对大家有所帮助。

这里用的的物理备份工具是 Percona-XtraBackup(PXB),有的同学可能不清楚其备份流程,所以这里先简单说下,PXB的备份步骤是这样的:

  1. 拷贝 InnoDB redo log,这是一个单独的线程在拷,直到备份结束;
  2. 拷贝所有InnoDB ibd文件;
  3. 加全局读锁,执行 FLUSH TABLES WITH READ LOCK(FTWRL);
  4. 拷贝 frm、MYD、MYI 等文件;
  5. 获取位点信息,执行 show slave status 和 show master status;
  6. 解锁,UNLOCK TABLES;
  7. 做一些收尾处理,备份结束。

如果 MyISAM 表很多话,全局读锁的持有时间会比较长,所以一般都在备库做备份。

另外 FLUSH TABLE WITH READ LOCK 这条命令会获取2个MDL锁,全局读锁(MDL_key::GLOBAL)和全局COMMIT(MDL_key::COMMIT)锁,MDL锁详情可以参考之前的月报MDL 实现分析

死锁分析

CASE 1

我们先看一下死锁时的现场是怎样的:

mysql> show processlist;
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
| Id | User        | Host            | db   | Command | Time | State                                  | Info              |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
|  1 | root        | 127.0.0.1:53309 | NULL | Query   |  278 | init                                   | show slave status |
|  2 | system user |                 | NULL | Connect |  381 | Queueing master event to the relay log | NULL              |
|  3 | system user |                 | NULL | Connect |  311 | Waiting for commit lock                | NULL              |
|  4 | root        | 127.0.0.1:53312 | NULL | Query   |    0 | init                                   | show processlist  |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+

可以看到 show slave status 被堵了很久,另外 SQL 线程在 Waiting for commit lock,说明在等待 COMMIT 锁。

这时候如果我们再连接进去执行 show slave status 也会被堵,并且即使 Ctrl-C kill 掉线程,线程依然还在。

mysql> show processlist;
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
| Id | User        | Host            | db   | Command | Time | State                                  | Info              |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
|  1 | root        | 127.0.0.1:53309 | NULL | Query   |  753 | init                                   | show slave status |
|  2 | system user |                 | NULL | Connect |  856 | Queueing master event to the relay log | NULL              |
|  3 | system user |                 | NULL | Connect |  786 | Waiting for commit lock                | NULL              |
|  4 | root        | 127.0.0.1:53312 | NULL | Killed  |  188 | init                                   | show slave status |
|  5 | root        | 127.0.0.1:53314 | NULL | Query   |    0 | init                                   | show processlist  |
|  8 | root        | 127.0.0.1:53318 | NULL | Killed  |  125 | init                                   | show slave status |
| 11 | root        | 127.0.0.1:53321 | NULL | Killed  |  123 | init                                   | show slave status |
| 14 | root        | 127.0.0.1:53324 | NULL | Query   |  120 | init                                   | show slave status |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+

pstack 看下相关线程的 backtrace,show slave status 线程的 backtrace 如下,非常明显是在等mutex,对应代码为 mysql_mutex_lock(&mi->rli->data_lock):

#0  __lll_lock_wait
#1  _L_lock_974
#2  __GI___pthread_mutex_lock
#3  inline_mysql_mutex_lock
#4  show_slave_status
#5  mysql_execute_command
#6  mysql_parse
#7  dispatch_command
#8  do_command
#9  do_handle_one_connection
#10 handle_one_connection
...

SQL 线程的 backtrace 如下,在等 COMMIT 锁:

#0  pthread_cond_timedwait
#1  inline_mysql_cond_timedwait
#2  MDL_wait::timed_wait
#3  MDL_context::acquire_lock
#4  ha_commit_trans
#5  trans_commit
#6  Xid_log_event::do_commit
#7  Xid_log_event::do_apply_event
#8  Log_event::apply_event
#9  apply_event_and_update_pos
#10 exec_relay_log_event
#11 handle_slave_sql
...

如果我们gdb进去,去调试SQL线程,在 MDL_context::acquire_lock中:

(gdb) p (MDL_key::enum_mdl_namespace)lock->key->m_ptr[0]
$24 = MDL_key::COMMIT
(gdb) p ((THD*)lock->m_granted.m_list.m_first->m_ctx->m_owner)->thread_id
$25 = 1

可以看到 COMMIT 锁被线程 1 持有。

SQL线程在 Xid_log_event::do_commit 之前会持有 rli_ptr->data_lock

所以现在就清楚了,是线程1(备份线程)和线程3(SQL线程)死锁了,还原下死锁过程:

  1. 备份线程执行 FTWRL,拿到 COMMIT 锁;
  2. SQL线程执行到Xid event,准备提交事务,请求 COMMIT 锁,被备份线程阻塞;
  3. 备份线程为了获取 slave 执行位点,执行 show slave status,需要获取 rli->data_lock,被 SQL 线程阻塞。

就这样2个线程互相持有等待,形成死锁。

我们知道,MDL 是有死锁检测的,为什么这里没有检测到呢?因为rli->data_lock是一个mutex,不属于MDL系统的,在这个死锁场景中,MDL锁系统只能检测到对 COMMIT 锁的请求,是不存在死锁的。

之后的 show slave status 都被堵,是因为在执行 show slave status 前,会请求一个mutex:

mysql_mutex_lock(&LOCK_active_mi);
res= show_slave_status(thd, active_mi);
mysql_mutex_unlock(&LOCK_active_mi);

之前死锁的 show slave status 没有退出,后面的 show slave status 自然堵在这个 mutex 上,并且因为无法检测 thd->killed,所以一直无法退出。

死锁的原因是SQL线程在提交的时候,持有 rli->data_lock 锁,其实这个是不需要的,MySQL 官方在这个 patch 中修复。

CASE 2

在上面的bug修复后,又出现了死锁,但死锁的情况却不一样,show processlist 结果如下:

mysql> show processlist;
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+
| Id | User        | Host            | db   | Command | Time | State                            | Info              |
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+
|  2 | system user |                 | NULL | Connect |  436 | Waiting for master to send event | NULL              |
|  3 | system user |                 | NULL | Connect |  157 | Waiting for commit lock          | NULL              |
|  6 | root        | 127.0.0.1:42787 | NULL | Query   |   86 | init                             | show slave status |
|  7 | root        | 127.0.0.1:42788 | NULL | Query   |   96 | Killing slave                    | stop slave        |
|  8 | root        | 127.0.0.1:42789 | NULL | Query   |    0 | init                             | show processlist  |
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+

依然是 SQL 线程在等待commit锁,然后 show slave status 被堵住没有返回,不同的是多了一个 stop slave; 我们来看下 stop slave 的backtrace:

#0  pthread_cond_timedwait
#1  inline_mysql_cond_timedwait
#2  terminate_slave_thread
#3  terminate_slave_threads
#4  stop_slave
#5  mysql_execute_command
#6  mysql_parse
#7  dispatch_command
#8  do_command
#9  do_handle_one_connection
#10 handle_one_connection
...

对应代码,可以发现 stop slave 正在等待 SQL 线程退出,而SQL线程此时正在等待备份线程(id=6)持有的 COMMIT 锁。整个死锁过程是这样的:

  1. 备份线程执行 FTWRL,拿到 COMMIT 锁;
  2. SQL线程执行到Xid event,准备提交事务,请求 COMMIT 锁,被备份线程阻塞;
  3. 用户执行 stop slave,准备停掉备库复制线程,等待 SQL 线程退出;
  4. 备份线程为了获取 slave 执行位点,执行 show slave status,需要获取 LOCK_active_mi 锁,被用户线程(stop slave)阻塞。

这次是备份线程、SQL 线程、用户线程3个线程互相持有等待,形成死锁。

这次并不是代码bug,算是一个用法问题,因此我们在运维过程中,如果发现 SQL 线程在 Waiting for commit lock,就不要 stop slave。

死锁解决

如果不可避免出现了死锁,该怎么解决呢?

通过上面的分析可以看到,不管是在 case 1 还是 case 2,备份线程和用户线程都不再接受响应了,要解决死锁的话,只能 kill 掉 SQL 线程了,那么直接 kill 是否有风险呢?

SQL 线程能执行 Xid event,说明是在更新事务引擎表,kill 掉应该没问题(事务可以回滚,之后可以重做),但是5.6有这样的一个bug,会导致SQL线程在等待 COMMIT 锁的时候被kill,直接跳过事务,这样备库会比主库少一个事务,因此 kill 后需要对比主备数据,把少的事务补上。

如果你使用的 MySQL 版本已经修掉这个bug,也就是在 5.6.21 版本及之后,那么 kill SQL 线程是安全的。

死锁重现

如果为了测试或研究代码,要想复现死锁该怎么办呢?如果直接在备库执行一个 FTWRL,很可能是复现不了的,因为FTWRL是获取2个锁,全局读锁和全局 COMMIT 锁,SQL 线程非常可能被全局读锁堵到(Waiting for global read lock),而不是被 COMMIT 锁堵(Waiting for commit lock)。

一种方法是写 testcase,用 dubug sync 功能设置同步点,让线程停在指定的地方,但这要求 mysqld 跑在deubg模式下,并且要求有一定的MySQL 源码开发基础;
另一种方法是改代码,延长do_commit的时间,比如 sleep 一段时间,这样就给我们足够的时间让 FTWRL 在 SQL 线程请求 COMMIT 锁前执行完成,但是这需改代码,然后重新编译安装;
如果我们不会用debug sync,又不想改代码重新编译安装,就想在已有的环境测,改怎么办呢?SYSTEMTAP!

systemtap 起初只支持在内核空间进行探测,0.6 版本之后可以在用户空间进行探测,使用 systemtap 需要程序中包含 debug 信息(程序编译时加上 -g 选项)。

列出所有我们可以对 mysqld 进行探测的地方。

sudo stap -L 'process("/usr/sbin/mysqld").function("*")'

列出所有可以对 Xid_log_event 类进行探测的地方。

sudo stap -L 'process("/usr/sbin/mysqld").function("*Xid_log_event::*")'

如果我们想让 Xid_log_event::do_commit 执行有点延迟,可以这样做:

sudo stap -v -g -d /usr/bin/mysqld --ldd  -e 'probe process(16011).function("Xid_log_event::do_commit") { printf("got it\n")  mdelay(3000) }'

16011 是正在跑的备库进程PID,执行上面的 stap 命令后,每当备库执行到 Xid_log_event::do_commit 时,stap 就会打出个 “got it”,然后 SQL 线程暂停3s,这就给了我们充足的时间去执行 FTWRL,在SQL线程 commit 前拿到 COMMIT 锁。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
2月前
|
存储 消息中间件 监控
MySQL 到 ClickHouse 明细分析链路改造:数据校验、补偿与延迟治理
蒋星熠Jaxonic,数据领域技术深耕者。擅长MySQL到ClickHouse链路改造,精通实时同步、数据校验与延迟治理,致力于构建高性能、高一致性的数据架构体系。
MySQL 到 ClickHouse 明细分析链路改造:数据校验、补偿与延迟治理
|
3月前
|
缓存 关系型数据库 BI
使用MYSQL Report分析数据库性能(下)
使用MYSQL Report分析数据库性能
162 3
|
2月前
|
NoSQL 算法 Redis
【Docker】(3)学习Docker中 镜像与容器数据卷、映射关系!手把手带你安装 MySql主从同步 和 Redis三主三从集群!并且进行主从切换与扩容操作,还有分析 哈希分区 等知识点!
Union文件系统(UnionFS)是一种**分层、轻量级并且高性能的文件系统**,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem) Union 文件系统是 Docker 镜像的基础。 镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
498 5
|
3月前
|
缓存 监控 关系型数据库
使用MYSQL Report分析数据库性能(上)
最终建议:当前系统是完美的读密集型负载模型,优化重点应放在减少行读取量和提高数据定位效率。通过索引优化、分区策略和内存缓存,预期可降低30%的CPU负载,同时保持100%的缓冲池命中率。建议每百万次查询后刷新统计信息以持续优化
246 6
|
3月前
|
缓存 监控 关系型数据库
使用MYSQL Report分析数据库性能(中)
使用MYSQL Report分析数据库性能
169 1
|
4月前
|
存储 关系型数据库 MySQL
深入理解MySQL索引类型及其应用场景分析。
通过以上介绍可以看出各类MySQL指标各自拥有明显利弊与最佳实践情墁,在实际业务处理过程中选择正确型号极其重要以确保系统运作流畅而稳健。
198 12
|
4月前
|
存储 关系型数据库 MySQL
在CentOS 8.x上安装Percona Xtrabackup工具备份MySQL数据步骤。
以上就是在CentOS8.x上通过Perconaxtabbackup工具对Mysql进行高效率、高可靠性、无锁定影响地实现在线快速全量及增加式数据库资料保存与恢复流程。通过以上流程可以有效地将Mysql相关资料按需求完成定期或不定期地保存与灾难恢复需求。
394 10
|
5月前
|
存储 SQL 关系型数据库
MySQL的Redo Log与Binlog机制对照分析
通过合理的配置和细致的管理,这两种日志机制相互配合,能够有效地提升MySQL数据库的可靠性和稳定性。
196 10
|
3月前
|
关系型数据库 MySQL 数据库
自建数据库如何迁移至RDS MySQL实例
数据库迁移是一项复杂且耗时的工程,需考虑数据安全、完整性及业务中断影响。使用阿里云数据传输服务DTS,可快速、平滑完成迁移任务,将应用停机时间降至分钟级。您还可通过全量备份自建数据库并恢复至RDS MySQL实例,实现间接迁移上云。
|
3月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
798 152

相关产品

  • 云数据库 RDS MySQL 版
  • 推荐镜像

    更多