MySQL8.0: 從新設計的日誌子系統

背景

當前幾乎全部的關係數據庫都採用日誌先行的方式,也就是所謂WRITE-AFTER-LOG(WAL),這是由於日誌一般是順序寫的,而且寫入量相比修改的數據一般要小不少。經過redo log來確保提交的事務必然具備持久性。(目前也有另一種理論叫作Write Ahead Log, 由CMU的教授提出,主要適用於Nvme,這裏在CMU的peloton項目裏有個介紹)mysql

然而日誌因爲要保證順序性,須要鎖來保護全部日誌拷貝到buffer都是有序的,引入了一個嚴重的鎖競爭點,特別是在多核場景下,這裏的競爭會很是明顯,沒法發揮出多核心的優點。算法

爲了解決這個問題,MySQL8.0對日誌系統進行了從新設計,將整個模塊變成了lock-free的模式(小道消息,目前官方也在對事務模塊和鎖模塊改形成lock-free模式,相信到時候InnoDB的擴展性必然會提高一大截, 將來可期!)sql

具體的,咱們能夠對應到幾個模塊:數據庫

- 拷貝到buffer: 每一個mini transaction將本身的本地日誌拷貝到全局Buffer中
- 寫磁盤:包括寫磁盤和調用fsync進行持久化
- 事務提交:當事務undo被標記爲prepare(若是binlog打開) 或者commit時,須要確保日誌被刷到磁盤,以確保事務的持久性
- Checkpoint: 按期對日誌作checkpoint,減小崩潰恢復時日誌的應用量

如下是對上述幾個模塊的簡要介紹數組

實現

寫log buffer
在5.7版本中,Innodb的log buffer其實是分紅了兩個區域,輪換着來寫,從而實如今寫一個buffer 時,另一個buffer依然能夠繼續往裏面拷貝日誌。但到了8.0版本,全部日誌相關的mutex都已經移除了,劃分緩衝區域也就沒有必要了,而是將log buffer當作一個環來使用。安全

首先,持有一個s lock, 並經過原子操做獲取當前mtr的start_lsn,和sn號(lsn減去log block頭和尾的大小,表示有效日誌量),這樣至關於在順序增長的lsn序列中保留了本身的一段範圍(得到mtr_t::start_lsn 和mtr_t::end_lsn), 經過start_lsn取模log buffer size,獲得其在log buffer中的位置,而後逐個block進行拷貝(log_buffer_write), 每寫一個mtr log block,就將其start_lsn和end_lsn加入到log.recent_written中,維持了一個link結構, 一個mtr 可能會更新屢次link_buf併發

(InnoDB裏增長了一個叫link_buf的類,其具體的做用就是將不連續的變量維護成一個鏈表,舉個簡單的例子:異步

buf[lsn_1] = lsn_2
buf[lsn_2] = lsn_3
Buf[lsn_3] = 0
Buf[lsn_4] = lsn_5
Lsn_1 = 10
Lsn_2 = 100
Lsn_3 = 200
Lsn_4 = 300
Lsn_5 = 400
經過這種方式,實際能夠追蹤到全部併發寫入到buffer的mtr範圍,並快速檢測到buffer中的hole,例如上例中,lsn_3 ~ lsn_4屬於尚未寫入日誌的空洞
如上提到的log.recent_written, 能夠確保寫到磁盤的日誌不存在空洞,如上例,只能寫到lsn_3這個位置)
在拷貝完日誌後,就須要將髒塊加入flush list中。注意因爲如今實現了徹底併發,咱們沒法作到按照LSN順序插入到flush list上,而有序性是用於保證checkpoint點的正確性。所以在這裏一樣也引入了另一個link_buf,名爲log.recent_closed,來輔助獲取一個安全的checkpoint點。所以在加入flush list後,該mtr也會加入到recent_closed中(相似buf[mtr->start_lsn] - mtr->end_lsn)

注意log.recent_writtern 和log.recent_closed都是有空間限制的,若是超出其capability,就須要等待,但這種狀況通常不多見函數

能夠看到這裏的代碼和5.7及以前版本已經徹底不一樣了:oop

  • 日誌能夠併發拷貝,但會存在hole
  • Flush list再也不有序

咱們以前慣用log_get_lsn或者直接log_sys->lsn來得到最新的lsn點,而在8.0版本,經過將log.sn轉換成最新的lsn,但這個lsn點並不表明該點以前的日誌都拷貝到buffer。以前咱們提到在拷貝buffer以前須要加一個s_lock, 若是咱們在持有x鎖的前提下去取lsn,才能保證是最新的。

寫磁盤

目前有兩個後臺線程來作日誌持久化,一個是log_writer線程,一個是log_flusher線程,顧名思義,前者負責寫日誌到磁盤,後者負責fsync日誌

Log_writer會根據log.recent_written中的記錄找到安全的lsn, 將對應日誌寫磁盤,同時回收log.recent_written中的空間。 若是當前srv_flush_log_at_trx_commit設置爲1的話,還回去喚醒log_flusher線程

log_flusher線程的主要工做是fsync日誌文件,同時推動log.flushed_to_disk_lsn。隨後嘗試去喚醒等待的用戶線程(若是隻涉及一個event slot)或者喚醒log_flush_notifier線程。

Log_notifier線程專門用於喚醒等待日誌寫入的線程,根據上次flush的log lsn和當前flush lsn,來計算對應的event slot,並遍歷數組喚醒等待的線程。

能夠看到這裏已經徹底作到了異步化,再加上併發拷貝log buffer, 能夠極大的發揮硬件性能。

事務提交

在innodb事務提交時,對應的Undo狀態被修改後,須要調用log_write_up_to去確保日誌已經寫盤了。在5.7及以前版本中,該函數就是用於寫日誌到磁盤。而到了8.0版本,該函數只有喚醒後臺線程及等待的邏輯。

一個有趣的問題是,因爲目前用戶線程僅須要等待喚醒,而無需去操做臨界區域,咱們能夠在其退出innodb後再調用log_write_up_to 進行等待(參考bug#90641)

Checkpoint

因爲如今髒頁並非按照LSN順序寫入的,所以選擇一個安全的checkpoint點相當重要,這個工做主要由後臺線程log_checkpointer來完成。

計算最老lsn的工做在log_get_available_for_checkpoint_lsn中完成:

- 首先找到log.recent_closed中的最小lsn,這個lsn點以前的page確定已經加入到flush list上了
- 其次取出當前flush list中最後一個非臨時表page的lsn,並取多個Buffer Pool中的最小值返回,而後減去一個安全的閾值(即log.recent_closed的最大空間)
- 上面兩個值去最小的那個

很顯然,爲了不掃描所有flush list鏈表,這裏採用了樂觀的算法,只要最大限度的保證作checkpoint的點是安全的便可。 這裏引入的一個問題是,作checkpoint時多是在一個mtr log的中間,在崩潰恢復時,可能須要對其定位的log block作特殊處理(在以前的版本中,能夠確保checkpoint lsn是一個mtr log的安全邊界)

隱藏參數

如上所述,這裏引入了多個後臺線程來增長系統的併發度,而在內部也有大量參數來對系統進行調整,以得到最優性能,但爲了不引發用戶困惑,有一些參數是被隱藏的(在定義時經過PLUGIN_VAR_EXPERIMENTAL來控制)。

若是你想使用這些參數,須要本身去編譯mysql代碼,並在cmake時增長參數-DENABLE_EXPERIMENT_SYSVARS=1

以下,打開選項後和日誌相關的參數包括:

+--------------------------------------+---------------+
| Variable_name                        | Value         |
+--------------------------------------+---------------+
| innodb_log_buffer_size               | 16777216      |
| innodb_log_checkpoint_every          | 1000          |
| innodb_log_checksums                 | ON            |
| innodb_log_closer_spin_delay         | 0             |
| innodb_log_closer_timeout            | 1000          |
| innodb_log_file_size                 | 2147483648    |
| innodb_log_files_in_group            | 8             |
| innodb_log_flush_events              | 2048          |
| innodb_log_flush_notifier_spin_delay | 0             |
| innodb_log_flush_notifier_timeout    | 10            |
| innodb_log_flusher_spin_delay        | 25000         |
| innodb_log_flusher_timeout           | 10            |
| innodb_log_group_home_dir            | /u01/my80/log |
| innodb_log_recent_closed_size        | 2097152       |
| innodb_log_recent_written_size       | 1048576       |
| innodb_log_spin_cpu_abs_lwm          | 80            |
| innodb_log_spin_cpu_pct_hwm          | 50            |
| innodb_log_wait_for_flush_spin_delay | 25000         |
| innodb_log_wait_for_flush_spin_hwm   | 0             |
| innodb_log_wait_for_flush_timeout    | 1000          |
| innodb_log_wait_for_write_spin_delay | 25000         |
| innodb_log_wait_for_write_timeout    | 1000          |
| innodb_log_write_ahead_size          | 8192          |
| innodb_log_write_events              | 2048          |
| innodb_log_write_max_size            | 4096          |
| innodb_log_write_notifier_spin_delay | 0             |
| innodb_log_write_notifier_timeout    | 10            |
| innodb_log_writer_spin_delay         | 25000         |
| innodb_log_writer_timeout            | 10            |
+--------------------------------------+---------------+
30 rows in set (0.01 sec)

經過這些參數,你能夠對新的日誌系統進行各類微調來得到最優性能。注意這裏不少參數目前還看不到官方文檔的描述,你可能須要結合代碼來看。有一些比較有趣的參數例如innodb_log_spin_cpu_pct_hwm/lwm 能夠控制user cpu超過多少百分比時,是否還容許用戶線程繼續spin loop

本文做者:zhaiwx_yinfeng

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索