【轉載】MySQL 外部XA及其在分佈式事務中的應用分析

  1. XA原理

關於XA,分佈式事務處理的原理,可見[3];關於MySQL XA的說明,可見[1][2]。html

 

MySQL XA分爲兩類,內部XA與外部XA;內部XA用於同一實例下跨多個引擎的事務,由你們熟悉的Binlog做爲協調者;外部XA用於跨多MySQL實例的分佈式事務,須要應用層介入做爲協調者(崩潰時的懸掛事務,全局提交仍是回滾,須要由應用層決定,對應用層的實現要求較高);mysql

 

本文,假設讀者已經知道MySQL外部XA的使用,而將重點放在MySQL如何處理外部XA的crash recover,以及面對不一樣的crash recover的情形,應用程序如何處理,纔可以保證分佈式事務的一致性。最後,本文簡單分析一下目前MySQL外部XA支持存在的問題,以及可選的解決方案。sql

 

源代碼分析基於MySQL 5.1.49,MySQL 5.5.16。數據庫

 

  1. MySQL處理流程

    1. MySQL 外部XA –正常處理流程

MySQL外部XA的正常處理流程,這裏不許備介紹,能夠參考[1][2][3]。接下來我重點描述一下MySQL外部XA的崩潰恢復流程,畢竟此流程跟應用程序如何正確使用外部XA息息相關。緩存

  1. MySQL外部XA –崩潰恢復流程

若一個運行外部XA事務的MySQL節點發生崩潰,那麼其重啓以後的崩潰恢復,涉及到外部XA處理的流程以下:安全

Crash recover:服務器

 

// 1.    讀取binlog文件,將文件中的xid存入commit_list hash表網絡

//     顧名思義,所謂的commit_list,就是說此list中對應prepare狀態的xidjsp

//    在崩潰恢復過程當中都可以被提交,而不在commit_list中的xid,均須回滾分佈式

//     binlog中的xid,都是屬於內部xid,由MySQL產生,用於內部XA

Log.cc::TC_LOG_BINLOG::recover

 

// 2.    遍歷底層全部的事務引擎,收集處於XA_PREPARED狀態的全部xid

//    這些xid列表,既包括內部xid,也包括外部xid,存儲引擎內部不作區分

Handler.cc::ha_recover(commit_list)

 

// 執行各引擎層面提供的recover方法,收集全部的處於prepared狀態的xid

// 根據xid分類:

// 3.    若xid屬於內部xid,那麼在commit_list中查找此xid,

//    若存在,則提交此xid對應的事務;不然,回滾此事務

// 4.    若xid屬於外部xid,那麼則將xid插入xid_cache hash表

//    xid_cache中的全部xid,將會經過xa recover命令返回,等待外部程序決策

Handler.cc::xarecover_handlerton

 

    // 5.    收集InnoDB引擎中,處於prepare狀態的全部xid,並返回

    got = hton->recover(innobase_xa_recover)

 

    my_xid x = info->list[i].get_my_xid();

    if (!x)

        // 若當前爲外部xid,那麼將xid插入xid_cache hash表

xid_cache_insert(&xid_cache, x);

    else

        if (x in commit_list)

            // 若當前爲內部xid,同時此xid在binlog中存在,則提交

            hton->commit_by_xid();

        else

            // 若當前爲內部xid,同時此xid在binlog中不存在,則回滾

            hton->rollback_by_xid();

 

經過以上的分析,能夠總結出:

  • MySQL內部,會對xid作區分。內部xid有MySQL本身產生(MySQL內部xid格式,將在本文下面給出),用於多引擎間事務的一致性;外部xid由應用程序給出,用於跨多MySQL實例的分佈式事務。可是存儲引擎層不作區分(區分在MySQL上層)。

     

  • crash recover時,存儲引擎負責將引擎內部,處於prepare狀態的事務收集,並返回MySQL上層。

     

  • Binlog做爲內部XA的協調者[5],在binlog中出現的內部xid,在crash recover時,由binlog負責提交;在binlog中未出現的xid,由binlog負責回滾。(這是由於,binlog不進行prepare,只進行commit,所以在binlog中出現的內部xid,必定可以保證其在底層各存儲引擎中已經完成prepare)。

     

  • 外部XA事務的xid,在crash recover過程當中僅僅是插入xid_cache中,而不作其餘處理。等到用戶發起xa recover命令時,將xid_cache中處於prepare狀態的xid返回。

     

  • xa recover命令的流程處理以下。

 

xa recover命令處理流程:

 

sql_parse.cc::mysql_execute_command

    case SQLCOM_XA_RECOVER:

        mysql_xa_recover();

            // 遍歷xid_cache,找出其中的狀態處於XA_PREPARED的事務,發送客戶端

            while (xs = hash_element(&xid_cache,))

                if (xs->xa_state == XA_PREPARED)

                    protocol->write();

 

根據xa recover命令收集到的各MySQL實例返回的xid列表,而後再對比應用程序端日誌,決定這些xid,哪些全局commit,哪些rollback。

 

因爲測試中只有一個MySQL實例,所以此時能夠直接選擇commit處於prepare狀態的xid。

 

  1. MySQL內部xid格式

上面提到,MySQL有外部XA與內部XA,內部XA對應的xid由MySQL內部產生,有特定的格式:

  • MySQL內部xid格式:    MYSQL_XID_PREFIX + server_id + my_xid

    MYSQL_XID_PREFIX:    MySQLXid(源碼寫死)        8 bytes

    server_id:                MySQL實例的id,ulong,        4 bytes

    my_xid:                內部自增序列,ulonglong,    8 bytes

     

    MySQL內部xid由以上3部分組成,總長度爲20。

    判斷是否爲內部xid的代碼以下:

    gtrid_length == MYSQL_XID_GTRID_LEN

    &&bqual_length == 0

    &&!memcmp(data, MYSQL_XID_PREFIX, MYSQL_XID_PREFIX_LEN)

     

    其中:MYSQL_XID_GTRID_LEN = 20;MYSQL_XID_PREFIX_LEN = 8;

     

     

    例如:「MySQLXid 0004「

    server_id = 」;my_xid = 4

     

    所以,使用時應該注意,不要在外部構造這種形式的xid,不然MySQL就會將內部xid與外部xid混淆。

     

    在測試中,我構造了一個外部xid = ‘MySQLXidxxxx00100000′,長度爲20 bytes,前八個字符爲‘MySQLXid’。在事務完成xa prepare以後,關閉MySQL數據庫。MySQL在crash recover時,直接將此xid認爲是內部xid,並在內部由Binlog直接rollback此事務,致使使用xa recover命令沒法看到任何prepare狀態的xa事務。

     

    可是,反過來考慮,如果應用程序自己不想處理懸掛事務,那麼將外部xid構形成內部的形式不失爲一種較好的策略,由binlog來負責處理懸掛事務的提交與回滾。付出的代價則是:崩潰時,未提交事務在各個MySQL實例上的狀態可能不一致(部分節點提交;部分節點回滾)。

 

  1. MySQL 崩潰恢復& Binlog

前面提到了MySQL外部XA的崩潰恢復流程。在本小節咱們簡單分析一下崩潰恢復過程當中的Binlog文件的讀取問題。

 

經過跟蹤TC_LOG_BINLOG::open函數,發如今crash recover過程當中,MySQL全量讀取最後一個Binlog文件,這與MariaDB WorkLog#164:Extend crash recovery to recover non-prepared transactions from binlog[6]中的說法一致:...The existing scan always scans the full last binlog file, and we should keep this...

 

可是這樣就帶來一個疑問:

爲何僅僅全量讀取最後一個Binlog文件就能夠呢?若是最後一個binlog文件很短,如何保證底層引擎處於prepare狀態的事務不會出如今前一個Binlog文件之中?

 

回答這個疑問,須要從目前MySQL寫Binlog與底層存儲引擎(InnoDB)寫redo log的方式分析:

  1. 同一事務只能寫到同一個Binlog文件中,不能跨文件。
  2. 爲了保證底層引擎Commit順序與Binlog順序一致,目前MySQL+InnoDB不支持group commit(新版的Precona,MariaDB除外),同一時間只有一個事務能夠進行提交(內部的XA事務,二階段提交):InnoDB prepare + Binlog flush + InnoDB commit這一系列操做。所以下一個事務開始進行InnoDB prepare時,前一個事務的系列動做必定結束,事務已經提交。意味着crash recovery時,最多隻有一個InnoDB事務處於prepare狀態。
  3. 結合1,2可得,最後一個prepare事務必定位於最後的Binlog文件中。

 

上面說到,因爲MySQL+InnoDB不支持group commit,所以只讀最後一個Binlog是可行的,那麼若是是最新版的Percona/MariaDB,已經支持group commit (關於group commit的具體實現,能夠參考個人另一篇短文:MariaDB&PerconaXtraDB Group Commit實現簡要分析[7]),那麼仍舊讀取最後一個Binlog文件是否同樣可行呢?

 

答案是確定的,由於目前Percona/MariaDB的最新版本實現中,仍舊採用的是全量讀取最後一個Binlog文件的策略,那麼此時又是如何保證前一個Binlog文件中全部的日誌對應的事務,其在底層InnoDB引擎中已經完成提交動做了呢?

通過閱讀MariaDB 5.3.4的代碼,我找到了答案:

  1. 同一事務只能寫在同一Binlog文件中,不能跨文件,這個要求仍舊保留。
  2. Binlog在進行group commit時,須要統計參與本次group commit的全部內部XA事務的數量(prepared_xids,何用?)。
  3. 若當前Binlog文件已經超出指定的大小,須要切換,那麼在切換以前,必須等待當前Binlog文件對應的prepared_xids歸零(換句話說,也就是要保證當前Binlog文件中的全部內部XA事務,在存儲引擎中所有提交,完成commit & fsync)。如此一來,就可以保證切換到新的Binlog文件以後,老的Binlog文件對應的因此事務,都已經肯定提交。
  4. prepared_xids歸零前提?要讓prepared_xids歸零,首先必須將新的Binlog group commit暫停,經過對LOCK_log mutex加鎖便可實現(LOCK_log mutex功能可見[7],新的binlog group commit開始前,必須得到此mutex)。
  5. prepared_xids歸零操做?Binlog模塊(TC_LOG_BINLOG)提供一個unlog方法,該方法每調用一次,對prepared_xids –,直到prepared_xids歸零,便可進行binlog文件的切換操做。每一個事務,在完成全部的commit步驟(包括底層的存儲引擎commit),返回用戶以前,調用此方法;若binlog group commit中有事務失敗,一樣調用此方法。所以,只要binlog中的事務對應的底層引擎所有完成commit,prepared_xids必定爲0,也意味着能夠切換Binlog文件。
  6. 總結:group commit下的crash recovery,一樣只須要遍歷最後一個Binlog文件便可。MariaDB在實現group commit的過程當中,已經改動binlog的實現,用於支持此方法。

 

一樣仍是在MariaDB WL#164[6]中,提到了遍歷binlog的一種優化,目前,InnoDB redo log在commit日誌中已經記錄了對應的Binlog日誌的(文件名,位置)信息。只要將此信息返回,就能夠從指定位置開始遍歷Binlog,如此一來,使用更大的Binlog文件,也不會影響crash recovery時,讀取Binlog文件的性能。

  1. MySQL 外部XA分析

    1. 做用分析

MySQL外部XA能夠用在分佈式數據庫代理層,例如開源的代理工具:ameoba[4],網易的DDB,淘寶的TDDL,B2B的Cobar等等。

 

經過MySQL外部XA,這些工具能夠提供跨庫的分佈式事務。固然,這些工具也就成了外部XA事務的協調者角色。在crash recover時控制懸掛事務是全局commit,或者rollback。

 

在crash recover以後,外部應用程序可能會遇到如下幾種狀況:

 

  • 狀況一:分佈式事務對應的MySQL實例,部分完成prepare,部分未完成prepare。此時直接回滾完成prepare的實例便可。n_prepared <Total Nodes (處於prepare狀態的節點數量要小於參與分佈式事務的全部節點總數)。

     

  • 狀況二:分佈式事務對應的MySQL實例,所有完成prepare,未開始進行commit。此時便可提交此事務,也可回滾此事務(根據分佈式事務原理,全部節點都完成prepare,應該提交)。n_prepared = Total Nodes。

     

  • 狀況三:分佈式事務對應的MySQL實例,所有完成prepare,而且部分節點已經完成commit。此時應該提交該事務處於prepare狀態的節點。n_prepared < Total Nodes。對比狀況三與狀況一,僅僅經過prepare節點的數量沒法區分,所以應用程序須要在prepare完成以後記錄日誌(此時,應用程序起着事務協調者(Transcaction Coordinator)的角色,而根據 MariaDB WorkLog#132[5]的說法,TC角色是能夠進行」middle engine」優化的,不須要prepare過程,全部MySQL節點xa prepare返回以後,應用程序直接寫commit標識便可,而後再對每一個MySQL節點進行xa commit操做。),從而用於區分狀況一與狀況三。

     

  • 狀況四:分佈式事務對應的MySQL實例,所有完成commit。此時事務已經提交成功,xid不會出如今執行xa recover的任一個節點。不須要特殊處理。

 

  • 狀況五:未記錄任何prepare日誌。那麼全部的事務,在各個存儲引擎的crash recover時,都會被回滾,不須要外部特殊處理。
  1. MySQL外部XA不足

經過前面的分析,可知應用程序配合MySQL的XA事務功能,可以較好的支持分佈式環境下的事務。可是,這個支持並不完美,根據個人分析,有可能會出現如下幾個問題:

 

  • 問題一:主備庫數據不一致。

    MySQL的主備庫的同步,經過Binlog的複製完成。而Binlog是MySQL內部XA事務的協調者,而且MySQL爲binlog作了優化——binlog不寫prepare日誌,只寫commit日誌。

     

    考慮前面提到的狀況二,全部的參與節點prepare完成,在進行xa commit前crash。crash recover若是選擇commit此事務。因爲binlog在prepare階段未寫,所以主庫中看來,此分佈式事務最終提交了,可是此事務的操做並未寫到binlog中,所以也就未能成功複製到備庫,從而致使主備庫數據不一致的狀況出現。

     

    在MySQL 5.5.16版本中作過測試,這個問題實際存在。crash recover以後,對xa recover返回的事務運行xa commit,對應事務提交,可是操做並未寫入binlog,所以沒法複製到備庫。

     

    那麼是否回滾全部prepare的事務,就能夠避免此問題呢?結論是仍舊不行,不只不能解決問題一,甚至可能引發問題二。

 

  • 問題二:同一事務,在各參與節點,最終狀態不一致(部分提交,部分回滾)。

    若回滾全部prepare狀態的分佈式事務,會產生問題二。考慮狀況三(全部節點完成prepare,部分節點完成commit),該分佈式事務對應的節點,部分已經提交,沒法回滾,而部分節點回滾。最終致使同一分佈式事務,在各參與節點,最終狀態不一致。

 

  • 問題三:源碼級別問題。MySQL 5.1.49源碼對於外部XA事務處理存在bug,在MySQL 5.5.16版本中,此bug已經被fix。通過驗證發現,在我已下載的MySQL 5.1.61與以後的全部版本,此bug均已經被fix。

    在MySQL 5.1.49中,全部xa recover返回的外部xid,都不能被提交。緣由以下:

     

    當運行xa commit ‘xid_name’命令時,MySQL會判斷當前xid_name的錯誤信息,若存在錯誤信息,那麼就在內部將xa commit命令強制轉換爲xa rollback。xid_name的狀態存於xid_cache中,在crash recover階段,由函數Handler.cc::xarecover_handlerton調用xid_cache_insert(&xid_cache, x)函數完成插入。MySQL 5.1.49在實現xid_cache_insert函數有bug。

                …

    xs->xa_state=xa_state;

            xs->xid.set(xid);

            xs->in_thd=0;

            xs->rm_error=0;

                res=my_hash_insert(&xid_cache, (uchar*)xs);

                    …

    MySQL 5.1.49中,缺乏了xs->rm_error =0這一行,未初始化rm_error,致使xa commit時判斷出錯,沒法commit。MySQL 5.5.16已經fix此bug,加上了黑色這一行的初始化,應用程序能夠xa commit。

  1. 不足的解決方案

從MySQL外部XA不足的分析能夠看出,除了實現bug以外,產生其他兩個問題的最大緣由,仍是在於MySQL針對binlog作的」middle engine」優化,binlog的prepare不寫日誌。在MySQL內部XA事務中,這個優化是可行的,由於Binlog自己的角色就是事務協調者(Transaction Coordinator),事務協調者能夠不進行prepare [5]。

可是對於MySQL外部XA事務,Binlog已經不是事務協調者的角色,其也是一個參與者,或者說是Resource Manager。所以Binlog的prepare日誌是不可省略的。

爲了解決MySQL外部XA事務crash recover過程當中出現的問題,我以爲只能修改binlog模塊。使binlog模塊在正常運行過程當中也區份內部XA事務與外部XA事務。內部XA事務能夠仍舊沿用如今的方案;而外部XA事務,須要增長寫prepare日誌的功能,已經crash recover時處理prepare日誌的功能。

  1. 參考資料

[1] Sergei Golubchik.Distributed Transaction Processing with MySQL XA

[2] http://dev.mysql.com/doc/refman/5.1/en/xa.html

[3] X/Open.Distributed TP: The XA Specification

[4] 陳思儒. Amoeba

[5] MariaDB WorkLog#132: Transaction coordinator plugin

[6] MariaDB WorkLog#164: Extend crash recovery to recover non-prepared transactions from binlog

[7] 何登成. MariaDB&PerconaXtraDB Group Commit實現簡要分析

 

轉載來自:http://hedengcheng.com/?p=136

 

 

 

 

 

《高性能MySQL(第二版)》關於分佈式XA事務的說明

5.11  分佈式(XA)事務

Distributed(XA) Transactions

存儲引擎事務在存儲引擎內部被賦予了ACID(譯註1)屬性,分佈式(XA)事務是一種高層次事務,它能夠利用兩段提交的方式將ACID屬性擴展到存儲引擎外部,甚至數據庫外部。MySQL 5.0及其以上的版本部分支持XA事務。

XA事務須要事務協調員,它會通知全部的參與者準備提交事務(階段一)。當協調員從全部參與者那裏收到"就緒(Ready)"信號時,它會通知全部參與者進行真正的提交(階段二)。MySQL能夠是XA事務的參與者,但不能是協調員。

MySQL內部其實有兩種XA事務。MySQL服務器能參與由外部管理的分佈式事務,但它內部使用了XA事務來協調存儲引擎和二進制日誌。

 

5.11.1  內部XA事務

Internal XA Transactions

MySQL內部使用XA事務的緣由是服務器和存儲引擎之間是隔離的。存儲引擎之間是徹底獨立的,彼此不知道對方的存在,因此任何跨引擎的事務本質上都是分佈的,而且要求第三方來進行協調。MySQL就是第三方。假如沒有XA事務,跨引擎事務提交須要順序地要求每一個引擎進行提交。這樣就會引入一種可能,就是在某個引擎提交以後發生了崩潰,可是另一個引擎還未提交。這就打破了事務的原則。

若是把記錄事件的二進制日誌當作一個"存儲引擎",那麼就能理解爲何即便是單個事務性引擎也須要XA事務。存儲引擎把事件提交給二進制日誌時,須要和服務器進行同步,由於是服務器,而不是存儲引擎處理二進制日誌。

當前的XA在性能上有些進退兩難。它打破了InnoDB從MySQL 5.0以來的對羣體提交(Group Commit)(一種使用單次I/O提交多個事務的技術)的支持,因此會致使了不少fsync()調用。若是二進制日誌處於激活狀態,那麼每一個事務都會須要等待日誌同步,而且每次事務提交都要求兩第二天志重寫,而不是一次。換句話說,若是想讓事務和二進制日誌安全地同步,就會要求至少三次fsync()調用。防止其發生的惟一辦法就是禁用二進制日誌並把innodb_support_xa設置爲0。

這樣設置沒法兼容複製。複製須要二進制日誌和XA支持,而且爲了儘量地安全,還需要把sync_binlog設置成1,這樣設置就能對存儲引擎和二進制日誌進行同步。(不然的話,XA支持就沒有必要了,由於二進制日誌不會被提交到磁盤上)。這是強烈推薦使用帶有備用電池的寫入緩存的磁盤陣列控制器的一個緣由,它能加快fsync()調用而且恢復性能。

下一章將會詳細講解如何配置事務日誌及二進制日誌。

 

 

5.11.2  外部XA事務

External XA Transactions

MySQL能夠參與,但不能管理外部分佈式事務。它不支持完整的XA規範。例如,XA規範容許鏈接運行單個事務中的鏈接,可是MySQL如今還不能作到這一點。

外部XA事務的開銷比內部XA事務更高,這是由於延遲會增長,而且參與者失敗的可能性更大。在WAN、甚至是因特網上使用XA,一個常見的問題就是網絡性能不可預測。當有不可預測的部分,好比較慢的網絡或一個有可能好久都不點擊"保存"按鈕的用戶,最好的選擇就是避免XA事務。任何耽擱提交的因素都會有很高的代價,由於它致使的不是單個系統延遲,而是許多系統。

能夠用另外的方式設計分佈式事務。例如,能夠在本地把數據插入隊列,而後把它自動地分佈成小而快的事務。也可使用MySQL複製把數據從一個地方搬運到另一個地方。咱們也發現某些使用了分佈式事務的應用程序其實根本不必使用事務。

總的說來,XA事務是一種在服務器之間同步數據的有用的方式。若是由於某些緣由,好比不能使用複製或數據更新的性能並非關鍵因素,它的效果會不錯。

 

 

綜上所述,分佈式XA事務還很差用,問題多,性能也很差,想一想使用其餘替代方案吧。

相關文章
相關標籤/搜索