1. 程式人生 > >MySQL crash-safe replication(3): MySQL的Crash Safe和Binlog的關係

MySQL crash-safe replication(3): MySQL的Crash Safe和Binlog的關係

2016-12-23 17:29 宋利兵

作者:宋利兵

來源:MySQL程式碼研究(mysqlcode)

0、導讀

本文重點介紹了InnoDB的crash safe和binlog之間的關係,以及2階段提交、組提交等概念。看完後,相信您對MySQL Crash Recovery的過程,以及如何保證Crash Safe會有充分的認識。

本文約2200字,閱讀時間約15分鐘。

0 - 什麼是CrashSafe
CrashSafe指MySQL伺服器宕機重啟後,能夠保證:
- 所有已經提交的事務的資料仍然存在。
- 所有沒有提交的事務的資料自動回滾。


前面的文章講過,Innodb通過Redo Log和Undo Log可以保證以上兩點。為了保證嚴格的CrashSafe,必須要在每個事務提交的時候,將Redo Log寫入硬體儲存。這樣做會犧牲一些效能,但是可靠性最好。為了平衡兩者,InnoDB提供了一個系統變數,使用者可以根據應用的需求自行調整。

 

- innodb_flush_log_at_trx_commit
  0 - 每N秒將Redo Log Buffer的記錄寫入Redo Log檔案,並且將檔案刷入硬體儲存1次。N由innodb_flush_log_at_timeout控制。
  1 - 每個事務提交時,將記錄從Redo Log Buffer寫入Redo Log檔案,並且將檔案刷入硬體儲存。
  2 - 每個事務提交時,僅將記錄從Redo Log Buffer寫入Redo Log檔案。Redo Log何時刷入硬體儲存由作業系統和innodb_flush_log_at_timeout決定。這個選項可以保證在MySQL宕機,而作業系統正常工作時,資料的完整性。

那麼CrashSafe和Binlog有什麼關係呢?

1 - 帶Binlog的CrashSafe
當啟動Binlog後,事務會產生Binlog Event,這些Event被看做事務資料的一部分。因此要保證事務的Binlog Event和InnoDB引擎中的資料的一致性。所以帶Binlog的CrashSafe要求MySQL宕機重啟後能夠保證:
- 所有已經提交的事務的資料仍然存在。


- 所有沒有提交的事務的資料自動回滾。
- 所有已經提交了的事務的Binlog Event也仍然存在。
- 所有沒有提交事務沒有記錄Binlog Event。
  這些要求很好理解,如果重啟後資料還在,但是Binlog Event沒有了,就沒辦法複製到其他節點上了。如果重啟後,資料沒了,但是Binlog Event還在,那麼不存在的資料就會被複制到其他節點上,從而導致主從的不一致。

為了保證帶Binlog的CrashSafe,MySQL內部使用的兩階段提交(Two Phase Commit)。

 

2 - MySQL的Two Phase Commit(2PC)
在開啟Binlog後,MySQL內部會自動將普通事務當做一個XA事務來處理:

- 自動為每個事務分配一個唯一的ID

- COMMIT會被自動的分成Prepare和Commit兩個階段。

- Binlog會被當做事務協調者(Transaction Coordinator),Binlog Event會被當做協調者日誌。

想了解2PC,可以參考文件:https://en.wikipedia.org/wiki/Two-phase_commit_protocol。

 

- 分散式事務ID(XID)
使用2PC時,MySQL會自動的為每一個事務分配一個ID,叫XID。XID是唯一的,每個事務的XID都不相同。XID會分別被Binlog和InnoDB記入日誌中,供恢復時使用。MySQ內部的XID由三部分組成:
- 字首部分
  字首部分是字串"MySQLXid"
- Server ID部分
  當前MySQL的server_id
- query_id部分
  為了保證XID的的唯一性,數字部分使用了query_id。MySQL內部會自動的為每一個語句分配一個query_id,全域性唯一。
參考程式碼:sql/xa。h的 struct xid_t結構。

- 事務的協調者Binlog
Binlog在2PC中充當了事務的協調者(Transaction Coordinator)。由Binlog來通知InnoDB引擎來執行prepare,commit或者rollback的步驟。事務提交的整個過程如下:
1. 協調者準備階段(Prepare Phase)
    告訴引擎做Prepare,InnoDB更改事務狀態,並將Redo Log刷入磁碟。
2. 協調者提交階段(Commit Phase)
    2.1 記錄協調者日誌,即Binlog日誌。
    2.2  告訴引擎做commit。

注意:記錄Binlog是在InnoDB引擎Prepare(即Redo Log寫入磁碟)之後,這點至關重要。

 

在MySQ的程式碼中將協調者叫做tc_log。在MySQL啟動時,tc_log將被初始化為mysql_bin_log物件。參考sql/mysqld.cc中的init_server_components():
if (opt_bin_log)
   tc_log= &mysql_bin_log;
而在事務提交時,會依次執行:
tc_log->prepare();
tc_log->commit();
參考程式碼:sql/handler.cc中的ha_commit_trans()。

當mysql_bin_log是tc_log時,prepare和commit的程式碼在sql/binlog.cc中:
MYSQL_BIN_LOG::prepare()
MYSQL_BIN_LOG::commit()

- 協調者日誌Xid_log_event
作為協調者,Binlog需要將事務的XID記入日誌,供恢復時使用。Xid_log_event有以下幾個特點:
- 僅記錄query_id
  因為字首部分不變,server_id已經記錄在Event Header中,Xid_log_event中只記錄query_id部分。
- 標誌事務的結束  
  在Binlog中相當於一個事務的COMMIT語句。
  一個事務在Binlog中看起來時這樣的: 

Query_log_event("BEGIN");
DML產生的events;              
Xid_log_event;                    

- DDL沒有BEGIN,也沒有Xid_log_event。

- 僅InnoDB的DML會產生Xid_log_event
  因為MyISAM不支援2PC所以不能用Xid_log_event,但會有COMMIT Event。

Query_log_event("BEGIN");
DML產生的events;
Query_log_event("COMMIT");


問題:Query_log_event("COMMIT")和Xid_log_event有不同的影響嗎?
- Xid_log_event中的Xid可以幫助master實現CrashSafe。
- Slave的CrashSafe不依賴Xid_log_event

事務在Slave上重做時,會重新產生XID。所以Slave伺服器的CrashSafe並不依賴於Xid_log_event。Xid_log_event和Query_log_event("COMMIT"),只是作為事務的結尾,告訴Slave Applier去提交這個事務。因此二者在Slave上的影響是一樣的。

3 - 恢復(Recovery)
這個機制是如何保證MySQL的CrashSafe的呢,我們來分析一下。這裡我們假設使用者設定了以下引數來保證可靠性:
sync_binlog=1
innodb_flush_log_at_trx_commit=1

 

- 恢復前事務的狀態
在恢復開始前事務有以下幾種狀態:
- InnoDB中已經提交
  根據前面2PC的過程,可知Binlog中也一定記錄了該事務的的Events。所以這種事務是一致的不需要處理。
- InnoDB中是prepared狀態,Binlog中有該事務的Events。
  需要通知InnoDB提交這些事務。
- InnoDB中是prepared狀態,Binlog中沒有該事務的Events。
  因為Binlog還沒記錄,需要通知InnoDB回滾這些事務。
- Before InnoDB Prepare
  事務可能還沒執行完,因此InnoDB中的狀態還沒有prepare。根據2PC的過程,Binlog中也沒有該事務的events。 需要通知InnoDB回滾這些事務。

 

- 恢復過程
從上面的事務狀態可以看出:恢復時事務要提交還是回滾,是由Binlog來決定的。
- 事務的Xid_log_event存在,就要提交。
- 事務的Xid_log_event不存在,就要回滾。

恢復的過程非常簡單:
- 從Binlog中讀出所有的Xid_log_event
- 告訴InnoDB提交這些XID的事務
- InnoDB回滾其它的事務

疑問1:如果事務的Binlog Event只記錄了一部分怎麼辦?
只有最後一個事務的Event會發生這樣的情況。在恢復時,binlog會自動的將這個不完整的事務Events從Binlog檔案中給清除掉。

 

疑問2:隨著長時間的執行,Binlog中會積累了很多Xid_log_event,讀取所有的Xid_log_event會不會效率很低?

當然很低,所以Binlog中有一個機制來保證恢復時只用讀取最後一個Binlog檔案中的Xid_log_event。這種機制很像一個簡單的Xid_log_event的checkpoint機制。

 

- Xid_log_event Checkpoint

這個機制和binlog的檔案切換有關,在切換到一個新的Binlog檔案前:
- 要等待當前Binlog檔案中的所有事務都已經在InnoDB中提交了。
- 告訴InnoDB刷Redo Log到硬體儲存。
通過這個機制可以保證在做恢復時,除了最後一個Binlog檔案中的事務,其他檔案中的事務在InnoDB中一定是已經提交的狀態。

 

參考程式碼:sql/binlog.cc中:

MYSQL_BIN_LOG::recovery()

MYSQL_BIN_LOG::new_file_impl()

MYSQL_BIN_LOG::inc_prep_xids()

MYSQL_BIN_LOG::dec_prep_xids()

 

4 - CrashSafe的寫盤次數
前面說道要想保證CrashSafe就要設定下面兩個引數為1:
sync_binlog=1
innodb_flush_log_at_trx_commit=1
下面我們來看看這兩個引數的作用。
- sync_binlog
sync_binlog是控制Binlog寫盤的,1表示每次都寫。由於Binlog使用了組提交(Group Commit)的機制,它代表一組事務提交時必須要將Binlog檔案寫入硬體儲存1次。
- innodb_flush_log_at_trx_commit的寫盤次數
這個變數是用來控制InnoDB commit時寫盤的方法的。現在commit被分成了兩個階段,到底在哪個階段寫盤,還是兩個階段都要寫盤呢?
- Prepare階段時需要寫盤
 2PC要求在Prepare時就要將資料持久化,只有這樣,恢復時才能提交已經記錄了Xid_log_event的事務。
- Commit階段時不需要寫盤
 如果Commit階段不寫盤,會造成什麼結果呢?已經Cmmit了的事務,在恢復時的狀態可能是Prepared。由於恢復時,Prepared的事務可以通過Xid_log_event來提交事務,所以在恢復後事務的狀態就是正確的。因此在Commit階段不需要寫盤。

 

總的來說保證MySQL服務的CrashSafe需要寫兩次盤。在2PC的過程中,InnoDB只在prepare階段時,寫一次盤。Binlog在commit階段,會設定一個引數告訴InnoDB不要寫盤。這個引數是thd->durability_property= HA_IGNORE_DURABILITY;程式碼在sql/binlog.cc的MYSQL_BIN_LOG::ordered_commit()中。

 

- Prepare階段寫盤優化

我們知道Binlog使用了Group Commit機制來減少IO,提高效能。Prepare有沒有可能做Group Commit呢?只要我們能保證任何事務的Redo Log是在它的Binlog Event寫入Binlog檔案前,被刷入了持久儲存就可以。優化後的做法是:
1. 協調者準備階段(Prepare Phase)
   設定thd->durability_property告訴InnoDB不寫盤。 告訴引擎做Prepare,InnoDB更改事務狀態。
2. 協調者提交階段(Commit Phase)
   2.1.1 獲取一組事務。
   2.1.2 通知InnoDB將Redo Log寫入硬體儲存。
   2.1.3 將這組事務的Binlog Event寫入Binlog檔案。
   2.2 告訴引擎做commit。

這個結合了Binlog Group Commit機制的改進對效能的提升還是很顯著的。而且這個改進是中國的社群使用者阿里雲的翟衛祥同學提出並提供的程式碼補丁。詳情可參考MySQL的bug頁面:http://bugs.mysql.com/bug.php?id=73202。

參考程式碼:sql/binlog.cc中的MYSQL_BIN_LOG::process_flush_stage_queue()

 

5 - 總結

MySQL通過兩階段提交的方式來保證CrashSafe。CrashSafe需要Server層、Binlog和InnoDB的協同工作才能完成。由於DDL和MyISAM不支援事務性,因此沒辦法保證CrashSafe。

 

注意:本文的程式碼都是指MySQL-5.7中的程式碼,其他版本可能會有不一致。

 

 

延伸閱讀,宋利兵老師其他文章

MySQL資料庫InnoDB儲存引擎Log漫遊(1)

MySQL資料庫InnoDB儲存引擎Log漫遊(2)

MySQL資料庫InnoDB儲存引擎Log漫遊(3)

由淺入深理解InnoDB的索引實現(1)

由淺入深理解InnoDB的索引實現(2)