當前幾乎全部的關係數據庫都採用日誌先行的方式,也就是所謂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
咱們以前慣用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
本文爲雲棲社區原創內容,未經容許不得轉載。