主要靠
redo log
和
binlog
保證持久化到磁碟,就能確保 MySQL 異常重啟後,資料可以恢復。
binlog 的寫入機制
事務執行過程中,先把日誌寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 檔案中。如圖,
可以看到,每個執行緒有自己 binlog cache,但是共用同一份 binlog 檔案。
圖中的 write,指的就是指把日誌寫入到檔案系統的 page cache,並沒有把資料持久化到磁碟,所以速度比較快。
圖中的 fsync,才是將資料持久化到磁碟的操作。一般情況下,我們認為 fsync 才佔磁碟的 IOPS。
write 和 fsync 的時機,是由引數 sync_binlog 控制的:
sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;
sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;
sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才 fsync。
因此,在出現 IO 瓶頸的場景裡,將 sync_binlog 設定成一個比較大的值,可以提升效能。在實際的業務場景中,考慮到丟失日誌量的可控性,一般不建議將這個引數設成 0,比較常見的是將其設定為 100~1000 中的某個數值。
redo log 的寫入機制
和binlog相似,不過redo log buffer是執行緒共享的(
為何binlog buffer不能共享?
MySQL 這麼設計的主要原因是,binlog 是不能“被打斷的”。一個事務的 binlog 必須連續寫,因此要整個事務完成後,再一起寫到檔案裡。而 redo log 並沒有這個要求,中間有生成的日誌可以寫到 redo log buffer 中。redo log buffer 中的內容還能“搭便車”,其他事務提交的時候可以被一起寫到磁碟中。)。
redo log中的資料可能存在的三種狀態:
存在 redo log buffer 中,物理上是在 MySQL 程序記憶體中,就是圖中的紅色部分;
寫到磁碟 (write),但是沒有持久化(fsync),物理上是在檔案系統的 page cache 裡面,也就是圖中的黃色部分;
持久化到磁碟,對應的是 hard disk,也就是圖中的綠色部分。
日誌寫到 redo log buffer 是很快的,wirte 到 page cache 也差不多,但是持久化到磁碟的速度就慢多了。
為了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 引數,它有三種可能取值:
設定為 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;
設定為 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁碟;
設定為 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。
如果
innodb_flush_log_at_trx_commit 這個引數設定0
或2的時候,InnoDB也會有一個後臺執行緒,每隔 1 秒,就會把 redo log buffer 中的日誌,呼叫 write 寫到檔案系統的 page cache,然後呼叫 fsync 持久化到磁碟。(
無論這個引數設定的是0、1還是2,最終都會通過後臺執行緒刷進page cache,再由作業系統寫進磁碟。
)
innodb_flush_log_at_trx_commit引數詳解
兩階段提交的時候說過,時序上 redo log 先 prepare(
所謂的 redo log prepare,是“當前事務提交”的一個階段,也就是說,在事務A提交的時候,我們才會走到事務A的redo log prepare這個階段。事務A在提交前,有一部分redo log被事務B提前持久化,但是事務A還沒有進入提交階段,是無所謂“redo log prepare”的。
), 再寫 binlog,最後再把 redo log 執行commit。
如果把 innodb_flush_log_at_trx_commit 設定成 1,那麼 redo log 在 prepare 階段就要持久化一次,因為有一個崩潰恢復邏輯是要依賴於 prepare 的 redo log,再加上 binlog 來恢復的。
每秒一次後臺輪詢刷盤,再加上崩潰恢復這個邏輯,InnoDB 就認為 redo log 在 commit 的時候就不需要 fsync 了,只會 write 到檔案系統的 page cache 中就夠了
(
這裡說明了,redo log 狀態改為commit的時候不會進行fsync,因為只要binlog 寫磁碟成功,就算redo log 的狀態還是prepare也沒有關係會被認為事務已經執行成功,所以只需要write 到page cache就ok了,沒必要再浪費io主動去進行一次fsync。這個write動作交給後臺執行緒去執行。
)。
通常我們說 MySQL 的“雙 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設定成 1。也就是說,一個事務完整提交前,需要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。
組提交(group commit)機制
你可能有一個疑問,這意味著我從 MySQL 看到的 TPS 是每秒兩萬的話,每秒就會寫四萬次磁碟。但是用工具測試出來,磁碟能力也就兩萬左右,怎麼能實現兩萬的 TPS?
這裡,我需要先和你介紹日誌邏輯序列號(log sequence number,LSN)的概念。LSN 是單調遞增的,
用來對應 redo log
的一個個
寫入點
。每次寫入長度為 length 的 redo log, LSN 的值就會加上 length。如圖,
是三個併發事務 (trx1, trx2, trx3) 在 prepare 階段,都寫完 redo log buffer,持久化到磁碟的過程,對應的 LSN 分別是 50、120 和 160。
從圖中可以看到,
trx1 是第一個到達的,會被選為這組的 leader;
等 trx1 要開始寫盤的時候,這個組裡面已經有了三個事務,這時候 LSN 也變成了 160;
trx1 去寫盤的時候,帶的就是 LSN=160,因此等 trx1 返回時,所有 LSN 小於等於 160 的 redo log,都已經被持久化到磁碟。
這時候 trx2 和 trx3 就可以直接返回了。
所以,一次組提交裡面,組員越多,節約磁碟 IOPS 的效果越好。但如果只有單執行緒壓測,那就只能老老實實地一個事務對應一次持久化操作了。在併發更新場景下,第一個事務寫完 redo log buffer 以後,接下來這個 fsync 越晚呼叫,組員可能越多,節約 IOPS 的效果就越好。
為了讓一次 fsync 帶的組員更多,MySQL 有一個很有趣的最佳化:
拖時間。
如在兩階段提交的時候,如圖,
圖中,實際上,寫 binlog 是分成兩步的。
先把 binlog 從 binlog cache 中寫到磁碟上的 binlog 檔案。
呼叫 fsync 持久化。
MySQL 為了讓組提交的效果更好,把 redo log 做 fsync 的時間拖到了步驟 1 之後。也就是說,上面的圖變成了這樣:
這麼一來,binlog 也可以組提交了。在執行圖中第 4 步把 binlog fsync 到磁碟時,如果有多個事務的 binlog 已經寫完了,也是一起持久化的,這樣也可以減少 IOPS 的消耗。
事務執行期間,還沒到提交階段,如果發生 crash 的話,redo log 肯定丟了,這會不會導致主備不一致呢?
不會。因為這時候 binlog 也還在 binlog cache 裡,沒發給備庫。crash 以後 redo log 和 binlog 都沒有了,從業務角度看這個事務也沒有提交,所以資料是一致的。