redo log 的寫入流程是怎麼樣的,如何保證 redo log 真實地寫入了磁盤。那麼今天,我們就再一起看看 mysql 寫入 binlog 和 redo log 的流程。
binlog 的寫入機制
其實,binlog 的寫入邏輯比較簡單:事務執行過程中,先把日志寫到 binlog cache,事務送出的時候,再把 binlog cache 寫到 binlog 檔案中。
一個事務的 binlog 是不能被拆開的,是以不論這個事務多大,也要確定一次性寫入。這就涉及到了 binlog cache 的儲存問題。
系統給 binlog cache 配置設定了一片記憶體,每個線程一個,參數 binlog_cache_size 用于控制單個線程内 binlog cache 所占記憶體的大小。如果超過了這個參數規定的大小,就要暫存到磁盤。
事務送出的時候,執行器把 binlog cache 裡的完整事務寫入到 binlog 中,并清空 binlog cache。狀态如圖 1 所示。
圖 1 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 中的某個數值。
但是,将 sync_binlog 設定為 n,對應的風險是:如果主機發生異常重新開機,會丢失最近 n 個事務的 binlog 日志。
redo log 的寫入機制
接下來,我們再說說 redo log 的寫入機制。
在專欄的第 15 篇答疑文章中,我給你介紹了 redo log buffer。事務在執行過程中,生成的 redo log 是要先寫到 redo log buffer 的。
然後就有同學問了,redo log buffer 裡面的内容,是不是每次生成後都要直接持久化到磁盤呢?
答案是,不需要。
如果事務執行期間 mysql 發生異常重新開機,那這部分日志就丢了。由于事務并沒有送出,是以這時日志丢了也不會有損失。
那麼,另外一個問題是,事務還沒送出的時候,redo log buffer 中的部分日志有沒有可能被持久化到磁盤呢?
答案是,确實會有。
這個問題,要從 redo log 可能存在的三種狀态說起。這三種狀态,對應的就是圖 2 中的三個顔色塊。
圖 2 mysql 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 有一個背景線程,每隔 1 秒,就會把 redo log buffer 中的日志,調用 write 寫到檔案系統的 page cache,然後調用 fsync 持久化到磁盤。
注意,事務執行中間過程的 redo log 也是直接寫在 redo log buffer 中的,這些 redo log 也會被背景線程一起持久化到磁盤。也就是說,一個沒有送出的事務的 redo log,也是可能已經持久化到磁盤的。
實際上,除了背景線程每秒一次的輪詢操作外,還有兩種場景會讓一個沒有送出的事務的 redo log 寫入到磁盤中。
一種是,redo log buffer 占用的空間即将達到 innodb_log_buffer_size 一半的時候,背景線程會主動寫盤。注意,由于這個事務并沒有送出,是以這個寫盤動作隻是 write,而沒有調用 fsync,也就是隻留在了檔案系統的 page cache。
另一種是,并行的事務送出的時候,順帶将這個事務的 redo log buffer 持久化到磁盤。假設一個事務 a 執行到一半,已經寫了一些 redo log 到 buffer 中,這時候有另外一個線程的事務 b 送出,如果 innodb_flush_log_at_trx_commit 設定的是 1,那麼按照這個參數的邏輯,事務 b 要把 redo log buffer 裡的日志全部持久化到磁盤。這時候,就會帶上事務 a 在 redo log buffer 裡的日志一起持久化到磁盤。
這裡需要說明的是,我們介紹兩階段送出的時候說過,時序上 redo log 先 prepare, 再寫 binlog,最後再把 redo log commit。
如果把 innodb_flush_log_at_trx_commit 設定成 1,那麼 redo log 在 prepare 階段就要持久化一次,因為有一個崩潰恢複邏輯是要依賴于 prepare 的 redo log,再加上 binlog 來恢複的。(如果你印象有點兒模糊了,可以再回顧下第 15 篇文章中的相關内容)。
每秒一次背景輪詢刷盤,再加上崩潰恢複這個邏輯,innodb 就認為 redo log 在 commit 的時候就不需要 fsync 了,隻會 write 到檔案系統的 page cache 中就夠了。
通常我們說 mysql 的“雙 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設定成 1。也就是說,一個事務完整送出前,需要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。
這時候,你可能有一個疑問,這意味着我從 mysql 看到的 tps 是每秒兩萬的話,每秒就會寫四萬次磁盤。但是,我用工具測試出來,磁盤能力也就兩萬左右,怎麼能實作兩萬的 tps?
解釋這個問題,就要用到組送出(group commit)機制了。
這裡,我需要先和你介紹日志邏輯序列号(log sequence number,lsn)的概念。lsn 是單調遞增的,用來對應 redo log 的一個個寫入點。每次寫入長度為 length 的 redo log, lsn 的值就會加上 length。
lsn 也會寫到 innodb 的資料頁中,來確定資料頁不會被多次執行重複的 redo log。關于 lsn 和 redo log、checkpoint 的關系,我會在後面的文章中詳細展開。
如圖 3 所示,是三個并發事務 (trx1, trx2, trx3) 在 prepare 階段,都寫完 redo log buffer,持久化到磁盤的過程,對應的 lsn 分别是 50、120 和 160。
圖 3 redo log 組送出
從圖中可以看到,
trx1 是第一個到達的,會被選為這組的 leader;
等 trx1 要開始寫盤的時候,這個組裡面已經有了三個事務,這時候 lsn 也變成了 160;
trx1 去寫盤的時候,帶的就是 lsn=160,是以等 trx1 傳回時,所有 lsn 小于等于 160 的 redo log,都已經被持久化到磁盤;
這時候 trx2 和 trx3 就可以直接傳回了。
是以,一次組送出裡面,組員越多,節約磁盤 iops 的效果越好。但如果隻有單線程壓測,那就隻能老老實實地一個事務對應一次持久化操作了。
在并發更新場景下,第一個事務寫完 redo log buffer 以後,接下來這個 fsync 越晚調用,組員可能越多,節約 iops 的效果就越好。
為了讓一次 fsync 帶的組員更多,mysql 有一個很有趣的優化:拖時間。在介紹兩階段送出的時候,我曾經給你畫了一個圖,現在我把它截過來。
圖 4 兩階段送出
圖中,我把“寫 binlog”當成一個動作。但實際上,寫 binlog 是分成兩步的:
先把 binlog 從 binlog cache 中寫到磁盤上的 binlog 檔案;
調用 fsync 持久化。
mysql 為了讓組送出的效果更好,把 redo log 做 fsync 的時間拖到了步驟 1 之後。也就是說,上面的圖變成了這樣:
這麼一來,binlog 也可以組送出了。在執行圖 5 中第 4 步把 binlog fsync 到磁盤時,如果有多個事務的 binlog 已經寫完了,也是一起持久化的,這樣也可以減少 iops 的消耗。
不過通常情況下第 3 步執行得會很快,是以 binlog 的 write 和 fsync 間的間隔時間短,導緻能集合到一起持久化的 binlog 比較少,是以 binlog 的組送出的效果通常不如 redo log 的效果那麼好。
如果你想提升 binlog 組送出的效果,可以通過設定 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 來實作。
binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;
binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以後才調用 fsync。
這兩個條件是或的關系,也就是說隻要有一個滿足條件就會調用 fsync。
是以,當 binlog_group_commit_sync_delay 設定為 0 的時候,binlog_group_commit_sync_no_delay_count 也無效了。
之前有同學在評論區問到,wal 機制是減少磁盤寫,可是每次送出事務都要寫 redo log 和 binlog,這磁盤讀寫次數也沒變少呀?
現在你就能了解了,wal 機制主要得益于兩個方面:
redo log 和 binlog 都是順序寫,磁盤的順序寫比随機寫速度要快;
組送出機制,可以大幅度降低磁盤的 iops 消耗。
分析到這裡,我們再來回答這個問題:如果你的 mysql 現在出現了性能瓶頸,而且瓶頸在 io 上,可以通過哪些方法來提升性能呢?
針對這個問題,可以考慮以下三種方法:
設定 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 參數,減少 binlog 的寫盤次數。這個方法是基于“額外的故意等待”來實作的,是以可能會增加語句的響應時間,但沒有丢失資料的風險。
将 sync_binlog 設定為大于 1 的值(比較常見是 100~1000)。這樣做的風險是,主機掉電時會丢 binlog 日志。
将 innodb_flush_log_at_trx_commit 設定為 2。這樣做的風險是,主機掉電的時候會丢資料。
我不建議你把 innodb_flush_log_at_trx_commit 設定成 0。因為把這個參數設定成 0,表示 redo log 隻儲存在記憶體中,這樣的話 mysql 本身異常重新開機也會丢資料,風險太大。而 redo log 寫到檔案系統的 page cache 的速度也是很快的,是以将這個參數設定成 2 跟設定成 0 其實性能差不多,但這樣做 mysql 異常重新開機時就不會丢資料了,相比之下風險會更小。
小結
在專欄的第 2 篇和第 15 篇文章中,我和你分析了,如果 redo log 和 binlog 是完整的,mysql 是如何保證 crash-safe 的。今天這篇文章,我着重和你介紹的是 mysql 是“怎麼保證 redo log 和 binlog 是完整的”。
希望這三篇文章串起來的内容,能夠讓你對 crash-safe 這個概念有更清晰的了解。
之前的第 15 篇答疑文章釋出之後,有同學繼續留言問到了一些跟日志相關的問題,這裡為了友善你回顧、學習,我再集中回答一次這些問題。
問題 1:執行一個 update 語句以後,我再去執行 hexdump 指令直接檢視 ibd 檔案内容,為什麼沒有看到資料有改變呢?
回答:這可能是因為 wal 機制的原因。update 語句執行完成後,innodb 隻保證寫完了 redo log、記憶體,可能還沒來得及将資料寫到磁盤。
問題 2:為什麼 binlog cache 是每個線程自己維護的,而 redo log buffer 是全局共用的?
回答:mysql 這麼設計的主要原因是,binlog 是不能“被打斷的”。一個事務的 binlog 必須連續寫,是以要整個事務完成後,再一起寫到檔案裡。
而 redo log 并沒有這個要求,中間有生成的日志可以寫到 redo log buffer 中。redo log buffer 中的内容還能“搭便車”,其他事務送出的時候可以被一起寫到磁盤中。
問題 3:事務執行期間,還沒到送出階段,如果發生 crash 的話,redo log 肯定丢了,這會不會導緻主備不一緻呢?
回答:不會。因為這時候 binlog 也還在 binlog cache 裡,沒發給備庫。crash 以後 redo log 和 binlog 都沒有了,從業務角度看這個事務也沒有送出,是以資料是一緻的。
問題 4:如果 binlog 寫完盤以後發生 crash,這時候還沒給用戶端答複就重新開機了。等用戶端再重連進來,發現事務已經送出成功了,這是不是 bug?
回答:不是。
你可以設想一下更極端的情況,整個事務都送出成功了,redo log commit 完成了,備庫也收到 binlog 并執行了。但是主庫和用戶端網絡斷開了,導緻事務成功的包傳回不回去,這時候用戶端也會收到“網絡斷開”的異常。這種也隻能算是事務成功的,不能認為是 bug。
實際上資料庫的 crash-safe 保證的是:
如果用戶端收到事務成功的消息,事務就一定持久化了;
如果用戶端收到事務失敗(比如主鍵沖突、復原等)的消息,事務就一定失敗了;
如果用戶端收到“執行異常”的消息,應用需要重連後通過查詢目前狀态來繼續後續的邏輯。此時資料庫隻需要保證内部(資料和日志之間,主庫和備庫之間)一緻就可以了。
你可以把你的了解或者經驗寫在留言區,我會在下一篇文章的末尾選取有趣的評論和你一起分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。