MySQL binlog和redo的組提交

賀子_DBA時代發表於2018-04-30
整體概述:組提交(group commit)是MYSQL處理日誌的一種最佳化方式,主要為了解決寫日誌時頻繁刷磁碟的問題。組提交伴隨著MYSQL的發展不斷最佳化,從最初只支援redo log 組提交,到目前5.6官方版本同時支援redo log 和binlog組提交。組提交的實現大大提高了mysql的事務處理效能
mysql5.6官方版本之前:
5.6之前只支援redo組提交,當開啟了binlog,為了保證redo和binlog的一致性,使用了兩階段提交(先重新整理redo,再重新整理binlog,並且以binlog重新整理成功與否來判斷事務是否成功),為了保證binlog和commit的順序一致,binlog使用了序列化的重新整理,導致binlog無法組提交, 5.6最佳化了這個步驟,引入了佇列,保證了binlog有順序,進而binlog也可以組提交;
接下來重點說說一:redo 組提交,二:兩階段提交。三:5.6 binlog組提交;
一:redo 組提交
WAL(Write-Ahead-Logging)是實現事務永續性的一個常用技術,基本原理是在提交事務時,為了避免磁碟頁面的隨機寫,只需要保證事務的redo log寫入磁碟即可,這樣可以透過redo log的順序寫代替頁面的隨機寫,並且可以保證事務的永續性,提高了資料庫系統的效能。雖然WAL使用順序寫替代了隨機寫,但是,每次事務提交,仍然需要有一次日誌刷盤動作,受限於磁碟IO,這個操作仍然是事務併發的瓶頸。
redo組提交思想是,將多個事務redo log的刷盤動作合併,減少磁碟順序寫。Innodb的日誌系統裡面,每條redo log都有一個LSN(Log Sequence Number),LSN是單調遞增的。每個事務執行更新操作都會包含一條或多條redo log,各個事務將日誌複製到log_sys_buffer時(log_sys_buffer 透過log_mutex保護),都會獲取當前最大的LSN,因此可以保證不同事務的LSN不會重複。那麼假設三個事務Trx1,Trx2和Trx3的日誌的最大LSN分別為LSN1,LSN2,LSN3(LSN1<lsn2<lsn3),它們同時進行提交,那麼如果trx3日誌先獲取到log_mutex進行落盤,它就可以順便把[lsn1---lsn3]這段日誌也刷了,這樣trx1和trx2就不用再次請求磁碟io。組提交的基本流程如下:</lsn2<lsn3),它們同時進行提交,那麼如果trx3日誌先獲取到log_mutex進行落盤,它就可以順便把[lsn1---lsn3]這段日誌也刷了,這樣trx1和trx2就不用再次請求磁碟io。組
1)獲取 log_mutex
2)若flushed_to_disk_lsn>=lsn,表示日誌已經被刷盤,跳轉5
3)若 current_flush_lsn>=lsn,表示日誌正在刷盤中,跳轉5後進入等待狀態
4)將小於LSN的日誌刷盤(flush and sync)
5)退出log_mutex
備註:lsn表示事務的lsn,flushed_to_disk_lsn和current_flush_lsn分別表示已刷盤的LSN和正在刷盤的LSN。
redo log 組提交最佳化
我們知道,在開啟binlog的情況下,prepare階段,會對redo log進行一次刷盤操作(innodb_flush_log_at_trx_commit=1),確保對data頁和undo 頁的更新已經重新整理到磁碟;commit階段,會進行刷binlog操作(sync_binlog=1),並且會對事務的undo log從prepare狀態設定為提交狀態(可清理狀態)。透過兩階段提交方式(innodb_support_xa=1),可以保證事務的binlog和redo log順序一致。二階段提交過程中,mysql_binlog作為協調者,各個儲存引擎和mysql_binlog作為參與者。故障恢復時,掃描最後一個binlog檔案(在flush階段,判斷binlog是否超過閥值,進行rotate binlog檔案,rotate的binlog檔案中對應的事務一定是已經提交的,處於prepared的事務的binlog還沒有刷進來,因為還沒進入ordered_commit函式),提取其中的xid;重做檢查點以後的redo日誌,讀取事務的undo段資訊,蒐集處於prepare階段的事務連結串列,將事務的xid與binlog中的xid對比,若存在,則提交,否則就回滾。
透過上述的描述可知,每個事務提交時,都會觸發一次redo flush動作,由於磁碟讀寫比較慢,因此很影響系統的吞吐量。淘寶童鞋做了一個最佳化,將prepare階段的刷redo動作移到了commit(flush-sync-commit)的flush階段之前,保證刷binlog之前,一定會刷redo。這樣就不會違背原有的故障恢復邏輯。移到commit階段的好處是,可以不用每個事務都刷盤,而是leader執行緒幫助刷一批redo。如何實現,很簡單,因為log_sys->lsn始終保持了當前最大的lsn,只要我們刷redo刷到當前的log_sys->lsn,就一定能保證,將要刷binlog的事務redo日誌一定已經落盤。透過延遲寫redo方式,實現了redo log組提交的目的,而且減少了log_sys->mutex的競爭。目前這種策略已經被官方mysql5.7.6引入。
二:兩階段提交
在單機情況下,redo log組提交很好地解決了日誌落盤問題,那麼開啟binlog後,binlog能否和redo log一樣也開啟組提交?首先開啟binlog後,我們要解決的一個問題是,如何保證binlog和redo log的一致性。因為binlog是Master-Slave的橋樑,如果順序不一致,意味著Master-Slave可能不一致。MYSQL透過兩階段提交很好地解決了這一問題。
Prepare階段:innodb刷redo log,並將回滾段設定為Prepared狀態,binlog不作任何操作;
commit階段:innodb釋放鎖,釋放回滾段,設定提交狀態,binlog刷binlog日誌。
出現異常,需要故障恢復時,若發現事務處於Prepare階段,並且binlog存在則提交,否則回滾。透過兩階段提交,保證了redo log和binlog在任何情況下的一致性。
伴隨著這個問題,我重點說下,MySQL innodb 引擎事務commit的過程:
MySQL為了保證master和slave的資料一致性,就必須保證binlog和InnoDB redo日誌的一致性(因為備庫透過二進位制日誌重放主庫提交的事務,而主庫binlog寫入在commit之前,如果寫完binlog主庫crash,再次啟動時會回滾事務。但此時從庫已經執行,則會造成主備資料不一致)。所以在開啟Binlog後,如何保證binlog和InnoDB redo日誌的一致性呢?為此,MySQL引入二階段提交(two phase commit or 2pc),MySQL內部會自動將普通事務當做一個XA事務(內部分散式事物)來處理:
MySQL透過兩階段提交(內部XA的兩階段提交)很好地解決了這一問題,兩階段提交關鍵在於保證redo刷盤之後才能寫binloglog 檔案;MySQL5.6以前,為了保證資料庫上層二進位制日誌的寫入順序和InnoDB層的事務提交順序一致,MySQL資料庫內部使用了prepare_commit_mutex鎖。但是持有這把鎖之後,會導致組提交失敗;
      回到上節的問題,開啟binlog後,如何在保證redo log-binlog一致的基礎上,實現組提交。因為這個問題,5.6以前,mysql在開啟binlog的情況下,無法實現組提交,透過一個臭名昭著的prepare_commit_mutex,將binlog刷盤序列化,序列化的目的也僅僅是為了保證redo log-Binlog一致,但這種實現方式犧牲了效能。這個情況顯然是不能容忍的,因此各個mysql分支,mariadb,facebook,perconal等相繼出了補丁改進這一問題,mysql官方版本5.6也終於解決了這一問題。由於各個分支版本解決方法類似,我主要透過分析5.6的實現來說明實現方法。
     binlog組提交的基本思想是,引入佇列機制保證innodb commit順序與binlog落盤順序一致,並將事務分組,組內的binlog刷盤動作交給一個事務進行,實現組提交目的。binlog提交將提交分為了3個階段,FLUSH階段,SYNC階段和COMMIT階段。每個階段都有一個佇列,每個佇列有一個mutex保護,約定進入佇列第一個執行緒為leader,其他執行緒為follower,所有事情交由leader去做,leader做完所有動作後,通知follower刷盤結束。
MySQL5.6之前的如下兩個階段:
第一階段(準備階段):InnoDB prepare,持有prepare_commit_mutex,並且write/sync redo log; 將回滾段設定為Prepared狀態,binlog不作任何操作;
第二個階段(提交階段):Commit Phase 包含兩步
1.write/sync Binlog(這裡為了保證和redo的順序一樣只能序列化的重新整理);
2.InnoDB commit (寫入COMMIT標記後釋放prepare_commit_mutex,釋放回滾段,設定提交狀態);
以 binlog 的寫入與否作為事務提交成功與否的標誌,innodb commit標誌並不是事務提交成功與否的標誌。因為此時的事務崩潰恢復過程如下:
1.崩潰恢復時,掃描最後一個Binlog檔案,提取其中的xid;
2.InnoDB維持了狀態為Prepare的事務連結串列,將這些事務的xid和Binlog中記錄的xid做比較,如果在Binlog中存在,則提交,否則回滾事務。
透過這種方式,可以讓InnoDB redo 和Binlog中的事務狀態保持一致。
在寫入innodb commit標誌時崩潰,則恢復時,會藉助redo log 重新對commit標誌進行寫入;
在prepare階段崩潰,則binlog中肯定沒有這些xid,則藉助undo 回滾;
在write/sync binlog階段崩潰,也會藉助undo回滾。
MySQL5.6之前在開啟Binary log時使用prepare_commit_mutex和sync_log保證二進位制日誌和儲存引擎順序保持一致,prepare_commit_mutex的鎖機制造成高併發提交事務的時候效能非常差而且二進位制日誌也無法group commit;
MySQL5.6以及之後版本中的實現方式,重點解決了binlog組提交的問題:
上面的事務的兩階段提交過程是5.6之前版本中的實現,有嚴重的缺陷。當sync_binlog=1時,很明顯上述的第二階段中的 write/sync binlog會成為瓶頸,而且還是持有全域性大鎖(prepare_commit_mutex: prepare 和 commit共用一把鎖),這會導致效能急劇下降。
解決辦法就是MySQL5.6中的 binlog組提交。
binlog組提交的基本思想是,引入佇列機制保證innodb commit順序與binlog落盤順序一致,並將事務分組,組內的binlog刷盤動作交給一個事務進行,實現組提交目的。
binlog提交將提交階段分為了3個階段,FLUSH階段,SYNC階段和COMMIT階段。每個階段都有一個佇列,佇列中的第一個事務稱為leader,其他事務稱為follower,leader控制著follower的行為。
MySQL5.6之後的MySQL innodb 引擎事務commit的過程:
第一階段(準備階段):InnoDB prepare,持有prepare_commit_mutex,並且write/sync redo log; 將回滾段設定為Prepared狀態,完成後就釋放prepare_commit_mutex,binlog不作任何操作;
第二個階段(提交階段):InnoDB 提交階段發生變化了,將Commit階段拆分成了三步,每個階段的任務分配給一個專門的執行緒:
FLUSH 階段
1) 持有Lock_log mutex [leader持有,follower等待]
2) 獲取佇列中的一組binlog(佇列中的所有事務)
3) 將binlog buffer到I/O cache
4) 通知dump執行緒dump binlog
SYNC階段
這個階段和引數sync_binlog有關係,
1) 釋放Lock_log mutex,持有Lock_sync mutex[leader持有,follower等待]
2) 將一組binlog 落盤(sync動作,最耗時,假設sync_binlog為1)。
COMMIT階段(這裡不用寫redo log,在prepare階段已寫)
1) 釋放Lock_sync mutex,持有Lock_commit mutex[leader持有,follower等待]
2) 遍歷佇列中的事務,逐一進行innodb commit
3) 釋放Lock_commit mutex
4) 喚醒佇列中等待的執行緒
總結起來就是 :
FLUSH 階段-----將binlog從binlog buffer到I/O cache重新整理;
SYNC階段-------將binlog從I/O cache到底層磁碟;
COMMIT階段----innodb commit ,清除undo資訊;
每個stage都有自己的佇列。每個佇列各自有mutex保護,佇列之間是順序的。只有flush完成後,才能進入到sync階段的佇列中;sync完成後,才能進入到commit階段的佇列中。但是,這三個階段的作業是可以同時併發執行的,即當一組事務在進行commit階段時,其他新事務可以進行flush階段,實現了真正意義上的group commit。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29654823/viewspace-2153565/,如需轉載,請註明出處,否則將追究法律責任。

相關文章