MySQL 5.7在高並發下性能劣化問題的詳細剖析
TL;DR
MySQL 5.7高並發讀寫混合場景下rt飆升,業務係統大量超時報錯。本文總結了阿裏業務場景下遇到的坑,剖析問題背後的原因,幫助讀者更好的理解MySQL內核原理,降低升級MySQL 5.7的風險。
引言
MySQL 5.7自發布以來備受關注,不僅是因為5.7的在功能特性上大大豐富,它的讀寫性能上相對於之前的版本也有了很大提升。正是由於5.7卓越的表現,我們自去年起就開始著手將AliSQL整體搬遷到5.7上。然而經過一年多的整合測試我們發現,5.7宣稱的有些能力表現卻不盡如人意。這裏麵當然有很多有趣的故事可以講,本文要講的這個故事卻是對MySQL 5.7引以為傲的“高並發高性能”的一個很好的回應。
自MySQL從4.0發展演進到現在的8.0,高並發場景下MySQL的性能越來越強,以下是dimitrik針對MySQL的多個版本做的性能對比測試。
可以看到MySQL 5.7的讀能力有了很大的提升,這是因為它針對讀性能做了很多優化,其中就包括了InnoDB引擎層MVCC機製的改進。本文要介紹的性能退化問題就和這個MVCC機製的改進密切相關。
背景介紹
在說明問題之前有必要交代一下InnoDB的多版本控製(MVCC),與MVCC密切相關的是快照讀。所謂快照讀既是無鎖讀,那麼它是用來解決什麼樣問題的?
我們知道InnoDB是一個支持事務的引擎,事務的一個重要特性是隔離性,即還未提交的事務的修改對外界是不可見的。為了實現事務隔離性數據庫一般使用兩種實現手段,分別是當前讀和快照讀。
所謂當前讀就是加鎖讀,事務對訪問的數據進行加鎖,事務提交時釋放所持有的鎖。由於鎖的互斥性,正在被活躍事務修改的數據無法被其它事務訪問,必須等到修改它的事務提交。這種方式的優點是實現簡單,但缺點也很明顯,並發性能差。
而快照讀的特點是多個事務訪問相同數據時不需要加鎖,可以並發執行。具體做法是一份數據保存多個曆史版本,不同事務訪問不同曆史版本的數據彼此之間互不影響。不過快照讀也有一個明顯的缺點,那就是事務對數據隻能讀不能修改。
InnoDB的事務在修改一份數據時對其進行加鎖讀,而隻讀不改數據的時候進行快照讀,最大限度的提高事務的並發性能。
再來講講快照讀的具體實現,這裏涉及到幾個問題:1,曆史版本如何保存;2,如何拿到正確版本的曆史數據;3,曆史數據如何回收。
數據的曆史版本有兩種保存方法:一種保存全量的曆史數據,另一種保存能夠回滾到曆史數據的undo日誌。InnoDB采用的是後一種方式,事務更新數據同時產生一份對應的undo日誌,並且日誌中記錄了事務id。
事務使用快照(readview)去讀取數據正確的曆史版本,快照中包含的信息是當前所有活躍事務的id。開始讀之前分配一個快照(read-committed和repeatable-read隔離級別下快照分配方式略有不同),實際就是對當前所有的活躍事務做了個快照。在讀取數據過程中,由於需要通過undo日誌構造曆史版本,而每個undo日誌有一個事務id,對比undo日誌中的事務id和readview中的事務id就可以判斷曆史版本是否可見。說直白點就是,快照創建的那一刻生成這份曆史數據的事務是已提交狀態(可見)還是未提交(不可見)。
最後,曆史數據是需要回收的,不然undo日誌占用的空間會越來越大。InnoDB回收曆史數據的任務由後台purge線程完成,回收的原則是:隻能回收那些永遠不會再被用到的undo日誌。具體做法是:purge線程從當前所有readview中找到創建時間最久的那個oldest readview,那些比oldest readview還要舊的undo日誌就是可以被安全回收的。因此,InnoDB維護了一個全局的readview鏈表,鏈表中的readview按照創建時間排序,purge線程隻須找到鏈表尾端的readview就是oldest readview。
5.7的改進
根據上麵的介紹可以知道,事務進行一次快照讀的步驟如下:
1. 分配一個快照對象
2. 將快照對象加入到全局readview鏈表頭部
3. 對當前活躍事務打快照
3. 進行快照讀
4. 完成後,將快照對象從全局readview鏈表中移除
需要說明的是,rr隔離級別下每個事務使用一個快照,而rc隔離級別下每條query使用一個readview,所以快照的分配和釋放與事務的開始和結束不完全對應。
同時,後台purge線程進行一次purge操作的過程是
1. 從全局readview鏈表中找到oldest readview
2. 使用oldest readview去回收undo日誌
看到這裏可以很清楚的明白一件事:必定存在一把鎖保護全局readview鏈表。沒錯,這把鎖就是InnoDB事務係統的全局大鎖(trx_sys->mutex)。這把大鎖不光保護了全局的readview鏈表,還保護了全局的活躍事務鏈表等對象,事務在begin和commit等過程中也要競爭這把大鎖,所以這是一把比較熱的鎖。
為了減少這把鎖的爭用,MySQL 5.7對一些特定場景做了優化。比如對於autocommit的隻讀事務,優化了readview的創建過程。優化的原理是:如果上次快照讀與這次快照讀之間沒有寫事務發生,那麼一定也沒有任何數據被修改,所以理論上可以直接複用上次的readview,這樣做能夠很大程度減少事務係統全局鎖的爭用開銷。
根據WL#6578的描述,優化後隻讀事務快照讀的步驟如下:
改進之後,autocommit隻讀事務完成快照讀後,**並不會將快照從全局readview鏈表中刪除,而隻是將它設置成close狀態**。再次進行快照讀時,如果可以複用上次的快照則不需要操作全局readview鏈表,自然也不需要爭用事務係統全局鎖。而如果不能複用快照,依然需要操作全局readview鏈表,但是相對於改進前少了一次全局事務鎖的爭用。根據前麵dimitrik做的5.6與5.7隻讀事務性能比較對比測試圖,可以看到隻讀場景下5.7性能有了飛躍提升。
帶來的問題
這樣優化之後雖然對autocommit的隻讀事務的性能友好,但是某些場景下反而帶來了性能劣化。為什麼這麼講呢?因為我們實實在在踩到了這個坑!
讓我們從原理上分析性能劣化的原因,這樣修改之後雖然autocommit隻讀事務減少了對全局readview鏈表的操作,但是全局readview鏈表中卻多出了很多close狀態的readview,鏈表長度無端變長了!!
這樣帶來的最直接的影響是purge線程在獲取oldest readview的開銷變大了。之前隻要獲取全readview最末尾的readview就是oldest readview,但是修改之後需要從全局readview鏈表末端往前遍曆,直到
找到第一個非close狀態的readview。
在我們的業務場景下有非常多autocommit的query語句,這些query查詢就是一個個的autocommit隻讀事務,因此它們提交時readview會留在全局readview鏈表中。下圖是我們在全鏈路壓測時一個業務節點上抓取的全局readview鏈表的長度。
這樣一萬多個readview大部分都是close狀態,導致purge線程在查找oldest readview時須對鏈表進行大量的掃描,直接導致此過程非常耗時。下圖是我們用perf抓的函數cpu開銷
更加要命的是,後台purge線程在掃描全局readview鏈表時持有事務係統的全局鎖,從而導致這把鎖的爭用更加激烈,從perf結果上可以看到ut_delay()函數調用穩居前列。
全鏈路壓測過程我們一個核心業務上第一次暴露此問題,直接反應就是rt飆升吞吐下降。下圖是壓測過程中業務的rt表現(單位:us,平均RT已經到了8ms左右)
為什麼是全鏈路壓測過程中暴露此問題,因為此時業務場景滿足了以下幾個條件:
1. 活躍連接數夠多,高峰能達到10000+;
2. autocommit隻讀事務和寫事務混合並發;
3. 大部分是單條sql的簡單事務;
這是因為,有autocommit隻讀事務且事務並發度高才會導致全局readview鏈表中close的readview足夠多。有寫事務並發才會產生undo日誌,後台purge線程才需要進行回收,這樣才會去查找oldest readview。都是單條sql的簡單事務,事務begin和commit的開銷占比才夠大,競爭事務係統的全局大鎖才能影響到rt和吞吐。
雖然是全鏈路壓測過程中才集中爆發的問題,但是我們通過複盤係統監控發現此問題會經常導致rt飆升。這是因為業務習慣使用autocommit隻讀事務(autocommit=1時,一條query就是一個autocmmit隻讀事務),一旦讀壓力上升就會導致全局readview鏈表變長,後台purge線程獲取oldest readview時會"長時間"持有全局事務鎖,此時必然影響所有事務的begin和commit,尤其對於大多數小事務場景特別明顯。
由於這是一個非常普遍的問題,我們已經將它提交給了MySQL官方。所以,如果平時你的係統會有rt突然飆升的情況發生,可以考慮從這個方向思考。
如何解決
問題產生的根本原因是全局readview鏈表中close狀態的readview太多了,導致查找oldest readview時需要遍曆非常多的無效節點,而產生這個問題的原因是autocommit隻讀事務延遲釋放readview。清楚這一點後問題就很好解決了:autocommit隻讀事務結束快照讀後隨即將readviwe從全局鏈表中刪除。這樣修改後,查找oldest readview時依然隻需找到全局readview鏈表最後一個節點即可,非常地快捷。
修改後的影響:
1, 隻讀場景是否會降低到5.6的水平?
答:不會的,5.7對隻讀場景的優化是多方麵的,既包括上層元數據鎖的優化也包括InnoDB層的若幹優化,這些優化疊加起來在我們的環境下測試約有30%+的性能提升,而readview延遲的
優化隻占了很小一塊,根據我們的測試性能下降大概在5%以內
2, 對純讀寫事務並發是否有影響?
答:沒有影響,延遲釋放readview隻對autocommit隻讀事務有效,讀寫事務的readview完成快照讀後依然是要從全局readview鏈表中摘除,因此此修改對讀寫事務沒有影響。
3, 對隻讀事務和讀寫事務混合場景的影響?
答:此場景下性能是有提升的,因為能夠降低事務係統全局鎖的競爭。下圖是修改後的rt(單位:us。相同的壓力下,RT從修複前的8ms下降到了0.2ms)
最後更新:2017-10-12 13:03:53
上一篇:
雲棲大會開幕 聊聊阿裏的AI智能戰略
下一篇:
移動硬盤不小心格式化後的數據如何恢複
JUNIT在線API以及 assertTrue(boolean condition),assertTrue(String message, boolean condition)
《數據分析實戰:基於EXCEL和SPSS係列工具的實踐》一第3章
Java1.6多線程之同步方法
job:經典邏輯訓練題(1-39)(持續解答)
java IDE Eclipse
【雲棲直播】精彩推薦第2期:首屆阿裏巴巴研發效能嘉年華
ffmpeg入門之 Tutorial02
Blockchain and its Impact on Different Sectors
【深度】“信息瓶頸”理論揭示深度學習本質,Hinton說他要看1萬遍
網站建設和網站運營需要樣注意什麼?