天天看點

MySQL并發複制系列一:binlog組送出

mysql  binary log在mysql 5.1版本後推出主要用于主備複制的搭建,我們回顧下mysql 在開啟/關閉 binary log功能時是如何工作的 。

mysql沒有開啟binary log的情況下:

innodb存儲引擎通過redo和undo日志可以safe crash recovery資料庫,當資料crash recovery時,通過redo日志将所有已經在存儲引擎内部送出的事務應用redo log恢複,所有已經prepared但是沒有commit的transactions将會應用undo log做roll back。然後用戶端連接配接時就能看到已經送出的資料存在資料庫内,未送出被復原地資料需要重新執行。

mysql開啟binary log 的情況下:

為了保證存儲引擎和mysql資料庫上層的二進制日志保持一緻(因為備庫通過二進制日志重放主庫送出的事務,假設主庫存儲引擎已經送出而二進制日志沒有保持一緻,則會使備庫資料丢失造成主備資料不一緻),引入二階段送出(two phase commit or 2pc)

MySQL并發複制系列一:binlog組送出

圖1 二階段送出

mysql二階段送出流程:

storage engine(innodb) transaction prepare階段:即sql語句已經成功執行并生成redo和undo的記憶體日志

binary log日志提送出

write()将binary log記憶體日志資料寫入檔案系統緩存

fsync()将binary log 檔案系統緩存日志資料永久寫入磁盤

storage engine(innodb)内部送出

commit階段在存儲引擎内送出( innodb_flush_log_at_trx_commit控制)使undo和redo永久寫入磁盤

開啟binary log的mysql在crash recovery時:

當事務在prepare階段crash,資料庫recovery的時候該事務未寫入binary log并且存儲引擎未送出,将該事務roll back。

當事務在binary log日志已經fsync()永久寫入二進制日志時crash,但是存儲引擎未來得及commit,此時mysql資料庫recovery的時候将會從二進制日志的xid(mysql資料庫内部分布式事務xa)中擷取送出的資訊重新将該事務重做并commit使存儲引擎和二進制日志始終保持一緻。

以上提到單個事務的二階段送出過程,能夠保證存儲引擎和binary log日志保持一緻,但是在并發的情況下怎麼保證存儲引擎和binary log送出的順序一緻?當多個事務并發送出的情況,如果binary log和存儲引擎順序不一緻會造成什麼影響?

MySQL并發複制系列一:binlog組送出

圖2 innodb存儲引擎送出的順序與mysql上層的二進制日志順序不同

如上圖:事務按照t1、t2、t3順序開始執行,将二進制日志(按照t1、t2、t3順序)寫入日志檔案系統緩存,調用fsync()進行一次group commit将日志檔案永久寫入磁盤,但是存儲引擎送出的順序為t2、t3、t1。當t2、t3送出事務之後做了一個on-line的backup程式建立一個slave來做replication,那麼事務t1在slave機器restore mysql資料庫的時候發現未在存儲引擎内送出,t1事務被roll back,此時主備資料不一緻(搭建slave時,change master to的日志偏移量記錄t3在事務位置之後)。

結論:mysql資料庫上層二進制日志的寫入順序和存儲引擎innodb層的事務送出順序一緻,用于備份及恢複需要,如xtrabackup和innobackpex工具。

為了解決以上問題,在早期的mysql版本,通過prepare_commit_mutex 鎖保證mysq資料庫上層二進制日志和innodb存儲引擎層的事務送出順序一緻。

MySQL并發複制系列一:binlog組送出

圖3 通過prepare_commit_mutex保證存儲引擎和二進制日志順序送出順序一緻

圖3可以看出在prepare_commit_mutex,隻有當上一個事務commit後釋放鎖,下一個事務才可以進行prepara操作,并且在每個transaction過程中binary log沒有fsync()的調用。由于記憶體資料寫入磁盤的開銷很大,如果頻繁fsync()把日志資料永久寫入磁盤資料庫的性能将會急劇下降。此時mysql 資料庫提供sync_binlog參數來設定多少個binlog日志産生的時候調用一次fsync()把二進制日志刷入磁盤來提高整體性能,該參數的設定作用:

sync_binlog=0,二進制日志fsync()的操作基于作業系統。

sync_binlog=1,每一個transaction commit都會調用一次fsync(),此時能保證資料最安全但是性能影響較大。

sync_binlog=n,當資料庫crash的時候至少會丢失n-1個transactions。

圖3 所示mysql開啟binary log時使用prepare_commit_mutex和sync_log保證二進制日志和存儲引擎順序保持一緻(通過sync_binlog來控制日志的重新整理頻率),prepare_commit_mutex的鎖機制造成高并發送出事務的時候性能非常差而且二進制日志也無法group commit。

那麼如何保證mysql開啟binary log日志後使二進制日志寫入順序和存儲引擎送出順序保持一緻并且能夠進行二進制日志的group commit?

mysql 5.6 引入blgc(binary log group commit),二進制日志的送出過程分成三個階段,flush stage、sync stage、commit stage。

那麼事務送出過程簡化為:

存儲引擎(innodb) prepare    ---->    資料庫上層(binary log)   flush stage    ---->    sync stage    ---->    調存儲引擎(innodb)commit stage.

每個stage階段都有各自的隊列,使每個session的事務進行排隊。當一個線程注冊了一個空隊列,該線程就視為該隊列的leader,後注冊到該隊列的線程為follower,leader控制隊列中follower的行為。leader同時帶領目前隊列的所有follower到下一個stage去執行,當遇到下一個stage并非空隊列,此時leader可以變成follower到此隊列中(注:follower的線程不可能變成leader)

MySQL并發複制系列一:binlog組送出

圖4: 二進制日志三階段送出過程

在 flush stage:所有已經注冊線程都将寫入binary log緩存

在sync stage :binary log緩存的資料将會sync到磁盤,當sync_binlog=1時所有該隊列事務的二進制日志緩存永久寫入磁盤

在 commit stage:leader根據順序調用存儲引擎送出事務。

當一組事務在進行commit階段時,其他新的事務可以進行flush階段,進而使group commit不斷生效。那麼為了提高group commit中一組隊列的事務數量,mysql用binlog_max_flush_queue_time來控制在flush stage中的等待時間,讓flush隊列在此階段多等待一些時間來增加這一組事務隊列的數量使該隊列到sync階段可以一次fysn()更多的事務。

mysql 5.7 parallel replication實作主備多線程複制基于主庫binary log group commit, 并在binary log日志中辨別同一組事務的last_commited=n和該組事務内所有的事務送出順序。為了增加一組事務内的事務數量提高備庫組送出時的并發量引入了binlog_group_commit_sync_delay=n 和binlog_group_commit_sync_no_delay_count=n (注:binlog_max_flush_queue_time 在mysql的5.7.9及之後版本不再生效)參數,mysql等待binlog_group_commit_sync_delay毫秒直到達到binlog_group_commit_sync_no_delay_count事務個數時,将進行一次組送出。

<b>本文來自雲栖社群合作夥伴“dbgeek”</b>