標籤:

MySQL 5.7版本XA事務若干bug分析

MySQL 5.7版本XA事務若干bug分析

來自專欄資料庫內核10 人贊了文章

MySQL 5.7實現了對XA事務的完美支持,通過將XA PREPARE操作記錄在Binlog文件來解決prepare事務的持久化問題。MySQL 5.7 XA事務目前已經有多篇文章進行了介紹,可參考 mysql-5-7-完美的分散式事務支持,連接斷開導致XA事務丟失等。本文不對XA事務做全面介紹,而是分析MySQL 5.7版本存在的bug,相信對大家使用MySQL XA事務會有所幫助。

group commit相關問題

首先是一個和組提交相關的bug,該bug在官方最新的MySQL 5.7.22版本仍存在未修復的XA bug bug#88534,網易內部的兄弟部門也遇到過(詳見復現鏈接)。該bug的定位過程也能夠了解MySQL的XA事務PREPARE和COMMIT的具體實現。

下面是對bug#88534的描述,針對所述這兩點,我們將一一詳細展開分析::

1 ha_prepare function first prepare the binlog, then the engines.

If theres error or mysqld crashed when the engine prepare, the engine prepared info would lost. But the slave would receive the XA binlog.

2. As ha_prepare function call binlog_prepare first, the thd->durability_property would be HA_IGNORE_DURABILITY. Then the engines prepare would not flush logs. Event though the client receive success after issue XA prepare, the prepared transaction may also lost after server crash. And this would also cause slave replication not consistent.

xa prepare crash問題

第一點的意思是說XA Prepare的時候會調用函數ha_prepare,在該函數中會先進行Binlog prepare,然後再進行引擎層的prepare,如果Binlog prepare成功後,在執行引擎層prepare時出錯或mysqld crash了,引擎層的prepare信息會丟失,但Slave端仍會收到該XA事務的Prepare Binlog信息。這段話直接那代碼來說明,描述比較模糊。我們看下ha_prepare,很容易可以發現其所述的代碼段如下圖:

依次調用該事務所涉及的handlerton的prepare函數,那麼Binlog和引擎層(InnoDB)prepare函數對象是什麼呢,通過分析可知分別為binlog_prepare和innodb_xa_prepare,通過在執行XA Prepare時加gdb斷點確認確實如此:

所以,第一點就變為:ha_prepare函數會先調用binlog_prepare進行XA事務的Prepare操作,然後再調用引擎層的XA Prepare函數innodb_xa_prepare,假設在binlog_prepare成功返回後,執行innodb_xa_prepare時出錯或mysqld crash了,引擎層的prepare信息會丟失,但Slave端仍會收到該XA事務的Prepare Binlog信息。從後半句的描述可以猜測,在binlog_prepare函數中會將Binlog信息寫到Binlog文件中,其實熟悉MySQL group commit實現的同學知道,binlog_prepare函數在諮詢每條DML操作時都會調用,目的是為了更新事務的last_committed信息。另外我們也知道,MySQL 5.7的XA事務在做prepare的時候,會記Binlog,那麼很顯然,對於XA Prepare操作,binlog_prepare函數還會寫Binlog文件。正常來說,(還是與MySQL group commit實現有關)事務的Binlog是在MYSQL_BIN_LOG::ordered_commit函數中寫入Binlog文件的(group commit三部曲:flush,sync,commit)。是不是進行XA Prepare的時候binlog_prepare調用了ordered_commit函數,我們再加個斷點進行調試:

進一步看binlog_prepare的代碼發現,在參數all為true,且是XA Prepare時,會調用MYSQL_BIN_LOG::commit:

在commit函數中,會在事務的Binlog Cache中寫入XA PREPARE信息:

接著調用MYSQL_BIN_LOG::ordered_commit,注意,此時的傳入參數為(this=this@entry=0x1fb23c0, thd=thd@entry=0x9b56b70, all=all@entry=true, skip_commit=skip_commit@entry=true),skip_commit被賦予thd->get_transaction()->m_flags.commit_low(= !skip_commit),這也就是意味著,跟普通事務的ordered_commit不一樣,由XA Prepare觸發的該流程僅完成Binlog的持久化(並更新Binlog文件位置信息,從而dump線程可拉取最新的Binlog發送給Slave,詳見「semi-sync下是否會有此Bug」小節分析),不會進行引擎層的事務提交,另外,前面已經說了,XA Prepare是先進行Binlog prepare再InnoDB prepare,沒有進行prepare的事務也不應該直接進行commit。

binlog_prepare函數返回後,就以為這Binlog已經持久化,再繼續調用引擎層的prepare函數,最終會調用InnoDB事務系統的trx_prepare_low進行事務的prepare,調用棧如下:

如果進行innodb_xa_prepare時執行出錯,那麼會調用ha_rollback_trans回滾該事務,但由於binlog已經prepare了,在主從複製場景下,該Binlog會通過dump線程傳到Slave端。那麼就會出現主上正確得處理了執行出錯的prepare,但Slave上如果萬一這個錯誤的prepare執行成功了,那麼就會導致Slave端有個prepare事務,該事務會一直處於prepare狀態;如果執行innodb_xa_prepare時mysqld crash了,reboot後,由於InnoDB存儲引擎層,該事務未處於Prepare狀態,InnoDB在recv的undo處理階段,會將此活躍事務回滾掉,此時也會出現前述的情況,及Slave端該事務正常Prepare了,Master端回滾了。兩種情況都會導致主從數據不一致。不過由於Slave端事務處於Prepare階段,對用戶是不可見的,所以該情況造成的影響較小。

xa prepare數據未持久化

我們再來看下第二點:

2. As ha_prepare function call binlog_prepare first, the thd->durability_property would be HA_IGNORE_DURABILITY. Then the engines prepare would not flush logs. Event though the client receive success after issue XA prepare, the prepared transaction may also lost after server crash. And this would also cause slave replication not consistent.

意思是由於binlog_prepare調用MYSQL_BIN_LOG::ordered_commit時,將thd->durability_property設置為HA_IGNORE_DURABILITY(如下圖),導致InnoDB在進行prepare的時候不會持久化redo log(通過在trx_flush_log_if_needed_low設置斷點發現並沒有觸發)。也就是說,XA Prepare返回時,雖然引擎層已經將事務狀態設置為Prepared,但是並沒有將相應的日誌落盤,此時如果mysqld所在的伺服器crash了(由於redo採用buffer IO的方式,即會寫到page cache,所以mysqld宕機不會造成對應的日誌丟失),reboot後InnoDB recv時也可能會回滾對應的事務,即問題一的情況。

xa commit crash問題

除了bug中所述的這兩點問題,我們還發現了另一個問題,那就是如果在XA Commit時mysqld crash了,雖然此時XA Commit對應的Binlog已經寫了,但不會像普通事務一樣,在mysqld recovery階段通過Binlog和InnoDB中處於Prepared狀態的事務進行比對來提交對應的事務,即InnoDB中已經prepare的事務,如果該事務的Binlog已記錄在Binlog中,則MySQL Server層會執行Commit提交這些事務。通過查看代碼發現了其中的原因,Server層調用MYSQL_BIN_LOG::recover函數來執行前面所說的操作,它會遍歷最後一個Binlog文件,收集記錄了提交日誌的事務信息,然後調用InnoDB註冊的函數ha_recover完成這些事務在引擎層的提交。

細心的同學已經發現了,遍歷Binlog文件時,只會收集XID_EVENT記錄的事務信息。而對在QUERY_EVENT中的提交信息並沒有進行相應的處理。因為後者是有非事務引擎提交的事務。如下圖:

而XA COMMIT也是記錄在QUERY_EVENT中的,導致掃描的時候過濾掉了對應的XA事務。

這就是XA COMMIT無法crash safe的原因。

PS:最近在看淘寶的MySQL內核月報發現,有篇文章也建了本文所述的問題。mysql.taobao.org/monthl

修復xa prepare問題

至此,XA事務相關的問題都已經闡述了,應該說這些問題並不會對線上應用產生大的影響。而且出現的概率也比較小。這也是網易杭研截止目前沒有發現的原因。那麼如果要進行修復,應該怎麼做呢,bug鏈接中也附上了對應的說明:

相對來說,第二種方案更加友好。簡單說來就是在XA PREPARE時,調整prepare的順序,先讓InnoDB進行prepare,然後再Binlog prepare。但如果在Binlog完成prepare前,mysqld crash了,由於此時還沒有記錄Binlog,所以,mysqld reboot後需要將引擎層已經prepare的事務回滾掉。所以,需要基於Binlog文件中是否記錄XA PREPARE日誌來決定回滾掉引擎層已prepare的事務。但是Binlog文件的rotate操作不會受XA事務是否已提交的影響,也就是說,系統中還有處於prepare狀態的XA事務時,Binlog文件能夠正常rotate。這樣的話就無法僅掃描最後一個Binlog文件來獲取XA事務是否已經記錄了PREPARE Binlog信息,否則可能會導致XA事務被錯誤回滾。所以,在方案中,引入了一個新的系統表在Binlog rotate時記錄處於prepare的XA事務。

修復xa commit問題

對於XA COMMIT無法crash safe的問題,只需要修改XA COMMIT Binlog類型或者故障恢復時識別QUERY_EVENT並匹配「XA COMMIT」,解析其中記錄的XID信息。諮詢了MySQL官方開發,這個問題會在後續版本修改。

xtrabackup與XA相關的bug和選項

這是一個很容易被忽視的場景。DBA在對MySQL實例進行運維時發現使用xtrabackup備份恢復從庫,跟主庫建立複製關係時報複製出錯,提示xid不存在。這個問題第一個感覺是不是因為前述的bug(xa prepare命令返回後redo未落盤)導致:xa prepare返回後剛好開始進行xtrabackup備份,由於此時redo可能還未落盤,也就是說引擎層事務並不是prepare狀態,導致事務被回滾,從而出現xid找不到的情況,從Binlog文件和備份保留的gtid信息也能夠確定xa prepare在gtid集合中,而xa commit是在起複制之後。但再仔細一想,xtrabackup進行備份的時候,是會調用「FLUSH NO_WRITE_TO_BINLOG ENGINE LOGS」刷redo的,所以,在xtrabackup場景下xa prepare返回redo未落盤不是問題。那麼問題有出在哪裡呢?就在陷入絕境的時候,谷歌助攻了一把,騰訊有個兄弟反饋xtrabackup的時候xa prepare真會出問題,簡單說來就是xtrabackup加的全局鎖無法阻止xa prepare,也就是說xtrabackup進行備份的時候,xa prepare可以正常執行。那麼就很容易理解了。詳細分析可參考該文獻qkxue.net/info/207119/M。反饋給DBA,並詳細解釋了下,確實讓人信服。但我還是不放心,於是用InnoSQL 5.7.20版本試了下,結果卻是執行「FLUSH NO_WRITE_TO_BINLOG ENGINE LOGS」後,xa prepare卡住了!!!!基於鏈接中的patch看了5.7.20源碼,發現這個bug已經修復了。

所以,問題又重新回到原點。但最終在DBA的助攻下找到了真正的原因。這其實不是什麼bug,而是跟xtrabackup的redo-log選項有關,默認該選項為false,意味著xtrabackup在恢復的時候會把已經完成xa prepare的事務也回滾掉,意不意外??但確實就是這樣的,詳見日誌記錄:

最後,對比了下xtrabackup和MySQL的代碼,官方的代碼如下:

xtrabackup代碼如下:

也就是說,xtrabackup修改了InnoDB引擎的故障恢復邏輯,掃描undo的時候,對於prepared狀態的事務,會將其設置為active狀態。所以,對於使用xtrabackup進行MySQL從庫重建時一定要留意redo-log這個參數。

其他相關問題分析

group commit prepare優化

本文所述的第一個XA事務bug,與MySQL的group commit(組提交)機制有密切的關係。在分析了XA事務的bug後,還有一個關於組提交機制的疑問:上面提到,在prepare階段設置了HA_IGNORE_DURABILITY,不會刷redo到磁碟上,那麼這會不會導致普通事務在Commit時寫了Binlog,由於redo沒有刷盤導致數據丟失呢?回答顯然是不會。這就是MySQL 5.7著名的group commit prepare優化。即在prepare的時候不刷redo,減少一次獨自刷盤操作(與上面的有所不同,這裡的HA_IGNORE_DURABILITY是在MYSQL_BIN_LOG::prepare設置的),而是選擇在group comit的Binlog flush stage進行redo持久化,即在Binlog持久化前完成redo持久化(此時刷盤,可以將多個事務的redo一起刷掉,大大提高了效率),這樣就確保了數據不丟失,寫了Binlog就一定能夠通過redo恢復事務數據,同時又進一步提高了group commit的性能。對應的調用棧信息如下所示:

semi-sync下是否會有此Bug?有

上面我們已經了解在傳統的非同步複製模式下,該Bug可能會導致主從的XA事務狀態不一致。那麼在semi-sync場景下,該Bug是否仍存在。這個問題的本質是與semi-sync的Binlog傳輸和ACK機制有關。

首先,需要了解的是主上的dump線程何時將事務的Binlog發送給Slave。dump線程通過Binlog_sender::send_binlog(IO_CACHE *log_cache, my_off_t start_pos)來執行從start_pos開始的Binlog dump操作。其中會調用Binlog_sender::get_binlog_end_pos(IO_CACHE *log_cache)來循環等待Binlog文件的位置被更新,最終是通過信號量mysql_cond_t update_cond來實現。而在進行事務組提交MYSQL_BIN_LOG::ordered_commit中,有2處調用了update_binlog_end_pos(my_off_t pos)用來喚醒在update_cond上的線程。這兩處分別在flush stage和sync stage,具體在哪個stage喚醒等待位置更新的線程由參數sync_binlog決定,如果為1,則表示每完成一次事務組提交都需要進行Binlog持久化,對應得是在sync stage階段完成Binlog持久化後更新Binlog位置為本次組事務提交寫入偏移,從而喚醒dump線程向Slave傳輸本次事務組提交產生的Binlog。若sync_binlog不為1,則在flush stage階段,將參與組提交的事務的Binlog寫入到Binlog文件後,即更新Binlog位置,也就是說,Binlog無需先持久化再傳給Slave。對於所討論的這個Bug,由於在binlog_prepare中會調用MYSQL_BIN_LOG::ordered_commit,所以在完成binlog_prepare後,不管sync_binlog設置為何值,此時Binlog均可能已經傳給Slave。crash只會影響主上的Binlog文件的數據一致性。

其次,再來了解semi-sync的ACK機制。簡單來說,若主從均開啟了semi-sync,那麼在Slave連上主庫複製Binlog時,主庫能夠通過通過連接信息獲取該Slave是否開啟了semi-sync,進而在dump線程給Slave發送Binlog的某些場景(比如事務提交時。注意,並不是每次Binlog傳輸都需要Slave提供ACK)下設置ACK回復的狀態。若主庫需要Slave回復ACK,則可能在sync stage或者commit stage來等待Slave的ACK信息,具體在哪個stage等待由參數rpl_semi_sync_master_wait_point決定。如果設置為AFTER_SYNC,則在sync stage完成Binlog持久化後等待ACK。

若是AFTER_COMMIT,則在完成引擎層提交後再等待Slave的ACK。對於所討論的Bug,由於在binlog_prepare中會調用MYSQL_BIN_LOG::ordered_commit,innobase_xa_prepare在binlog_prepare之後,且redo log持久化標誌為HA_IGNORE_DURABILITY,所以,即使設置為AFTER_COMMIT模式,仍然會有問題。

MGR下是否會有此Bug?

不管非同步複製還是semi-sync,Binlog都是由主上的dump線程發送,Slave的io線程接收的。但MGR模式下並非如此,本地事務的Binlog是通過before_commit這個hook發送到底層的xcom/paxos協議上。

也就是說,before_commit的時候,事務的Binlog還沒有寫入到Binlog文件,更談不上是持久化。所以,MGR下不會出現事務的Binlog信息已經發送給Slave,但引擎層還未Prepare的情況。這也就是避免了在進行XA PREPARE時完成了binlog_prepare在innobase_xa_prepare的時候crash,reboot後出現的主從XA事務狀態不一致問題,因為前面已經提到,此時該XA PREPARE操作會在引擎層被回滾掉。由於在binlog_prepare時,Binlog已經走了paxos協議,所以其他節點會正常執行XA PREPARE,本節點在MGR recv階段,會基於gtid_executed信息,從其他節點拉取本節點還未執行的事務Binlog信息進行catchup,從而補上因為crash而回滾的XA PREPARE。

總結

本文分析了MySQL 5.7上發現的XA事務相關bug,有跟組提交相關的,也有跟xtrabackup備份恢復相關的,通過詳細分析,相信讀者在遇到類似問題的時候可以很快找到原因所在。了解了問題的起因後,也能夠在使用XA事務的時候進行規避。本文還介紹了MySQL組提交優化和 5.7 XA事務prepare和commit的代碼實現,對MySQL源碼感興趣的同學歡迎交流探討。

推薦閱讀:

你的MYSQL 有定期清理過binlog日誌嗎?
用elastic stack來分析下你的redis slowlog
InnoDB 存儲引擎原理解析
14.6.3 InnoDB Buffer Pool 配置
mysql如何使用臨時表,內存表來加快速度?

TAG:MySQL |