天天看點

MySQL 8.0:新的無鎖,可擴充的WAL設計

Write Ahead Log(WAL)是資料庫中最重要的元件之一。對資料檔案的所有更改都記錄在WAL中(稱為InnoDB中的重做日志)。這允許推遲将修改後的頁面重新整理到磁盤的時刻,仍然可以防止資料丢失。

在寫入重做日志時,寫入密集型工作負載的性能受到同步的限制,其中涉及許多使用者線程。在具有多個CPU核心和快速儲存設備(如現代SSD磁盤)的伺服器上測試性能時,這一點尤為明顯。

MySQL 8.0:新的無鎖,可擴充的WAL設計

我們需要一種新設計來解決目前和未來客戶和使用者所面臨的問題。調整舊設計以實作可擴充性不再是一種選擇。新設計也必須具有靈活性,以便我們可以将其擴充為将來進行分片和并行寫入。使用新設計,我們希望確定它能夠與現有API一起使用,最重要的是不要違反InnoDB其餘部分所依賴的合同。在這些限制下的挑戰性任務。

MySQL 8.0:新的無鎖,可擴充的WAL設計

重做日志可以看作是生産者/消費者持久隊列。執行更新的使用者線程可以看作是生産者,當InnoDB必須進行崩潰恢複時,恢複線程就是消費者。伺服器運作時,InnoDB不會從重做日志中讀取。

MySQL 8.0:新的無鎖,可擴充的WAL設計

但是,編寫具有多個生産者的可擴充日志隻是問題的一部分。還有InnoDB特定的細節也需要工作。最大的挑戰是保留髒頁面清單的總順序(也就是重新整理清單)。每個緩沖池有一個。對頁面的更改應用于所謂的迷你事務(mtr)中,這允許以原子方式修改多個頁面。當一個迷你事務送出時,它會将自己的日志記錄寫入日志緩沖區,進而增加名為LSN(日志序列号)的全局修改号。mtr具有需要添加到緩沖池特定重新整理清單的髒頁清單。每個重新整理清單在LSN上排序。在舊設計中,我們儲存了log_sys_t :: mutex和log_sys_t :: flush_order_mutex 以鎖步方式確定修改LSN上的總訂單保持在重新整理清單中。

MySQL 8.0:新的無鎖,可擴充的WAL設計

請注意,當某個mtr添加其髒頁(持有flush_order_mutex)時,另一個線程可能正在等待擷取flush_order_mutex(即使它想要将頁面添加到其他重新整理清單)。在這種情況下,等待線程持有log_sys_t :: mutex (以維持總順序),是以任何其他想要寫入日志緩沖區的線程都必須等待...删除這些互斥鎖後,無法保證順序重新整理清單。

MySQL 8.0:新的無鎖,可擴充的WAL設計

第二個問題是我們無法将完整的日志緩沖區寫入磁盤,因為LSN序列中可能存在漏洞,因為對日志緩沖區的寫入沒有按任何特定順序完成。

MySQL 8.0:新的無鎖,可擴充的WAL設計

第二個問題的解決方案是跟蹤哪些寫入完成,為此我們發明了一種新的無鎖資料結構。

MySQL 8.0:新的無鎖,可擴充的WAL設計

新資料結構具有固定大小的插槽陣列。插槽以原子方式更新并以循環方式重用。單個線程用于周遊和清除它們,在一個洞(空槽)處暫停。該線程更新最大可達LSN(M)。

MySQL 8.0:新的無鎖,可擴充的WAL設計

使用了此資料結構的兩個執行個體:recent_written和recent_closed。最近寫的instance用于跟蹤對日志緩沖區的已完成寫入。它可以提供最大LSN,進而完成對較小LSN值的所有寫入日志緩沖區的寫入。潛在的崩潰恢複需要在這樣的LSN處結束,是以它是我們考慮下一次寫入的最大LSN。插槽由相同的線程周遊,然後将日志緩沖區寫入磁盤。讀取/寫入插槽時設定的障礙保證了對日志緩沖區的讀寫操作的正确記憶體順序。

MySQL 8.0:新的無鎖,可擴充的WAL設計

我們來看看上面的圖檔。假設我們再次寫入日志緩沖區:

MySQL 8.0:新的無鎖,可擴充的WAL設計

現在,專用線程(log_writer)進入,周遊插槽:

MySQL 8.0:新的無鎖,可擴充的WAL設計

并更新沒有漏洞的最大LSN可達 - buf_ready_for_write_lsn:

MySQL 8.0:新的無鎖,可擴充的WAL設計

新資料結構的recent_closed執行個體用于解決與缺少log_sys_t :: flush_order_mutex相關的問題。要了解重新整理清單順序問題和無鎖解決方案,需要更詳細的解釋。

單個重新整理清單受其内部互斥鎖保護。但是我們不再保證按照增加LSN值的順序将髒頁添加到重新整理清單的保證。但是,必須滿足的兩個限制是:

  1. 檢查點- 如果LSN = L1有一個髒頁,其中L1 <L2,我們不能在LSN = L2處寫模糊檢查點。那是因為恢複從這樣的checkpoint_lsn開始。
  2. 法拉盛-  紅暈清單沖洗應始終從沖洗清單中最早的網頁。這樣我們更喜歡重新整理很久以前修改過的頁面,并且還有助于推進checkpoint_lsn。

在recent_closed執行個體中,我們跟蹤将髒頁添加到重新整理清單的并發執行,并跟蹤最大LSN(稱為M),以便完成較小LSN值的所有執行。線上程将其髒頁添加到重新整理清單之前,它等待直到M不是那麼遠。然後它添加頁面,然後将完成的操作報告給recent_closed。

MySQL 8.0:新的無鎖,可擴充的WAL設計

我們來舉個例子吧。假設某些mtr在送出期間将其所有日志記錄複制到start_lsn和end_lsn之間的LSN範圍的日志緩沖區中。它報告了對recent_written的完成寫入(日志記錄可能從現在開始寫入磁盤)。然後mtr必須等到它成立:start_lsn - M <L,其中L是一個常量,它限制了重新整理清單中的順序可能會失真的程度。條件成立後,mtr會将所有髒頁添加到緩沖池特定的重新整理清單中。現在,讓我們看一下重新整理清單。假設last_lsn是重新整理清單中最後一頁的LSN(最早添加在那裡)。在舊設計中,它是那裡最早的修改頁面,是以保證了重新整理清單中的所有頁面都具有oldest_modification> =last_lsn。在新設計中,隻保證重新整理清單中的所有頁面都具有oldest_modification> = last_lsn - L.條件成立,因為我們總是在插入頁面之前等待M太遠。

證明。假設我們有兩頁:P1為LSN = L1,P2為LSN = L2,P1先加入沖洗清單,但L2 <L1-L。在插入P1之前,我們確定L1-M <L。那麼M <= L2,因為還沒有插入P2,是以我們無法将L2推進到L2。是以L> L1 - M> = L1 - L2,是以L2> L1 - L.沖突 - 我們假設L2 <L1 - L.

MySQL 8.0:新的無鎖,可擴充的WAL設計

是以,我們放寬了之前的總訂單限制,但與此同時,我們為新訂單提供了足夠好的屬性。重新整理清單中的順序僅在本地失真,并且較小的LSN值的丢失髒頁面僅在最近的大小為L的時段内可能。這對于限制#2來說足夠好,并且它還允許選擇last_lsn -L作為候選對于檢查點LSN,滿足限制#1。

MySQL 8.0:新的無鎖,可擴充的WAL設計

這會影響恢複的方式。恢複邏輯可以從指向某個mtr中間的LSN開始,在這種情況下,它需要找到之後開始的第一個mtr,然後從那裡開始解析。現在,讓我們回到我們的例子。将所有頁面添加到重新整理清單後,start_lsn和end_lsn之間的已完成操作将報告給recent_closed。從那時起,log_closer線程可以周遊完成的添加,從start_lsn到end_lsn,并更新所有添加完成的最大LSN(将M設定為end_lsn)。

MySQL 8.0:新的無鎖,可擴充的WAL設計

由于無鎖日志緩沖區和重新整理清單中的輕松順序,并發迷你事務的送出之間的同步可以忽略不計!

到目前為止,我們描述了将頁面更改寫入重做日志緩沖區并将髒頁面添加到緩沖池特定的重新整理清單。讓我們來看看當我們需要将日志緩沖區寫入磁盤時會發生什麼。

我們為與重做日志寫入相關的特定任務引入了專用線程。使用者線程不再對重做檔案本身進行寫入。他們隻是等待他們需要重做重新整理到磁盤并且還沒有重新整理。

MySQL 8.0:新的無鎖,可擴充的WAL設計

該log_writer線程保持寫日志緩沖區的OS頁面緩存,甯願隻寫全塊,以避免以後需要覆寫不完整的塊。隻要資料在日志緩沖區中,就可以寫入。在舊設計中,寫入是在發生寫入資料的要求時啟動的,在這種情況下,寫入了整個日志緩沖區。在新設計中,寫入由專用線程驅動。它們可能更早開始,每次寫入的資料量可能由更好的政策驅動(例如,跳過不完整的塊)。log_writer線程還負責write_lsn的更新(寫完成後)。

有一個log_flusher線程,負責讀取write_lsn,調用fsync()調用和更新flushed_to_disk_lsn。這種對OS緩存和fsync()調用的寫入由兩個不同的并行線程以自己的速度驅動,它們之間的唯一同步發生在OS / FS的内部(write_lsn的原子讀寫除外) 。

MySQL 8.0:新的無鎖,可擴充的WAL設計

當事務送出時,相應的線程執行最後一個mtr,然後它需要等待重新整理到mtr的end_lsn的重做日志。在舊設計中,使用者線程要麼啟動fsync()本身,要麼等待其他使用者線程先前啟動的挂起fsync()的全局IO完成事件(然後在需要時重試)。

MySQL 8.0:新的無鎖,可擴充的WAL設計

在新設計中,它隻是等待,除非  flushed_to_disk_lsn 已經足夠大,因為它始終是執行fsync()的log_flusher線程。用于等待的事件被分片以提高可伸縮性。連續的重做塊以循環方式配置設定給連續的分片。等待flushed_to_disk_lsn的線程> = X,選擇X所屬的分片。這會減少嘗試等待時所需的同步。但更重要的是,由于這種分裂,我們隻能喚醒那些對進階flushed_to_disk_lsn感到滿意的線程(除了一些在最後一個塊中等待的線程)。

MySQL 8.0:新的無鎖,可擴充的WAL設計

當flushed_to_disk_lsn被提前時,log_flush_notifier線程喚醒等待LSN中間值的線程。請注意,當log_flush_notifier忙于通知時,可以在log_flusher線程中啟動下一個fsync()調用!

MySQL 8.0:新的無鎖,可擴充的WAL設計

當innodb_flush_log_at_trx_commit = 2 時使用相同的方法,在這種情況下,使用者不關心fsyncs()那麼多,隻等待對OS緩存的完成寫入(在這種情況下,log_write_notifier線程會通知它們   ,這與log_writer同步)write_lsn上的線程)。

因為等待事件并被喚醒會增加延遲,是以可以使用可選的自旋循環。除非我們在伺服器上沒有太多的空閑CPU資源,否則預設使用它。您可以通過新的動态系統變量來控制它:innodb_log_spin_cpu_abs_lwm和innodb_log_spin_cpu_pct_hwm。

MySQL 8.0:新的無鎖,可擴充的WAL設計

正如我們在開始時提到的,重做日志可以被視為生産者/消費者隊列。InnoDB依賴于模糊檢查點,潛在的恢複需要從這些檢查點開始。通過重新整理髒頁,InnoDB允許向前移動檢查點LSN。這允許我們回收重做日志中的空閑空間(在檢查點LSN基本上被認為是空閑之前的塊)并且還使得潛在的恢複更快(更短的隊列)。

MySQL 8.0:新的無鎖,可擴充的WAL設計

在舊設計中,使用者線程在選擇将寫入下一個檢查點的線程時互相競争。在新設計中,有一個專用的log_checkpointer線程,它監視重新整理清單中最舊的頁面,并決定寫下一個檢查點(根據多個标準)。這就是為什麼主線程不再需要處理定期檢查點的原因。通過新的無鎖設計,我們還将預設時間從7s減少到1s。這是因為我們可以更快地處理事務,因為設定了7s(我們寫了更多的資料/ s,是以更快的潛在恢複是這種變化的動機)。

新的WAL設計在更新資料時提供更高的并發性,在使用者線程之間提供非常小的(可忽略的)同步開銷!

讓我們看看在新的重做日志之前和之後的版本之間進行的簡單比較。這是一個針對8個表的sysbench oltp update_nokey測試,每個表有10M行,innodb_flush_log_at_trx_commit = 1。

繼續閱讀