問題背景
大約在今年5月份左右饰迹,由于系統(tǒng)同時需要訪問兩個MySQL集群的數(shù)據(jù)艰山,決定使用MyCat作為數(shù)據(jù)庫中間件,因此在一個夜黑風高的晚上勤庐,屏蔽了用戶訪問,準備將系統(tǒng)切換成MyCat好港。整個切換過程其實很簡單愉镰,配置好MyCat配置文件,修改應用的數(shù)據(jù)源訪問連接即可大功告成钧汹。但是用過MyCat的人應該都清楚丈探,MyCat有個很蛋疼的毛病,必須要求MySQL開啟大小寫忽略拔莱,否則啟動后找不到表名大寫的表碗降。不巧的是公司之前DBA要求所有的表名、字段名都必須大寫(OracleDBA塘秦,完全忽略了MySQL的感受)讼渊。沒轍,只能修改表名了尊剔,將大寫全部轉(zhuǎn)換為小寫爪幻。通過一個存儲過程循環(huán)游標構建ALTER TABLE tableName RENAME newName語句來將表名改為大寫。本以為執(zhí)行應該會很快結束,但是整個命令持續(xù)了半個小時挨稿,卡在一張表上不動了搔预,執(zhí)行SHOW ENGINE INNODB STATUS,發(fā)現(xiàn)已經(jīng)出現(xiàn)死鎖叶组。
疑問
這里先來介紹一下ALTER TABLE tableName RENAME newName命令,這一個名副其實的DDL操作历造,但是整個命令的執(zhí)行僅僅是變更INFORMATION_SCHEMA庫中相應字典表的數(shù)據(jù)(例如表名)甩十,且此時沒有任何對表的操作,怎么會導致死鎖問題呢吭产?
毫無思路侣监,但是仔細想想,這個操作應該會修改磁盤上.ibd臣淤、.frm文件的名稱橄霉,否則僅僅是字典表變了也找不到對應的文件。如果說修改文件的話邑蒋,就可能會出現(xiàn)死鎖問題姓蜂,若此時文件被其他進程讀寫占用呢,那就有可能會造成死鎖医吊。
究竟是不是這樣呢钱慢,由于本人對MySQL底層了解的不深,只好求助論壇了卿堂。(以下內(nèi)容摘自論壇)
探索
InnoDB buffer pool中的page管理牽涉到兩個鏈表束莫,一個是lru鏈表,一個是flush 臟塊鏈表草描,由于數(shù)據(jù)庫的特性:
1.臟塊的刷新览绿,是異步操作;
2.page存在兩個版本穗慕,一個是ibd文件的持久化版本饿敲,和buffer pool內(nèi)存中的當前版本。
所以在對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妓美,同步刷下臟塊僵腺;
記錄數(shù)據(jù)字典信息到cfg文件,用于目標端的表結構匹配和驗證壶栋,最后在目標端import的時候辰如,變更page的space,max_lsn等贵试。
2. drop table t
在對表進行刪除的時候琉兜,需要清理掉buffer pool中的page毙玻,但如果表比較大,占用過多的buffer pool桑滩,清理的動作會影響到在線的業(yè)務,所以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操作只是變更了數(shù)據(jù)字典中的table name和文件系統(tǒng)的ibd文件名稱,所以酿愧,在rename的過程中邀泉,不存在對buffer pool中屬于t表的page的同步操作嬉挡,但由于要變更表名,即需要同步對文件的IO操作庞钢。
問題現(xiàn)象:
在MySQL 5.5版本上,error日志大量報出以下的錯誤信息:
查看操作日志颜懊,是一個普通的rename語句操作风皿,但持續(xù)很久,因為rename只是數(shù)據(jù)字典的變更揪阶,除了MDL鎖阻塞以外
不應該持續(xù)這么長時間患朱,pstack查看線程棧信息:
這里我只列了有意義的三個線程:
用戶線程Thread 5
用戶線程確實在進行rename操作裁厅,但阻塞在fil_rename_tablespace函數(shù)中。
master線程Thread 120
InnoDB的master線程阻塞在fil_mutex_enter_and_prepare_for_io函數(shù)中执虹。
IO線程Thread 100
InnoDB的IO線程一共有8個,4個讀侥啤,4個寫線程,發(fā)現(xiàn)都在os_event_wait_low中茬故,也就是都空閑著等待condition中盖灸。
從上面的調(diào)用棧來看,線程之間長時間維持在這種狀態(tài)下磺芭,明顯發(fā)生了死鎖,在我們解這個死鎖之前钾腺,我們先來回顧一點背景知識,然后再說明死鎖的真正原因姻报。
InnoDB背景
checkpoint
由于對數(shù)據(jù)庫的數(shù)據(jù)操作也遵循read-update-write的方式间螟,所以數(shù)據(jù)的更新剧辐,會把buffer pool中的page變成臟塊邮府,由于write-ahead logs機制保證事務的完整性,臟塊的write可以變成異步的褂傀,但又由于buffer pool的大小終究有限,而且對于recovery的時間的要求同波,又要求臟塊的flush又要持續(xù)保證叠国。
MySQL 5.5的版本由master thread來承擔dirty flush的角色, dirty flush的過程就稱為making checkpoint粟焊,lsn的推進保證了recovery的時間不被持續(xù)的變長。刷新的策略悲雳,受到當前IO pending的情況香追,double write-buffer是否打開,buffer pool中dirty page所占的比例透典,以及innodb_max_dirty_pages_pct參數(shù)的設置,進行靈活刷新峭咒,具體的代碼細節(jié),這里就不展開了钙皮。
異步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順序?qū)懭氲絠bdata系統(tǒng)表空間中,如果完成之后系統(tǒng)crash鹦付,可以使用持久化的double write-buffer進行page恢復;
開始把 double write-buffer中的page郎嫁,寫入真正的ibd文件中潘明。依次提交異步IO操作秕噪,提交IO操作的步驟分為:
持有fil_system mutex,判斷當前tablespace是否可用腌巾,
判斷當前fil_space的stop_io標示,如果設置就循環(huán)等待
如果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層挚躯,首先使用自治事務變更數(shù)據(jù)字典擦秽,包括SYS_TABLES漩勤,SYS_FOREIGN缩搅;
變更數(shù)據(jù)字典的內(nèi)存對象,包括table, index, foreign list等誉己;
變更fil_space對象以及對應的ibd數(shù)據(jù)文件名稱,其中變更文件系統(tǒng)名稱的時候:
設置當前的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操作黄锤,只變更數(shù)據(jù)字典和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操作,我已經(jīng)提交了IO請求柬脸,user thread要進行rename可以先hold毙驯,等IO完成。
假設下面的時序:
master thread提交了1個IO請求爆价,設置了n_pending媳搪;
rename操作設置stop_io,判斷n_pending>0 就等待序愚;
master thread需要提交剩下的幾個IO等限,發(fā)現(xiàn)stop_io已設置芬膝,就等待形娇;
由于master thread沒有提交完這批IO,沒有喚醒IO helper thread桐早,導致第1個IO請求無法完成,n_pending一直等于1友存;
rename操作因為n_pending一直等于1陶衅,陷入了死等;
master thread發(fā)現(xiàn)stop_io等于true万哪,陷入了死等。
具體的代碼可以參考:
修復方法
修復的方法也比較簡單吟策,在fil_rename_tablespace的時候的止,如果發(fā)現(xiàn)node->n_pending > 0的時候诅福,在sleep之前匾委,發(fā)起一次喚醒動作氓润,即os_aio_simulated_wake_handler_threads,IO helper thread去完成master thread已經(jīng)提交的IO請求挨措,這樣n_pending就會降到0崩溪,死鎖就解開了。