今天這篇文章,我會繼續和你介紹在業務高峯期臨時提高性能的方法。從文章標題「MySQL是怎麼保證數據不丟的?」,你就能夠看出來,今天我和你介紹的方法,跟數據的可靠性有關。算法
在專欄前面文章和答疑篇中,我都着重介紹了WAL機制(你能夠再回顧下第2篇、第9篇、第12篇和第15篇文章中的相關內容),獲得的結論是:只要redo log和binlog保證持久化到磁盤,就能確保MySQL異常重啓後,數據能夠恢復。數據庫
評論區有同窗又繼續追問,redo log的寫入流程是怎麼樣的,如何保證redo log真實地寫入了磁盤。那麼今天,咱們就再一塊兒看看MySQL寫入binlog和redo log的流程。網絡
其實,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 和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的寫入機制。
在專欄的第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語句,後續影響備份恢復的功能。因爲須要另外一個知識點(主備同步協議),我放在後面的文章中說明。