用戶往往更容易注意到低併發性能的下降,而高併發性能的改善則往往較難察覺。因此,維持低併發性能至關重要,因為它直接影響用戶體驗和升級意願[1].
根據大量用戶反饋,在升級到 MySQL 8.0 之後,用戶普遍感受到性能下降,尤其是在批量插入和聯接操作中。這一下降趨勢在更高版本的 MySQL 中變得更加明顯。此外,一些 MySQL 愛好者和測試者在升級後報告了多個 sysbench 測試中的性能下降。
這些性能問題是否可以避免?或者,更具體地說,我們應該如何科學地評估持續的性能下降趨勢?這些都是需要考慮的重要問題。
儘管官方團隊持續進行優化,但性能的逐漸惡化不容忽視。在某些場景中,可能會出現改善,但這並不意味著所有場景的性能都得到同等優化。此外,針對特定場景進行性能優化也容易以降低其他領域的性能為代價。
MySQL 性能下降的根本原因
一般來說,隨著更多功能的添加,代碼基數增長,隨著功能的不斷擴展,性能變得越來越難以控制。
MySQL 開發人員常常未能注意到性能的下降,因為每次對代碼庫的添加僅會導致非常小的性能下降。然而,隨著時間的推移,這些小的下降會累積起來,導致顯著的累積效應,使得用戶感知到 MySQL 新版本的性能明顯下降。
例如,以下圖顯示了簡單單一連接操作的性能,其中 MySQL 8.0.40 與 MySQL 8.0.27 相比顯示出性能下降:
以下圖顯示了在單一併發下的批量插入性能測試,MySQL 8.0.40 與版本 5.7.44 的性能下降:
從以上兩個圖中可以看出,版本 8.0.40 的性能表現並不理想。
接下來,讓我們從代碼層面分析 MySQL 性能下降的根本原因。以下是 MySQL 8.0 中的 PT_insert_values_list::contextualize
函數:
對應的 MySQL 5.7 中的 PT_insert_values_list::contextualize
函數如下:
從代碼比較可以看出,MySQL 8.0 的代碼似乎更優雅,似乎有所進步。
不幸的是,很多時候,正是這些代碼改進背後的動機導致了性能的下降。MySQL 官方團隊用 deque
替代了之前的 List
數據結構,這已經成為性能逐漸下降的根本原因之一。讓我們來看看 deque
的文檔:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
如上所述,在極端情況下,保留單個元素需要分配整個數組,導致記憶體效率非常低。例如,在批量插入中,需要插入大量記錄時,官方實現將每個記錄存儲在單獨的deque中。即使記錄內容很少,仍然需要分配一個deque。MySQL的deque實現為每個deque分配1KB的內存以支持快速查找。
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
官方實現使用1KB的內存來存儲索引信息,即使記錄長度不大但記錄數量很多時,內存訪問地址可能變得不連續,導致緩存友好性差。這種設計旨在改善緩存友好性,但效果並不十分明顯。
值得注意的是,原始實現使用了List數據結構,其中通過內存池分配內存,提供了一定程度的緩存友好性。儘管隨機訪問效率較低,但針對List元素的順序訪問進行優化顯著提高了性能。
在升級到MySQL 8.0時,用戶觀察到批量插入性能顯著下降,其中一個主要原因是底層數據結構發生了重大變化。
此外,儘管官方團隊改進了重做日誌機制,但這也導致了MTR提交操作效率的降低。與MySQL 5.7相比,新增的代碼顯著降低了單個提交的性能,即使整體寫入吞吐量得到了很大提升。
讓我們檢查MySQL 5.7.44中MTR提交的核心execute
操作:
讓我們來檢視 MySQL 8.0.40 中 MTR commit 的核心 execute
運作:
比較之下,很明顯在 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
函數:
MySQL 8.0.40 中的 rec_init_offsets_comp_ordinary
函數如下:
從上述代碼可以清楚看出,引入即時添加/刪除列功能後,rec_init_offsets_comp_ordinary
函數變得明顯複雜,引入了更多函數調用並添加了一個嚴重影響緩存優化的 switch 語句。由於這個函數被頻繁調用,直接影響了更新索引、批量插入和連接的性能,導致嚴重的性能損失。
此外,在 MySQL 8.0 中的性能下降不僅限於上述問題;還有許多其他方面導致整體性能下降,特別是對內聯函數擴展的影響。例如,以下代碼影響內聯函數的擴展:
根據我們的測試,ib::fatal
語句嚴重干擾了內聯優化。對於經常訪問的函數,建議避免干擾內聯優化的語句。
接下來,讓我們看一個類似的問題。row_sel_store_mysql_field 函數
被頻繁調用,其中的 row_sel_field_store_in_mysql_format
是其中的一個熱門函數。具體代碼如下:
row_sel_field_store_in_mysql_format
函數最終調用 row_sel_field_store_in_mysql_format_func
。
row_sel_field_store_in_mysql_format_func
函數無法進行內聯化,因為存在 ib::fatal
代碼。
頻繁調用效率低下的函數,每秒執行數千萬次,會嚴重影響連接性能。
讓我們繼續探討性能下降的原因。以下官方的性能優化實際上是導致聯接性能下降的根本原因之一。雖然某些查詢可能會得到改善,但這些仍然是普通聯接操作性能下降的一些原因。
MySQL 的問題不僅僅限於此。如上面的分析所示,MySQL 性能下降並非毫無原因。當一系列小問題累積時,會導致用戶體驗到明顯的性能下降。然而,這些問題往往難以識別,使得解決它們變得更加困難。
所謂的「過早優化」是萬惡之源,而這一點在 MySQL 開發中並不適用。數據庫開發是一個複雜的過程,隨著時間的推移忽視性能會使後續的性能改進變得更加具有挑戰性。
減輕 MySQL 性能下降的解決方案
寫入性能下降的主要原因與 MTR 提交問題、瞬時添加/刪除列以及其他幾個因素有關。這些在傳統方法中難以優化。然而,用戶可以通過 PGO 優化來彌補性能下降。採用適當的策略,性能通常可以保持穩定。
為了批量插入性能下降,我們的開源版本[2]將官方deque替換為改進的列表實現。這主要解決了內存效率問題,並在一定程度上減輕了性能下降。通過將PGO優化與我們的開源版本結合,批量插入性能可以接近MySQL 5.7的水平。
用戶還可以利用多線程進行並行批處理,充分利用重做日誌的改進並發性,這可以顯著提升批量插入性能。
關於更新索引問題,由於新代碼的不可避免添加,PGO優化可以幫助緩解此問題。我們的PGO版本[2]可以顯著減輕這個問題。
對於讀取性能,特別是連接性能,我們已經做出了大幅改進,包括修復內聯問題和進行其他優化。通過添加PGO,連接性能可以比官方版本提高超過30%。
我們將繼續投入時間優化低並發性能。這個過程很長,但涉及到許多需要改進的領域。
該開源版本可供測試,我們將繼續努力改進MySQL性能。MySQL性能。
參考文獻
[1] 王斌(2024)。軟件工程中的解難之道:如何使MySQL更好。
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis