深入理解二階段提交協議(DDB對XA懸掛事務的處理分析)(一)
來自專欄網易雲社區7 人贊了文章
本文來自網易雲社區。
你知道二階段提交協議?嗯,你可能知道,那你知道各種宕機、通信異常情況下的故障恢復細節?
你知道DDB是分散式資料庫,那你知道DDB如何保證跨多個DBN的數據一致性?你知道什麼是懸掛事務?它何時出現?會有什麼影響?如何處理它們?你知道DDB可能出現數據不一致?那是在什麼場景?本文從原理出發……
從深入理解二階段提交協議開始,從事務處理模型到XA規範,從MySQL各版本對XA的支持情況到各種坑,從反編譯DDB窺視其內部,深入理解其事務協調者實現,再到暢想實現更高的數據一致性和容錯機制。 這篇萬字長文希望能給你帶來一些收穫。
本文背景
前段時間在了解分散式場景下的事務處理套路。二階段提交是一個非常經典且應用極為廣泛的分散式協議,那就先從它開始吧。 網上關於2PC的文章很多,但對在出現各種故障後,如何進行故障恢復講的較少。所以,我想著,要找個經典的中間件好好分析下。DDB從06年開始為網易核心產品提供分庫分表服務,十多年風雨,見證了各大產品興衰,自己始終屹立不倒,是杭研後台產品中資歷最老, 最為穩定的產品。DDB是怎麼處理跨DBN節點的事務呢?我很感興趣,可惜DDB是商業閉源的,核心文檔也保密,因此我只能反編譯DDB, 只為理解各種場景產生XA懸掛事務時,如何進行補救,如何保證數據一致性。 巧的是在理解反編譯代碼時找到了一個DDB的BUG,在我跟馬進、勁松確認後確實存在問題。 這個BUG會導致部分非同步提交的分散式分支事務狀態未知(實際應該是已提交,不影響業務數據)。
本文大致分為5個部分:
- 理解二階段提交協議及故障恢復
- 了解DTP模型&XA規範
- 了解MySQL對XA的支持
- 分析DDB對XA懸掛事務的處理
- 一點點想法,能否做的更好?
本文的討論範圍是二階段提交,不對其他分散式事務解決方案做討論,如有理解不到位的地方,歡迎大家指正。
理解二階段提交協議及故障恢復
在分散式系統中,每個節點無法知道其他節點的操作是成功或失敗。 當一個事務跨多個節點時,為了保持事務的ACID特性,需要引入一個協調者組件統一來管控所有參與者。 分散式系統的難點之一是如何在多個節點進行事務性操作時保持一致性。 二階段提交就是為保持事務提交一致性而設計的一種協議,二階段提交具有比較強的一致性。
二階段提交演算法的成立基於以下假設:
- 所有節點不會永久性損壞,即使損壞後仍然可以恢復
- 所有節點都採用WAL,日誌寫入後就被保存在可靠的存儲設備上
- 所有節點上的本地事務即使機器crash也可從WAL日誌上恢復
網上看到的二階段提交的圖基本如上圖,比較簡單,很多細節沒有講到,我畫了兩張細圖,結合著我們看下細節。
回滾流程
提交流程準備階段
- 協調者記錄事務開始日誌
- 協調者向所有參與者發送prepare消息,詢問是否可以執行事務提交,並等待參與者響應
- 參與者收到prepare消息後,根據自身情況,進行事務預處理,執行詢問發起為止的所有事務操作
- 如果能夠提交該事務,將undo信息和redo信息寫入日誌,進入預提交狀態
- 如果不能提交該事務,撤銷所做的變更,並記錄日誌
- 參與者響應協調者發起的詢問。如果事務預處理成功返回commit,否者返回abort。
準備階段只保留了最後一步耗時短暫的正式提交操作給第二階段執行。
提交階段
- 協調者等待所有參與者準備階段的反饋
- 如果收到某個參與者發來的abort消息或者遲遲未收到某個參與者發來的消息
- 標識該事務不能提交,協調者記錄abort日誌
- 向所有參與者發送abort消息,讓所有參與者撤銷準備階段的預處理
- 如果協調者收到所有參與者發來的commit消息
- 標識著該事務可以提交,協調者記錄commit日誌
- 向所有參與者發送commit消息,讓所有參與者提交事務
- 參與者等待協調者的指令
- 如果參與者收到的是abort消息
- 中止事務,利用之前寫入的undo日誌執行回滾,釋放準備階段鎖定的資源
- 記錄abort日誌
- 向協調者發送rollback done消息
- 如果參與者收到的是commit消息
- 提交事務,釋放準備階段鎖定的資源
- 記錄commit日誌
- 向協調者發送commit done消息
- 協調者等待所有參與者提交階段的反饋
- 如果協調者收到所有參與者發來的commit done消息
- 完成事務,記錄事務完成日誌
- 如果協調者收到所有參與者發來的rollback done消息
- 取消事務,記錄事務取消日誌
為什麼先寫日誌
為什麼在執行任務前需要先寫日誌?如果沒有日誌,宕機後重啟將無法知道事務狀態,無法進行故障恢復。宕機異常處理
正常情況下,兩階段提交機制都能較好的運行,當在事務進行過程中,參與者或協調者宕機了怎麼辦?- 階段一,協調者記錄全局事務開始日誌前宕機
協調者重啟後,所有參與者均未開始分支事務,不做任何處理
- 階段一,協調者記錄全局事務開始日誌後,發出prepare消息後宕機協調者重啟後,可能部分參與者已經完成一階段準備,所以需要進行回滾
- 階段一,協調者收到參與者響應後,記錄準備完成日誌前宕機協調者重啟後,因為沒有記錄準備完成日誌,所以需要進行回滾
- 階段一,協調者收到參與者響應後,記錄準備完成日誌後宕機協調者重啟後,已經記錄準備完成日誌,可以根據協商結果進行提交或回滾
- 階段二,協調者發出commit消息後宕機協調者重啟後,已經記錄準備完成日誌,可以根據協商結果進行提交或回滾
- 階段二,協調者收到參與者響應後,記錄事務完成日誌前宕機協調者重啟後,已經記錄準備完成日誌,可以根據協商結果進行提交或回滾
- 階段二,協調者收到參與者響應後,記錄事務完成日誌後宕機協調者重啟後,發現事務已經完成,不做任何處理
- 階段一,某個參與者宕機(未完成prepare)參與者重啟後,需要進行回滾(分散式事務的狀態是abort)
- 階段二,某個參與者宕機(已完成prepare)參與者重啟後,參與者如果沒有詢問其他參與者或協調者事務是否提交的能力,恢復後事務處於懸掛狀態,等待協調者指令(分散式事務的決策可能是提交,可能是回滾)
超時異常處理
- 階段一,協調者發出prepare消息,但沒有收到所有參與者的響應
協調者給所有參與者發送回滾指令
- 階段二,協調者發出commit消息,但沒有收到所有參與者的響應協調者可非同步不斷對超時節點嘗試提交
二階段提交的缺點
- 同步阻塞事務執行過程中,所有參與節點都是事務阻塞的。當參與者佔有資源時,其他訪問相關資源的進程也將處於阻塞狀態。 參與者對鎖資源的釋放必須等到事務結束,所以與一階段提交相比,執行同樣的事務,二階段會耗費更多時間。 事務執行時間的延長意味著鎖資源發生衝突的概率增加,當事務並發量達到一定數量時,會出現大量事務積壓甚至出現死鎖,系統性能會嚴重下滑。
- 單點故障一旦協調者發生故障,參與者會一直阻塞。參與者完成準備階段後,協調者發生故障,所有的參與者都將處於鎖定事務資源的狀態中(事務懸掛狀態),無法繼續完成事務操作。
- 數據不一致在提交階段中,當協調者向參與者發送commit消息後,發生了局部網路異常或者在發送commit消息過程中協調者發生了故障, 這會導致只有一部分參與者接受到了commit消息。而這些參與者收到commit消息後就會執行commit操作。 但是其他未收到commit消息的參與者無法執行commit。於是整個分散式系統出現了數據不一致性。
對於問題1,同步阻塞這是二階段提交協議自身決定的,我們無能為力(有其他的方式優化,可能會引起數據不一致)。
對於問題2、3呢?我們是否也無可奈何?來,我們慢慢分析。了解DTP模型&XA規範
名詞介紹
DTP(Distributed Transaction Process):一種實現分散式事務處理系統的概念模型AP(Application Program):應用程序實現所需的業務功能,定義事務邊界,使用資源管理器訪問資源,通常決定是否提交或回滾全局事務
TM(Transaction Manager):事務管理器負責分配事務標識碼,負責事務完成和失敗恢復,協調AP和RM實現分散式事務的完整性RM(Resource Manager):資源管理器提供訪問共享資源,DTP要求RM必須支持事務,並能夠將全局事務標識定位到自己的內部事務XA(eXtended Architecture):XA是DTP的一部分介面規範;是RM和TM的介面定義和交互規範,實現了DTP環境中的二階段提交全局事務:對於一次性操作多個資源管理器的事務,就是全局事務分支事務:每個RM的本地事務是這個全局事務的一個事務分支懸掛事務:完成準備階段的分支事務DTP模型
- AP通過TM控制事務邊界
- AP聲明需要哪些RM,TM註冊RM
- AP使用RM完成分支事務
- AP通過TM提交事務
- TM通知RM提交事務
XA介面規範定義
ax_reg 向事務管理器註冊資源管理器ax_unreg 向事務管理器取消註冊資源管理器xa_close 終止應用程序對資源管理器的使用xa_commit 通知資源管理器提交事務分支xa_complete 詢問指定的非同步xa_操作是否完成xa_end 解除線程與事務分支的關聯xa_forget 允許資源管理器丟棄自行完成的事務分支信息xa_open 初始化資源管理器,供應用程序使用xa_prepare 通知資源管理器準備提交事務分支xa_recover 獲取資源管理器已準備或自行完成的事務標識符XID列表xa_rollback 通知資源管理器回滾事務分支xa_start 啟動或恢復事務分支,將XID與資源管理器請求線程的未來工作關聯
ax_開頭的,是TM提供給RM調用,用於支持RM加入/退出集群時的動態註冊機制
xa_開頭的,是RM提供給TM調用,用於實現二階段提交中的各種事務提交、恢復
了解MySQL對XA的支持
MySQL從5.0.3開始支持分散式事務,僅InnoDB存儲引擎支持MySQL XA事務。 MySQL XA是基於XA規範實現的,支持分散式事務,允許多個資料庫實例參與一個全局事務。
MySQL XA命令操作
- xa start xid: 開啟一個XA事務
- xa end xid: 將事務設為idle狀態,表示事務內的SQL操作完成
- xa prepare xid: 實現事務提交的準備工作,事務設為prepared狀態。如果無法完成提交前的準備操作,該語句會執行失敗
- xa commit xid: 提交事務
- xa rollback xid: 回滾事務
- xa recover: 查看prepared狀態的XA事務
MySQL XA狀態轉換
- 執行xa start來啟動一個XA事務,事務處於active狀態
- active狀態的XA事務,可以執行SQL語句,這些語句都將處於該事務中
- active狀態的XA事務,可以執行xa end命令。執行後事務進入idle狀態
- idle狀態的XA事務,可以執行xa prepare或xa commit…one phase命令
- 執行xa prepare命令,事務進入prepared狀態。
- 執行xa commit…one phase命令,直接提交事務。因為事務已終止,xid將不會被xa recover列出。
- prepared狀態的XA事務,可以執行commit或者rollback命令
- 執行xa commit命令提交事務
- 執行xa rollback命令回滾事務
MySQL內部XA原理
MySQL對binlog做了優化,prepare不寫binlog日誌,commit才寫日誌。 在開啟binlog後,binlog會被當做事務協調者,binlog event會被當做協調者日誌,MySQL內部會自動將普通事務當做一個XA事務來處理。 由binlog通知InnoDB引擎來執行prepare,commit或者rollback。事務提交的整個過程如下:- 準備階段
- 通知InnoDB prepare:更改事務狀態,將undo、redo log落盤
- 提交階段
- 記錄協調者日誌(binlog日誌),並通過fsync()永久落盤
- 通知InnoDB commit
內部XA異常恢復
- 準備階段redo log落盤前宕機InnoDB中還沒prepare,binlog中也沒有該事務的events。通知InnoDB回滾事務
- 準備階段redo log落盤後宕機(binlog落盤前)InnoDB中是prepared狀態,binlog中沒有該事務的events。通知InnoDB回滾事務
- 提交階段binlog落盤後宕機InnoDB中是prepared狀態,binlog中有該事務的events。通知InnoDB提交事務
MySQL外部XA問題
看起來很完美,然而在MySQL5.7.7以前,MySQL對外部XA的支持是有限的,主要存在兩個問題:- 問題一:準備階段的事務被回滾已經prepare的事務在連接斷開後事務會被回滾(不符合2PC協議規範)
- 問題二:數據不一致MySQL主備庫的同步是通過binlog複製完成的。prepare狀態的事務在節點宕機後重啟,引擎層通過recover機制可以恢復該事務:
- 如果選擇commit,在主庫中會提交事務,但未寫binlog(宕機後保存binlog的cache丟失,server已經不知道事務的情況了),也就不能複製到備庫,從而導致主備庫數據不一致
- 如果選擇rollback,可以保證此節點主備庫一致,然而如果該分散式事務對應的節點,部分已經提交(無法回滾),而部分節點回滾。最終導致同一分散式事務,在各參與節點最終狀態不一致。
上述問題在MySQL中存在了很久,直到MySQL5.7.7版本才修復。解決方案是區分XA事務和本地事務,本地事務還是在commit時記錄binlog,對於XA事務在prepare階段也記錄binlog。 同時為了避免prepare事務阻塞session,SQL Thread線程在回放prepare階段時會把相關cache與SQL Thread的連接句柄脫離(類似於客戶端斷開連接的處理)。
針對這兩個問題,大家可以用docker跑兩個不同版本的mysql容器測試下,這樣理解會深刻一點。我這邊演示下MySQL5.7.18的主從同步。
MySQL複製
(本圖是通過binlog進行數據複製回放的大體流程,不同的複製方式執行細節有些不一樣)主要原理
- master執行sql之後記錄二進位binlog
- slave連接master,從master獲取binlog存到本地relaylog,並從上次記住的位置開始執行sql,一旦遇到錯誤則停止同步
MySQL複製分為非同步複製、半同步複製、無損半同步複製、MGR(MySQL Group Replication)、同步複製(MySQL Cluster)。 非同步複製當master出現故障後,binlog未及時傳到slave,如果master切換,會導致主從數據不一致。 下面我們驗證下無損半同步。
1. 啟動主從兩個容器
version: 3services: mysql-master: hostname: mysql-master image: mysql:5.7 ports: - "3305:3307" volumes: - ~/dockermapping/mysql5.7-master-slave/mysql-master/config:/etc/mysql/conf.d - ~/dockermapping/mysql5.7-master-slave/mysql-master/mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: root mysql-slave: hostname: mysql-slave image: mysql:5.7 ports: - "3304:3307" volumes: - ~/dockermapping/mysql5.7-master-slave/mysql-slave/config:/etc/mysql/conf.d - ~/dockermapping/mysql5.7-master-slave/mysql-slave/mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: root
2. MySQL主從配置
#master mysql.cnf[mysqld]port=3307log-bin=mysql-binserver-id=1plugin-load=rpl_semi_sync_master=semisync_master.sorpl_semi_sync_master_enabled=1sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES#slave mysql.cnf[mysqld]port=3307log-bin=mysql-binserver-id=2plugin-load=rpl_semi_sync_slave=semisync_slave.sorpl_semi_sync_slave_enabled=1sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
3. 賬戶同步配置
#mastergrant replication slave on *.* to rep1@mysql-slave identified by 123456;flush privileges;#slavechange master to master_host=mysql-master,master_port=3307,master_user=rep1,master_password=123456,master_log_file=mysql-bin.000004,master_log_pos=4328;
4. 啟動XA完成一階段提交
mysql> xa start 1;Query OK, 0 rows affected (0.01 sec)mysql> insert into t values(1);Query OK, 1 rows affected (0.01 sec)mysql> xa end 1;Query OK, 0 rows affected (0.01 sec)mysql> xa prepare 1;Query OK, 0 rows affected (0.01 sec)
5. 觀察master的binlog
mysql> show binlog events in mysql-bin.000004 from 7710;+------------------+------+----------------+-----------+-------------+--------------------------------------+| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |+------------------+------+----------------+-----------+-------------+--------------------------------------+| mysql-bin.000004 | 7710 | Anonymous_Gtid | 1 | 7775 | SET @@SESSION.GTID_NEXT= ANONYMOUS || mysql-bin.000004 | 7775 | Query | 1 | 7862 | XA START X31,X,1 || mysql-bin.000004 | 7862 | Table_map | 1 | 7908 | table_id: 219 (test.t) || mysql-bin.000004 | 7908 | Write_rows | 1 | 7946 | table_id: 219 flags: STMT_END_F || mysql-bin.000004 | 7946 | Query | 1 | 8031 | XA END X31,X,1 || mysql-bin.000004 | 8031 | XA_prepare | 1 | 8068 | XA PREPARE X31,X,1 |+------------------+------+----------------+-----------+-------------+--------------------------------------+我們發現prepare狀態的XA分支事務已經寫到binlog
6. 再觀察slave的relaylog
mysql> show relaylog events in mysql-slave-relay-bin.000009 limit 20,20;+------------------------------+------+----------------+-----------+-------------+--------------------------------------+| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |+------------------------------+------+----------------+-----------+-------------+--------------------------------------+| mysql-slave-relay-bin.000009 | 1136 | Anonymous_Gtid | 1 | 988 | SET @@SESSION.GTID_NEXT= ANONYMOUS || mysql-slave-relay-bin.000009 | 1201 | Query | 1 | 1075 | XA START X31,X,1 || mysql-slave-relay-bin.000009 | 1288 | Table_map | 1 | 1121 | table_id: 219 (test.t) || mysql-slave-relay-bin.000009 | 1334 | Write_rows | 1 | 1159 | table_id: 219 flags: STMT_END_F || mysql-slave-relay-bin.000009 | 1372 | Query | 1 | 1244 | XA END X31,X,1 || mysql-slave-relay-bin.000009 | 1457 | XA_prepare | 1 | 1281 | XA PREPARE X31,X,1 |+------------------------------+------+----------------+-----------+-------------+--------------------------------------+6 rows in set (0.04 sec)我們發現master庫prepare狀態的binlog被同步到slave庫上relaybinlog
再做一個異常實驗
在主庫prepare的過程中,從庫掛了是否會導致數據不一致?1. master上操作mysql> xa start 6;Query OK, 0 rows affected (0.01 sec)mysql> insert into t values(6);Query OK, 1 rows affected (0.01 sec)
2. slave上操作
mysql> stop slave;Query OK, 0 rows affected (0.01 sec)mysql> xa recover;Empty set (0.01 sec)
3. master上操作
mysql> xa end 6;Query OK, 0 rows affected (0.01 sec)mysql> xa prepare 6;Query OK, 0 rows affected (**10.00 sec**)此時主庫在等待10s後將半同步轉為非同步
4. slave上操作
mysql> start slave;Query OK, 0 rows affected (0.01 sec)mysql> xa recover;+----------+--------------+--------------+------+| formatID | gtrid_length | bqual_length | data |+----------+--------------+--------------+------+| 1 | 1 | 0 | 6 |+----------+--------------+--------------+------+1 rows in set (0.03 sec)從庫跟上主庫的binlog,最終一致
通過修改切換複製的timeout時間(rpl_semi_sync_master_timeout默認10s),無損半同步的似乎可以做到強一致性;
通過設置主庫等多個slave收到日誌後才完成(rpl_semi_sync_master_wait_for_slave_count默認1)又能保證高可用。是否足夠完美?MySQL非同步複製滿足了可用性,主從互不影響,滿足分區容錯,但不滿足強一致性(最終一致)。
MySQL半同步複製滿足較強一致性,但因為要等slave確認,所以性能上差一點。MySQL Group Replication通過Paxos協議保證集群節點數據強一致性,似乎做到了真正的一致性,但也是以犧牲一定的性能為代價。本文來自網易雲社區,經作者陳志良授權發布。
原文地址:深入理解二階段提交協議(DDB對XA懸掛事務的處理分析)(一)-社區博客-網易雲
更多網易研發、產品、運營經驗分享請訪問網易雲社區。
推薦閱讀:
※基於IPFS的分散式數據共享系統的研究
※從構建分散式秒殺系統聊聊限流特技
※Flink源碼解析之State的實現
※分散式事務入門指南 · 常用分散式事務解決方案
※阿里最年輕合伙人胡喜:骨子裡沒點技術理想主義干不來自主研發