MySQL 8.0 性能衰退的深入分析

用戶往往更容易注意到低併發性能的下降,而高併發性能的改善則往往較難察覺。因此,維持低併發性能至關重要,因為它直接影響用戶體驗和升級意願[1].

根據大量用戶反饋,在升級到 MySQL 8.0 之後,用戶普遍感受到性能下降,尤其是在批量插入和聯接操作中。這一下降趨勢在更高版本的 MySQL 中變得更加明顯。此外,一些 MySQL 愛好者和測試者在升級後報告了多個 sysbench 測試中的性能下降。

這些性能問題是否可以避免?或者,更具體地說,我們應該如何科學地評估持續的性能下降趨勢?這些都是需要考慮的重要問題。

儘管官方團隊持續進行優化,但性能的逐漸惡化不容忽視。在某些場景中,可能會出現改善,但這並不意味著所有場景的性能都得到同等優化。此外,針對特定場景進行性能優化也容易以降低其他領域的性能為代價。

MySQL 性能下降的根本原因

一般來說,隨著更多功能的添加,代碼基數增長,隨著功能的不斷擴展,性能變得越來越難以控制。

MySQL 開發人員常常未能注意到性能的下降,因為每次對代碼庫的添加僅會導致非常小的性能下降。然而,隨著時間的推移,這些小的下降會累積起來,導致顯著的累積效應,使得用戶感知到 MySQL 新版本的性能明顯下降。

例如,以下圖顯示了簡單單一連接操作的性能,其中 MySQL 8.0.40 與 MySQL 8.0.27 相比顯示出性能下降:

Figure 1. Significant decline in join performance in MySQL 8.0.40.

以下圖顯示了在單一併發下的批量插入性能測試,MySQL 8.0.40 與版本 5.7.44 的性能下降:

Figure 2. Significant decline in bulk insert performance in MySQL 8.0.40.

從以上兩個圖中可以看出,版本 8.0.40 的性能表現並不理想。

接下來,讓我們從代碼層面分析 MySQL 性能下降的根本原因。以下是 MySQL 8.0 中的 PT_insert_values_list::contextualize 函數:

C++

 

對應的 MySQL 5.7 中的 PT_insert_values_list::contextualize 函數如下:

C++

 

從代碼比較可以看出,MySQL 8.0 的代碼似乎更優雅,似乎有所進步。

不幸的是,很多時候,正是這些代碼改進背後的動機導致了性能的下降。MySQL 官方團隊用 deque 替代了之前的 List 數據結構,這已經成為性能逐漸下降的根本原因之一。讓我們來看看 deque 的文檔:

Markdown

 

如上所述,在極端情況下,保留單個元素需要分配整個數組,導致記憶體效率非常低。例如,在批量插入中,需要插入大量記錄時,官方實現將每個記錄存儲在單獨的deque中。即使記錄內容很少,仍然需要分配一個deque。MySQL的deque實現為每個deque分配1KB的內存以支持快速查找。

Plain Text

 

官方實現使用1KB的內存來存儲索引信息,即使記錄長度不大但記錄數量很多時,內存訪問地址可能變得不連續,導致緩存友好性差。這種設計旨在改善緩存友好性,但效果並不十分明顯。

值得注意的是,原始實現使用了List數據結構,其中通過內存池分配內存,提供了一定程度的緩存友好性。儘管隨機訪問效率較低,但針對List元素的順序訪問進行優化顯著提高了性能。

在升級到MySQL 8.0時,用戶觀察到批量插入性能顯著下降,其中一個主要原因是底層數據結構發生了重大變化。

此外,儘管官方團隊改進了重做日誌機制,但這也導致了MTR提交操作效率的降低。與MySQL 5.7相比,新增的代碼顯著降低了單個提交的性能,即使整體寫入吞吐量得到了很大提升。

讓我們檢查MySQL 5.7.44中MTR提交的核心execute操作:

C++

 

讓我們來檢視 MySQL 8.0.40 中 MTR commit 的核心 execute 運作:

C++

 

比較之下,很明顯在 MySQL 8.0.40 中,MTR commit 中的 execute 運作變得更為複雜,牽涉到更多步驟。這種複雜性是低並發寫入效能下降的主要原因之一。

特別是操作 m_impl->m_log.for_each_block(write_log)log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn) 具有顯著的開銷。這些變更是為了增強高並發效能,但卻以低並發效能為代價。

重做日誌對高並發模式的優先排序導致低並發工作負載的表現不佳。雖然引入 innodb_log_writer_threads 旨在緩解低並發效能問題,但並不影響上述函數的執行。由於這些操作變得更為複雜並需要頻繁的 MTR commit,效能仍然大幅下降。

讓我們來看看即時添加/刪除功能對效能的影響。以下是 MySQL 5.7 中的 rec_init_offsets_comp_ordinary 函數:

C++

 

MySQL 8.0.40 中的 rec_init_offsets_comp_ordinary 函數如下:

C++

 

從上述代碼可以清楚看出,引入即時添加/刪除列功能後,rec_init_offsets_comp_ordinary 函數變得明顯複雜,引入了更多函數調用並添加了一個嚴重影響緩存優化的 switch 語句。由於這個函數被頻繁調用,直接影響了更新索引、批量插入和連接的性能,導致嚴重的性能損失。

此外,在 MySQL 8.0 中的性能下降不僅限於上述問題;還有許多其他方面導致整體性能下降,特別是對內聯函數擴展的影響。例如,以下代碼影響內聯函數的擴展:

C++

 

根據我們的測試,ib::fatal 語句嚴重干擾了內聯優化。對於經常訪問的函數,建議避免干擾內聯優化的語句。

接下來,讓我們看一個類似的問題。row_sel_store_mysql_field 函數被頻繁調用,其中的 row_sel_field_store_in_mysql_format 是其中的一個熱門函數。具體代碼如下:

C++

 

row_sel_field_store_in_mysql_format 函數最終調用 row_sel_field_store_in_mysql_format_func

C++

 

row_sel_field_store_in_mysql_format_func 函數無法進行內聯化,因為存在 ib::fatal 代碼。

C++

 

頻繁調用效率低下的函數,每秒執行數千萬次,會嚴重影響連接性能。

讓我們繼續探討性能下降的原因。以下官方的性能優化實際上是導致聯接性能下降的根本原因之一。雖然某些查詢可能會得到改善,但這些仍然是普通聯接操作性能下降的一些原因。

GitHub Flavored Markdown

 

MySQL 的問題不僅僅限於此。如上面的分析所示,MySQL 性能下降並非毫無原因。當一系列小問題累積時,會導致用戶體驗到明顯的性能下降。然而,這些問題往往難以識別,使得解決它們變得更加困難。

所謂的「過早優化」是萬惡之源,而這一點在 MySQL 開發中並不適用。數據庫開發是一個複雜的過程,隨著時間的推移忽視性能會使後續的性能改進變得更加具有挑戰性。

減輕 MySQL 性能下降的解決方案

寫入性能下降的主要原因與 MTR 提交問題、瞬時添加/刪除列以及其他幾個因素有關。這些在傳統方法中難以優化。然而,用戶可以通過 PGO 優化來彌補性能下降。採用適當的策略,性能通常可以保持穩定。

為了批量插入性能下降,我們的開源版本[2]將官方deque替換為改進的列表實現。這主要解決了內存效率問題,並在一定程度上減輕了性能下降。通過將PGO優化與我們的開源版本結合,批量插入性能可以接近MySQL 5.7的水平。

Figure 3. Optimized MySQL 8.0.40 with PGO performs roughly on par with version 5.7.

用戶還可以利用多線程進行並行批處理,充分利用重做日誌的改進並發性,這可以顯著提升批量插入性能。

關於更新索引問題,由於新代碼的不可避免添加,PGO優化可以幫助緩解此問題。我們的PGO版本[2]可以顯著減輕這個問題。

對於讀取性能,特別是連接性能,我們已經做出了大幅改進,包括修復內聯問題和進行其他優化。通過添加PGO,連接性能可以比官方版本提高超過30%。

Figure 4. Using PGO, along with our optimizations, can lead to significant improvements in join performance.

我們將繼續投入時間優化低並發性能。這個過程很長,但涉及到許多需要改進的領域。

該開源版本可供測試,我們將繼續努力改進MySQL性能。MySQL性能

參考文獻

[1] 王斌(2024)。軟件工程中的解難之道:如何使MySQL更好。

[2] 增強版MySQL · GitHub

Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis