今天這篇文章,我會繼續和你介紹在業務高峰期臨時提升性能的方法。從文章標題“MySQL是怎么保證數據不丟的?”,你就可以看出來,今天我和你介紹的方法,跟數據的可靠性有關。
在專欄前面文章和答疑篇中,我都着重介紹了WAL機制(你可以再回顧下第2篇、第9篇、第12篇和第15篇文章中的相關內容),得到的結論是:只要redo log和binlog保證持久化到磁盤,就能確保MySQL異常重啟后,數據可以恢復。
評論區有同學又繼續追問,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所示。
可以看到,每個線程有自己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 中的三個顏色塊。
這三種狀態分別是:
-
存在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。
從圖中可以看到,
-
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從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保證的是:
-
如果客戶端收到事務成功的消息,事務就一定持久化了;
-
如果客戶端收到事務失敗(比如主鍵沖突、回滾等)的消息,事務就一定失敗了;
-
如果客戶端收到“執行異常”的消息,應用需要重連后通過查詢當前狀態來繼續后續的邏輯。此時數據庫只需要保證內部(數據和日志之間,主庫和備庫之間)一致就可以了。
最后,又到了課后問題時間。
今天我留給你的思考題是:你的生產庫設置的是“雙1”嗎? 如果平時是的話,你有在什么場景下改成過“非雙1”嗎?你的這個操作又是基於什么決定的?
另外,我們都知道這些設置可能有損,如果發生了異常,你的止損方案是什么?
你可以把你的理解或者經驗寫在留言區,我會在下一篇文章的末尾選取有趣的評論和你一起分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。
上期問題時間
我在上篇文章最后,想要你分享的是線上“救火”的經驗。
@Long 同學,在留言中提到了幾個很好的場景。
-
其中第3個問題,“如果一個數據庫是被客戶端的壓力打滿導致無法響應的,重啟數據庫是沒用的。”,說明他很好地思考了。
這個問題是因為重啟之后,業務請求還會再發。而且由於是重啟,buffer pool被清空,可能會導致語句執行得更慢。 -
他提到的第4個問題也很典型。有時候一個表上會出現多個單字段索引(而且往往這是因為運維工程師對索引原理不夠清晰做的設計),這樣就可能出現優化器選擇索引合並算法的現象。但實際上,索引合並算法的效率並不好。而通過將其中的一個索引改成聯合索引的方法,是一個很好的應對方案。
還有其他幾個同學提到的問題場景,也很好,很值得你一看。
@Max 同學提到一個很好的例子:客戶端程序的連接器,連接完成后會做一些諸如show columns的操作,在短連接模式下這個影響就非常大了。
這個提醒我們,在review項目的時候,不止要review我們自己業務的代碼,也要review連接器的行為。一般做法就是在測試環境,把general_log打開,用業務行為觸發連接,然后通過general log分析連接器的行為。
@Manjusaka 同學的留言中,第二點提得非常好:如果你的數據庫請求模式直接對應於客戶請求,這往往是一個危險的設計。因為客戶行為不可控,可能突然因為你們公司的一個運營推廣,壓力暴增,這樣很容易把數據庫打掛。
在設計模型里面設計一層,專門負責管理請求和數據庫服務資源,對於比較重要和大流量的業務,是一個好的設計方向。
@Vincent 同學提了一個好問題,用文中提到的DDL方案,會導致binlog里面少了這個DDL語句,后續影響備份恢復的功能。由於需要另一個知識點(主備同步協議),我放在后面的文章中說明。