今天這篇文章,我會繼續和你介紹在業務高峯期臨時提高性能的方法。從文章標題「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 所示。學習
圖 1 binlog 寫盤狀態測試
write 和 fsync 的時機,是由參數 sync_binlog 控制的:優化
1. sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;
2. sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;
3. sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才fsync。spa
所以,在出現 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 中的三個顏色塊。
圖 2 MySQL redo log 存儲狀態
這三種狀態分別是:
日誌寫到 redo log buffer 是很快的,wirte 到 page cache 也差很少,可是持久化到磁盤的速度就慢多了。
爲了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 參數,它有三種可能取值:
1. 設置爲 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;
2. 設置爲 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁盤;
3. 設置爲 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。
InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,而後調用 fsync 持久化到磁盤。
注意,事務執行中間過程的 redo log 也是直接寫在 redo log buffer 中的,這些 redolog 也會被後臺線程一塊兒持久化到磁盤。也就是說,一個沒有提交的事務的 redo log,也
是可能已經持久化到磁盤的。
實際上,除了後臺線程每秒一次的輪詢操做外,還有兩種場景會讓一個沒有提交的事務的redo log 寫入到磁盤中。
1. 一種是,redo log buffer 佔用的空間即將達到 innodb_log_buffer_size 一半的時候,後臺線程會主動寫盤。注意,因爲這個事務並無提交,因此這個寫盤動做只是
write,而沒有調用 fsync,也就是隻留在了文件系統的 page cache。
2. 另外一種是,並行的事務提交的時候,順帶將這個事務的 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 的「雙 1」配置,指的就是 sync_binlog 和innodb_flush_log_at_trx_commit 都設置成 1。也就是說,一個事務完整提交前,須要
等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。
解釋這個問題,就要用到組提交(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 logbuffer,持久化到磁盤的過程,對應的 LSN 分別是 50、120 和 160。
圖 3 redo log 組提交
從圖中能夠看到
1. trx1 是第一個到達的,會被選爲這組的 leader;
2. 等 trx1 要開始寫盤的時候,這個組裏面已經有了三個事務,這時候 LSN 也變成了160;
3. trx1 去寫盤的時候,帶的就是 LSN=160,所以等 trx1 返回時,全部 LSN 小於等於160 的 redo log,都已經被持久化到磁盤;
4. 這時候 trx2 和 trx3 就能夠直接返回了。
因此,一次組提交裏面,組員越多,節約磁盤 IOPS 的效果越好。但若是隻有單線程壓測,那就只能老老實實地一個事務對應一次持久化操做了。
在併發更新場景下,第一個事務寫完 redo log buffer 之後,接下來這個 fsync 越晚調用,組員可能越多,節約 IOPS 的效果就越好。
爲了讓一次 fsync 帶的組員更多,MySQL 有一個頗有趣的優化:拖時間。在介紹兩階段提交的時候,我曾經給你畫了一個圖,如今我把它截過來。
圖 4 兩階段提交
圖中,我把「寫 binlog」當成一個動做。但實際上,寫 binlog 是分紅兩步的:
1. 先把 binlog 從 binlog cache 中寫到磁盤上的 binlog 文件;
2. 調用 fsync 持久化。
MySQL 爲了讓組提交的效果更好,把 redo log 作 fsync 的時間拖到了步驟 1 以後。也就是說,上面的圖變成了這樣:
圖 5 兩階段提交細化
這麼一來,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 來實現。
1. binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;
2. 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 機制主要得益於兩個方面:
1. redo log 和 binlog 都是順序寫,磁盤的順序寫比隨機寫速度要快;
2. 組提交機制,能夠大幅度下降磁盤的 IOPS 消耗。
針對這個問題,能夠考慮如下三種方法:
1. 設置 binlog_group_commit_sync_delay 和binlog_group_commit_sync_no_delay_count 參數,減小 binlog 的寫盤次數。這個方法是基於「額外的故意等待」來實現的,
所以可能會增長語句的響應時間,但沒有丟失數據的風險。
2. 將 sync_binlog 設置爲大於 1 的值(比較常見是 100~1000)。這樣作的風險是,主機掉電時會丟 binlog 日誌。
3. 將 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 完成了,備庫去,這時候客戶端也會收到「網絡斷開」的異常。這種也只能算是事務成功的,不能認爲是 bug。
實際上數據庫的 crash-safe 保證的是:
1. 若是客戶端收到事務成功的消息,事務就必定持久化了;
2. 若是客戶端收到事務失敗(好比主鍵衝突、回滾等)的消息,事務就必定失敗了;
3. 若是客戶端收到「執行異常」的消息,應用須要重連後經過查詢當前狀態來繼續後續的邏輯。此時數據庫只須要保證內部(數據和日誌之間,主庫和備庫之間)一致就能夠了。
最後,又到了課後問題時間。
今天我留給你的思考題是:你的生產庫設置的是「雙 1」嗎? 若是平時是的話,你有在什麼場景下改爲過「非雙 1」嗎?你的這個操做又是基於什麼決定的?
另外,咱們都知道這些設置可能有損,若是發生了異常,你的止損方案是什麼?
你能夠把你的理解或者經驗寫在留言區,我會在下一篇文章的末尾選取有趣的評論和你一塊兒分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上篇文章最後,想要你分享的是線上「救火」的經驗。
@Long 同窗,在留言中提到了幾個很好的場景。還有其餘幾個同窗提到的問題場景,也很好,很值得你一看。其中第 3 個問題,
「若是一個數據庫是被客戶端的壓力打滿致使沒法響應的,重啓數據庫是沒用的。」,說明他很好地思考了。這個問題是由於重啓以後,業務請求還會再發。並且因爲是重啓,buffer pool 被清
空,可能會致使語句執行得更慢。
他提到的第 4 個問題也很典型。有時候一個表上會出現多個單字段索引(並且每每這是由於運維工程師對索引原理不夠清晰作的設計),這樣就可能出現優化器選擇索引合併
算法的現象。但實際上,索引合併算法的效率並很差。而經過將其中的一個索引改爲聯合索引的方法,是一個很好的應對方案。
@Max 同窗提到一個很好的例子:客戶端程序的鏈接器,鏈接完成後會作一些諸如 show columns 的操做,在短鏈接模式下這個影響就很是大了。
這個提醒咱們,在 review 項目的時候,不止要 review 咱們本身業務的代碼,也要 review 鏈接器的行爲。通常作法就是在測試環境,把general_log 打開,用業務行爲觸發鏈接,
而後經過 general log 分析鏈接器的行爲。
@Manjusaka 同窗的留言中,第二點提得很是好:若是你的數據庫請求模式直接對應於客戶請求,這每每是一個危險的設計。由於客戶行爲不可控,可能忽然由於大家公司的一個運營推廣,
壓力暴增,這樣很容易把數據庫打掛。在設計模型裏面設計一層,專門負責管理請求和數據庫服務資源,對於比較重要和大流量的業務,是一個好的設計方向。
@Vincent 同窗提了一個好問題,用文中提到的 DDL 方案,會致使 binlog裏面少了這個 DDL 語句,後續影響備份恢復的功能。因爲須要另外一個知識點(主備同步協議),我放在後面的文章中說明。