MySQL · BUG分析 · Rename table 死锁分析
http://mysql.taobao.org/monthly/2016/03/06/
背景
InnoDB buffer pool中的page管理牵涉到两个链表,一个是lru链表,一个是flush 脏块链表,由于数据库的特性:
- 脏块的刷新,是异步操作;
- page存在两个版本,一个是ibd文件的持久化版本,和buffer pool内存中的当前版本。
所以在对table对象进行ddl变更的时候,要维护两个版本之间的一致性,有一些操作需要同步进行page缓存的管理。例如以下三种ddl操作:
1. flush table t for export
这是MySQL 5.6提供的InnoDB transportable tablespace功能,用于在不同实例之间进行表传输。由于需要透明的在物理层面迁移ibd文件,所以需要保证buffer pool中的page和ibd文件中的page的一致性。其操作步骤如下:
- 持有t表的MDL锁,保证在t表上没有活跃事务,即buffer pool中的脏page都是已提交事务;
- 扫描buffer pool中的flush list,同步刷下脏块;
- 记录数据字典信息到cfg文件,用于目标端的表结构匹配和验证,最后在目标端import的时候,变更page的space,max_lsn等。
2. drop table t
在对表进行删除的时候,需要清理掉buffer pool中的page,但如果表比较大,占用过多的buffer pool,清理的动作会影响到在线的业务,所以MySQL提供了lazy drop table的方式。
同步方式: 扫描lru链表,如果page属于t表,就从lru链表,hash表, flush list中删除,回收block到free list中。
lazy方式: 扫描lru链表,如果page属于t表,就给page设置一个
space_was_being_deleted
属性,等lru置换或者checkpoint flush dirty block的时候进行清理。
3. alter table t rename to t1
rename table name操作,虽然是DDL,但rename操作只是变更了数据字典中的table name和文件系统的ibd文件名称,所以,在rename的过程中,不存在对buffer pool中属于t表的page的同步操作,但由于要变更表名,即需要同步对文件的IO操作。
而今天要讲的主题,就发生在这里,由于rename table进行IO操作同步的过程中,产生的死锁。
问题现象
在MySQL 5.5版本上,error日志大量报出以下的错误信息:
InnoDB: fil_sys open file LRU len 0
InnoDB: Warning: too many (300) files stay open while the maximum
InnoDB: allowed value would be 300.
InnoDB: You may need to raise the value of innodb_open_files in my.cnf.
...
InnoDB: Warning: problems renaming 'db_1/#sql-xxx_xxx' to 'db_1/xxx', 1000 iterations
InnoDB: Warning: tablespace './db_1/#sql-xxx_xxx.ibd' has i/o ops stopped for a long time 1000
查看操作日志,是一个普通的rename语句操作,但持续很久,因为rename只是数据字典的变更,除了MDL锁阻塞以外
不应该持续这么长时间,pstack查看线程栈信息:
Thread 5 (Thread 0x50ad7940 (LWP 25047)):
#0 0x000000364aacced2 in select () from /lib64/libc.so.6
#1 0x00002aaab2e595fb in os_thread_sleep ()
#2 0x00002aaab2e1a3e2 in fil_rename_tablespace ()
#3 0x00002aaab2e0672b in dict_table_rename_in_cache ()
#4 0x00002aaab2e86af5 in row_rename_table_for_mysql ()
#5 0x00002aaab2e316db in ha_innodb::rename_table ()
#6 0x00000000006bea6c in mysql_rename_table ()
#7 0x00000000006c77ff in mysql_alter_table ()
#8 0x00000000005c6a8e in mysql_execute_command ()
#9 0x00000000005cd371 in mysql_parse ()
#10 0x00000000005cd773 in dispatch_command ()
#11 0x00000000005cea04 in do_command ()
#12 0x00000000005bf0d7 in handle_one_connection ()
#13 0x000000364b6064a7 in start_thread () from /lib64/libpthread.so.0
#14 0x000000364aad3c2d in clone () from /lib64/libc.so.6
Thread 100 (Thread 0x42945940 (LWP 3870)):
#0 0x000000364b60ab99 in pthread_cond_wait@@GLIBC_2.3.2 ()
#1 0x00002aaab2e589a5 in os_event_wait_low ()
#2 0x00002aaab2e57dd4 in os_aio_simulated_handle ()
#3 0x00002aaab2e14ccc in fil_aio_wait ()
#4 0x00002aaab2ea2418 in io_handler_thread ()
#5 0x000000364b6064a7 in start_thread () from /lib64/libpthread.so.0
#6 0x000000364aad3c2d in clone () from /lib64/libc.so.6
Thread 120 (Thread 0x40da6940 (LWP 3882)):
#0 0x000000364aacced2 in select () from /lib64/libc.so.6
#1 0x00002aaab2e595fb in os_thread_sleep ()
#2 0x00002aaab2e18838 in fil_mutex_enter_and_prepare_for_io ()
#3 0x00002aaab2e18aa5 in fil_io ()
#4 0x00002aaab2df5b63 in buf_flush_buffered_writes ()
#5 0x00002aaab2df6048 in buf_flush_batch ()
#6 0x00002aaab2ea13d8 in srv_master_thread ()
#7 0x000000364b6064a7 in start_thread () from /lib64/libpthread.so.0
#8 0x000000364aad3c2d in clone () from /lib64/libc.so.6
这里我只列了有意义的三个线程:
- 用户线程Thread 5
用户线程确实在进行rename操作,但阻塞在fil_rename_tablespace
函数中。 - master线程Thread 120
InnoDB的master线程阻塞在fil_mutex_enter_and_prepare_for_io
函数中。 - IO线程Thread 100
InnoDB的IO线程一共有8个,4个读,4个写线程,发现都在os_event_wait_low
中,也就是都空闲着等待condition中。
从上面的调用栈来看,线程之间长时间维持在这种状态下,明显发生了死锁,在我们解这个死锁之前,我们先来回顾一点背景知识,然后再说明死锁的真正原因。
InnoDB背景
checkpoint
由于对数据库的数据操作也遵循read-update-write的方式,所以数据的更新,会把buffer pool中的page变成脏块,由于write-ahead logs机制保证事务的完整性,脏块的write可以变成异步的,但又由于buffer pool的大小终究有限,而且对于recovery的时间的要求,又要求脏块的flush又要持续保证。
MySQL 5.5的版本由master thread来承担dirty flush的角色, dirty flush的过程就称为making checkpoint,lsn的推进保证了recovery的时间不被持续的变长。刷新的策略,受到当前IO pending的情况,double write-buffer是否打开,buffer pool中dirty page所占的比例,以及innodb_max_dirty_pages_pct
参数的设置,进行灵活刷新,具体的代码细节,这里就不展开了。
异步IO
由于dirty flush是异步的,所以,master thread只负责提交IO请求,真正的IO操作是由IO helper thread来完成的。InnoDB使用的simulate AIO和native AIO会有一些差别,我们这里以simulate AIO为例进行说明。假设double write-buffer是打开的:
- 首先master thread搜集dirty pages,同步写入double write-buffer;
- 由于double write-buffer的方式是buffered write,所以等double write-buffer写满了之后;
- 同步把double write-buffer的page顺序写入到ibdata系统表空间中,如果完成之后系统crash,可以使用持久化的double write-buffer进行page恢复;
- 开始把 double write-buffer中的page,写入真正的ibd文件中。依次提交异步IO操作,提交IO操作的步骤分为:
- 持有fil_system mutex,判断当前tablespace是否可用,
- 判断当前fil_space的stop_io标示,如果设置就循环等待
- 如果stop_io没有标示,就打开fil_space对应的ibd文件句柄,然后递增 fil_space->n_pending
- 提交IO请求
- 等double write-buffer中的pages提交完所有的IO请求,使用
os_aio_simulated_wake_handler_threads
来唤醒IO helper thread来完成IO操作。
Rename 操作
接下来我们来看下rename操作的步骤:
- 首先在server层hold MDL锁;
- 进入InnoDB层,首先使用自治事务变更数据字典,包括SYS_TABLES,SYS_FOREIGN;
- 变更数据字典的内存对象,包括table, index, foreign list等;
- 变更fil_space对象以及对应的ibd数据文件名称,其中变更文件系统名称的时候:
- 设置当前的fil_space的stop_io,阻止再进行IO操作
- 判断当前是否有IO pending,如果有,就等IO pending结束
- 如果没有IO pending,就关闭opened的句柄,并rename文件名称
- 恢复stop_io标示
- 提交自治事务。
有了这些操作的具体步骤,我们就可以清晰的分析出死锁的原因。
死锁原因
两个线程,一个是master thread,需要提交flush dirty block的异步IO请求;一个是user thread,需要进行rename操作。
Rename操作,只变更数据字典和ibd文件名,并不需要同步buffer pool中的page,唯一需要同步的就是IO操作,通俗一点说,也就是在user thread进行rename table需要变更ibd文件名的时候,其它线程暂时不要对这个文件进行IO操作,等rename完成后,可以重新打开这个ibd文件,接着进行IO操作。
InnoDB使用两个标识来进行IO同步操作,即stop_io,n_pending。
stop_io:user thread要进行rename操作,提前设置这个标识,表示IO操作可以先hold暂停。
n_pending:master thread要进行flush操作,我已经提交了IO请求,user thread要进行rename可以先hold,等IO完成。
假设下面的时序:
- master thread提交了1个IO请求,设置了n_pending;
- rename操作设置stop_io,判断n_pending>0 就等待;
- master thread需要提交剩下的几个IO,发现stop_io已设置,就等待;
- 由于master thread没有提交完这批IO,没有唤醒IO helper thread,导致第1个IO请求无法完成,n_pending一直等于1;
- rename操作因为n_pending一直等于1,陷入了死等;
- master thread发现stop_io等于true,陷入了死等。
具体的代码可以参考:
1. master thread
fil0fil.cc: fil_mutex_enter_and_prepare_for_io
space = fil_space_get_by_id(space_id);
if (space != NULL && space->stop_ios) {
/* We are going to do a rename file and want to stop new i/o's for a while */
if (count2 > 20000) {
fputs("InnoDB: Warning: tablespace ", stderr);
ut_print_filename(stderr, space->name);
fprintf(stderr,
" has i/o ops stopped for a long time %lu\n",
(ulong) count2);
}
mutex_exit(&fil_system->mutex);
os_thread_sleep(20000);
count2++;
goto retry;
}
2. user thread
fil0fil.cc: fil_rename_tablespace
/* We temporarily close the .ibd file because we do not trust that
operating systems can rename an open file. For the closing we have to
wait until there are no pending i/o's or flushes on the file. */
space->stop_ios = TRUE;
ut_a(UT_LIST_GET_LEN(space->chain) == 1);
node = UT_LIST_GET_FIRST(space->chain);
if (node->n_pending > 0 || node->n_pending_flushes > 0) {
/* There are pending i/o's or flushes, sleep for a while and retry */
mutex_exit(&fil_system->mutex);
os_thread_sleep(20000);
goto retry;
修复方法
修复的方法也比较简单,在fil_rename_tablespace
的时候,如果发现node->n_pending > 0的时候,在sleep之前,发起一次唤醒动作,即os_aio_simulated_wake_handler_threads
,IO helper thread去完成master thread已经提交的IO请求,这样n_pending就会降到0,死锁就解开了。
MySQL · BUG分析 · Rename table 死锁分析的更多相关文章
- Mysql Innodb存储引擎 insert 死锁分析
http://chenzhenianqing.cn/articles/1308.html
- 线上BUG:MySQL死锁分析实战
原文链接:线上BUG:MySQL死锁分析实战 1 线上告警 我们不需要关注截图中得其他信息,只要能看到打印得org.springframework.dao.DeadlockLoserDataAcces ...
- Mysql锁和死锁分析
在MySQL中,行级锁并不是直接锁记录,而是锁索引.索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定 ...
- <转>一个最不可思议的MySQL死锁分析
1 死锁问题背景 1 1.1 一个不可思议的死锁 1 1.1.1 初步分析 3 1.2 如何阅读死锁日志 3 2 死锁原因深入剖析 4 2.1 Delete操作的加锁逻辑 4 2.2 死锁预防策略 5 ...
- MySQL死锁分析
死锁问题背景 做MySQL代码的深入分析也有些年头了,再加上自己10年左右的数据库内核研发经验,自认为对于MySQL/InnoDB的加锁实现了如指掌,正因如此,前段时间,还专门写了一篇洋洋洒洒的文章, ...
- MySQL Bug导致异常宕机的分析流程
原文链接:http://click.aliyun.com/m/42521/ 摘要: 本文主要通过一个bug来记录一下如何分析一个MySQL bug的崩溃信息. 版本:Percona 5.7.17-11 ...
- 一个最不可思议的MySQL死锁分析
1 死锁问题背景 1 1.1 一个不可思议的死锁 1 1.1.1 初步分析 3 1.2 如何阅读死锁日志 3 2 死锁原因深入剖析 4 2. ...
- Mysql查询语句使用select.. for update导致的数据库死锁分析
近期有一个业务需求,多台机器需要同时从Mysql一个表里查询数据并做后续业务逻辑,为了防止多台机器同时拿到一样的数据,每台机器需要在获取时锁住获取数据的数据段,保证多台机器不拿到相同的数据. 我们My ...
- 一次 MySQL 线上死锁分析实战
关键词:MySQL Index Merge 前言 MySQL 的锁机制相信大家在学习 MySQL 的时候都有简单的了解过,那既然有锁就必定绕不开死锁这个问题.其实 MySQL 在大部分场景下是不会存在 ...
随机推荐
- codeforces 691D Swaps in Permutation DFS
这个题刚开始我以为是每个交换只能用一次,然后一共m次操作 结果这个题的意思是操作数目不限,每个交换也可以无限次 所以可以交换的两个位置连边,只要两个位置连通,就可以呼唤 然后连通块内排序就好了 #in ...
- 给Webkit内核的浏览器控件增加互交功能
转载请说明出处,谢谢~~ 昨天封装了基于webkit的wke浏览器内核,做成了duilib的浏览器控件,实现了浏览功能,但是单单的浏览功能还不满足需求,在我的仿酷狗项目中乐库的功能需要与浏览器互交. ...
- 记:Tmall活动页面开发
一.年轻的我 “无人不成商”,如果一个电子商务网站想要做起来,搞活动时必不可少的(引入流量.提高用户黏度.活跃网站氛围),今天打折,明天送红包. (立秋活动,右) 作为一个前端,我当然要从技术的角度来 ...
- 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇05:角色中弹》
5.角色中弹 概述: 为了使游戏具有挑战性,大部分游戏设定中,游戏角色都有生命限制.即在游戏中,由于玩家的操控操控不当,导致游戏角色死亡游戏终止.打飞机游戏也不例外,当敌人击中角色的时候,角色宣判死亡 ...
- CDH5.5.1版HBase安装使用LZO压缩
1.安装 RHEL/CentOS/Oracle 5 Navigate to this link and save the file in the /etc/yum.repos.d/ dire ...
- Transform DataGrid 套用格式
<table class="easyui-datagrid" title="Transform DataGrid" style="width:5 ...
- 现代程序设计 homework-05
本次作业要求设计服务器和客户端,由于之前对网络编程是一窍不通,上上节课听宗学长讲述Tcp的时候心里想这个东西还真是高大上啊一点儿都听不懂,但是上个周末看了看C#网络编程的博客和书之后,发现这个东西入门 ...
- LightOJ 1074 Extended Traffic (最短路spfa+标记负环点)
Extended Traffic 题目链接: http://acm.hust.edu.cn/vjudge/contest/122685#problem/O Description Dhaka city ...
- VHDL TestBench基础(转)
TestBench的主要目标是: 实例化DUT-Design Under Test 为DUT产生激励波形 产生参考输出,并将DUT的输出与参考输出进行比较 提供测试通过或失败的指示 TestBench ...
- 如何把Excel数据转化成SQL语句-转
问题背景 在我们实际的程序开发.维护的过程中,很多时候都要和Excel打交道. 因为用户的数据很多时候是Excel存储的. 公司维护项目的时候,经常要帮客户导入Excel数据,这些数据很多,零 碎,而 ...