1. 程式人生 > >騰訊雲佈道師:一次效能峰值提升10W的DB調優之旅

騰訊雲佈道師:一次效能峰值提升10W的DB調優之旅

作者:張青林,騰訊雲佈道師、MySQL架構師,隸屬騰訊TEG-基礎架構部-CDB核心開發團隊,專注於MySQL核心研發&相關架構工作,有著服務多個10W級QPS客戶的資料庫優化及穩定性維護經驗。

騰訊雲資料庫團隊:繼承騰訊資料庫團隊十多年海量儲存的內部資料庫運營和運維經驗,推出一系列高效能關係型、分散式、文件型和快取類資料庫產品,並提供高可用性、自動化運維和易維護的雲資料庫綜合解決方案。

前言

經過週末兩天的折騰,在大家的幫助下最終將使用者DB的效能峰值由最初的不到7W的QPS+TPS提升至17W,心情也由最初的忐忑過渡到現在的平靜,現在想來,整個的優化過程感覺還是比較好玩的,趁著現在還有些印象,就把整個排查&優化過程詳細記錄下來,以備不時之需,也希望能給其他人一些啟發。

問題背景

上週團隊聚餐時,老大說有一個使用者使用DB的時候遇到了問題,現有的DB效能無法滿足使用者的效能需求。使用者在對現有的DB進行壓力測試時發現QPS+TPS小於7W/S,繼續加大壓力的時候Load上漲、IdleCPU很低、Threadrunning飆升、效能下降,最終導致網站處理併發能力的下降,無法達到預期的吞吐量。

使用者在對現有邏輯及吞吐量計算的基礎上提出了效能指標,即DB的單機效能QPS+TPS大於10W/S,只有這樣才能滿足業務要求,否則DB就是整個鏈路的瓶頸。

由於使用者的上線時間臨近,上線壓力比較大,老大說週末盡力搞定,如果搞不定,只能上最好的機器來解決效能問題,這樣的話,成本就要上來了。(當時正在吃飯,瞬間感覺壓力山大,不能好好吃肉了有木有……)

現場資訊收集

第二天還沒醒就收到老大的RTX訊息,然後懷著疑惑的心情火速上線,登入到機器上,開始了DB效能的調優之旅……

首先,使用orzdba監控工具查看了使用者例項的效能狀態,如下所示:

 orzdba 監控

從上面的效能資訊可以發現命中率 100%, 即使用者基本是全記憶體操作,thread running 較高,CPU 有少量, thread running 彪升,到底執行緒在做什麼呢?

懷著這樣的疑問,然後執行了一下 pstack {pid of mysqld} > pid.info 以獲取例項的內部執行緒資訊,然後使用 pt-pmp pid.info 將堆疊資訊進行顯示,發現瞭如下的資訊:

內部執行緒

根據上述的 pt-pmp & pstack 檔案相結合,可以看到如下堆疊:

堆疊

根據上面收集的資訊我們可以清楚的得出以下結論:

  • 應用在執行SQL語句的過程中,table_cache_manager 中的鎖衝突比較嚴重;
  • MySQL Server 層中的 MDL_lock 衝突比較重;
  • 例項開啟了 Performance_schema 功能;

經過了上面的分析,我們需要著重檢視上述問題的相關變數,變數設定的情況會對效能造成直接的影響,執行結果如下:

SQL語句

引數分析

這裡我們先來介紹一下上述引數在 MySQL 中的作用 & 含義:

table_open_cache_instances 簡介

table_open_cache_instances 指的是 MySQL 快取 table 控制代碼的分割槽的個數,而每一個 cache_instance 可以包含不超過table_open_cache/table_open_cache_instances 的table_cache_element,MySQL 開啟表的過程可以簡單的概括為:

  1. 根據執行緒的 thread_id 確定執行緒將要使用的 table_cache,即 thread_id % table_cache_instances;
  2. 從該 tabel_cache 元素中查詢相關係連的 table_cache_element,如果存在轉 3,如果不存在轉 4;
  3. 從 2 中查詢的table_cache_element 的 free_tables 中出取一個並返回,並調整 table_cache_element 中的 free_tables & used_tables 元素;
  4.  如果 2 中不存在,則重新建立一個 table, 並加入對應的 table_cache_element 的 used_tables的列表;

 從以上過程可以看出,MySQL 在開啟表的過程中會首先從 table_cache 中進行查詢有沒有可以直接使用的表控制代碼,有則直接使用,沒有則會建立並加入到對應的 table_cache 中對應的 table_cache_element 中,從剛才提取的現場資訊來看,有大量的執行緒在查詢 table_cache 的過程中阻塞著了,而 table_open_cache_instances 的個數為 1, 因此,此引數的設定需要調整,由於 table_open_cache_instances 的大小和 執行緒 ID & 併發 有關係,考慮當前的併發是1000左右,於是將該植設定為 32;

MySQL 中不同的執行緒雖然使用各自的 table 控制代碼,但是共享著同一個table_share,如果想從原始碼上了解 table & table_share 以及 兩者之間的相互,可以從變數 table_open_cache, table_open_cache_instances,table_definition_cache 入手,閱讀 Table_cache_manager, Table_cache, Table_cache::get_table 等相關程式碼,由於篇幅限制,在此不在詳述。

MDL Lock 的前世今生

 在 5.1 中有一個 binlog log 亂序的問題,詳情及復現方法可以參考這篇文章:《alter table rename 操作導致複製中斷》(http://mysqllover.com/?p=93),MDL_LOCK 就是為了解決上述問題而在 5.5 中引入的。

簡單來說 MDL Lock 是 MySQL Server 層中的表鎖,主要是為了控制 Server 層 DDL & DML 的併發而設計的, 但是 5.5 的設計中只有一把大鎖,所以到5.6中添加了引數 metadata_locks_hash_instances 來控制分割槽的數量,進而實現大鎖的拆分,雖然鎖的拆分提高了併發的效能,但是仍然存在著不少的效能問題,所以在 5.7.4 中 MDL Lock 的實現方式採用了 lock free 演算法,徹底的解決了 Server 層表鎖的效能問題,而引數 metadata_locks_hash_instances 也將會在之後的某個版本中被刪除掉。

參考文件:metadata_locks_hash_instances(http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_metadata_locks_hash_instances)

由於例項中的表的數目比較多,而 metadata_locks_hash_instances 的引數設定僅為8,因此,為了將底鎖的衝突的可能性,我們將此值設定為 32。

Performance Schema作用&影響

通俗來說,performance schema 是 MySQL 的內部診斷器,用於記錄 MySQL 在執行期間的各種資訊,如表鎖情況、mutex 竟爭情況、執行語句的情況等,和 Information Schema 類似的是擁用的資訊都是記憶體資訊,而不是存在磁碟上的,但和 information_schema 有以下不同點:

  • information_schema 中的資訊都是從 MySQL 的記憶體物件中讀出來的,只有在需要的時候才會讀取這些資訊,如 processlist, profile, innodb_trx 等,不需要額外的儲存或者函式呼叫,而 performance schema 則是通過一系列的回撥函式來將這些資訊額外的儲存起來,使用的時候進行顯示,因此 performance schema 消耗更多的 CPU & memory 資源;
  • Information_schema 中的表是沒有表定義檔案的,表結構是在記憶體中寫死的,而 performation_schema 中的表是有表定義檔案的;
  • 實現方式不同,Information_schema 只是對記憶體的 traverse copy, 而 performance_schema 則使用固定的介面來進行實現;
  • 作用不同,Information_schema 主要是輕量級的使用,即使在使用的時候也很少會有效能影響,performance_schema 則是 MySQL 的內部監控系統,可以很好的定位效能問題,但也很影響效能;

由以上的分析不難看出,在效能要求比較高的情況下,關閉 performance_schema 是一個不錯的選擇,因此將 performance_schema 關閉。另外關閉 performance_schema 的一個原因則是因為它本身的穩定性,因為之前在使用 performance_schema 的過程中遇到了不穩定的問題,當然,遇到一個問題我們就會修復一個,只是考慮到效能問題,我們暫時將其關閉。

Performance_schema 的詳細使用說明可以參考:

  • performance_schema 中文文件

    (http://keithlan.github.io/2015/07/17/22_performance_schema/)

  • MySQL_Performance_Schema 官方文件

    (https://dev.mysql.com/doc/refman/5.6/en/performance-schema.html)

經過上面的分析和判斷,我們對引數做了如下的調整:

table_open_cache_instances=32

metadata_locks_hash_instances=32

performance_schema=OFF

innodb_purge_threads=4

勉強解決問題

調整了以上引數後,我們重啟例項,然後要求客戶做新一輪的壓力測試,測試部分資料如下:

MySQL

從以上的測試資料來看, QPS + TPS > 10W 已經滿足要求,通過 perf top -p {pidof mysqld} 命令查看了一下系統負載,發現了一處比較吃 CPU 的地方 ut_delay,詳情如下:

CPU

使用 perf record & perf report 進行分析,發現呼叫比較多的地方是:

mutex_spin_wait,於是斷定 Innodb 底層資源衝突比較嚴重,根據以往的經驗執行如下命令:

 Innodb 底層資源

在 MySQL 內部,當 innodb 執行緒獲取 mutex 資源而得不到滿足時,會最多進行 innodb_sync_spin_loops 次嘗試獲取 mutex 資源,每次失敗後會呼叫 ut_delay(ut_rnd_interval(0, srv_spin_wait_delay),導致 ut_delay 佔用了過多的 CPU, 其中 ut_delay 的定義如下:

ut_delay

由於這兩個值的設定取決於例項的負載以及資源的竟爭情況,所以不斷的嘗試設定這兩個引數的值,經過多次的嘗試最終將這兩個引數分別設定為:

innodb_spin_wait_delay = 6, innodb_sync_spin_loops = 20 (請注意這兩個值不是推薦值!!) 才將 ut_delay 的佔用資源降下來,最終降低了不必要的 CPU 消耗的同時 idle cpu 也穩定在了 20+,具體資源佔用詳情如下:

idle cpu

優化到這個地步似乎達到了客戶要求的效能,即 DB 單機效能為 QPS + TPS > 10W,可是如果併發量在加大,我們的 DB 能扛住更高的壓力嗎?

又起波瀾

經過上面引數的調整,DB 已經不是效能的瓶頸,應用的吞吐量由之前的 1100 -> 1400+,但是離 2000 的吞吐量還比較遠,瓶頸出現在了應用端,為了增加吞吐量,客戶又增加了幾臺客戶端機器,連線數也由之前的 900+ 上升到 1000+,此時發現 DB 雖然能夠響應,但偶爾會出現 thread running 飆高的情況,具體執行狀態如下,其中 mysql_com_tps = (mysql_com_insert + mysql_com_update + mysql_com_delete):

DB

18 DB

查詢問題原因

Thread running 的偶爾飆升引起了我的注意,說明內部必然有衝突,隨著壓力和併發量的不斷增大,應用可能會受到類似之前的影響,因此很有必要檢視其中的原因並盡最大的努力解決之。通過仔細觀察 thread running & mysql com 資訊,當 thread running 較高 & com 資訊較低的時候,執行了 pt-pmp -p {pid of mysqld},抓到了以下資訊:

Thread running

Thread running

從上面的現場資訊不難看出有很大一部分執行緒是在執行 read_view 的相關操作中被阻塞著了,那麼什麼是 read view,它的作用是什麼,為什麼會有大量的執行緒執行這個操作的時候被阻塞呢?

什麼是 read view

read view 又稱讀檢視,用於儲存事務建立時的活躍事務集合。當事務建立時,執行緒會對 trx_sys 上全域性鎖,然後遍歷當前活躍事務列表,將當前活躍事務的ID儲存在陣列中的同時,記錄最大事務 low_limit_id & 最小事務 high_limit_id & 最小序列化事務 low_limit_no。

read view 的作用是什麼

InnoDB record 格式包含 {記錄頭,主建,Trx_id,roll_ptr, extra_column} 等資訊。

當事務執行時,凡是大於low_limit_id 的資料對於事務是不可見的,凡是事務小於 high_limit_id 的資料都是可見的,事務 ID 是 read_view 陣列中的某一個時也是不可見的,Purge thread 在執行 Purge 操作時,凡是小於 low_limit_no 的資料,都是可以被 Purge 的,因此, read view 是 MySQL MVCC 實現的基礎。

為什麼會有大量的執行緒阻塞

事務建立時的步驟如下:

  • 對 trx_sys->mutex 全域性上鎖;
  • 順序掃描 trx_sys->rw_trx_list,對 read_view 中的元素分配記憶體並進行賦值,主要包括活躍事務ID的集合的建立,low_limit_id , high_limit_id, low_limit_no 等;
  • 將該 read_view 新增到有序列表 trx_sys->view_list中;
  • 釋放 trx_sys->mutex 鎖;

由於read_view 的建立和銷燬都需要獲取 trx_sys->mutex, 當併發量很大的時候,事務連結串列會比較長,又由於遍歷本身也是一個費時的工作,所以此處便成為了瓶頸,既然我們遇到了這個問題,那麼社群應該也有類似的問題。

read view 問題解決過程

首先,我們看一下bug#49169(https://bugs.mysql.com/bug.php?id=49169),read_view_open_now is inefficient with many concurrent sessions, 即當併發量很大時 read_view_open_now 效率低下的問題,問題的原因主要有以下幾個:

  • 整個建立過程一直持有 trx_sys->mutex 鎖;
  • read_view 的記憶體在每次建立中被分配,事務提交後被釋放;
  • 需要遍歷 trx_sys->trx_list (5.5) 或 trx_sys->rw_list (5.6);
  • 併發較大,活躍事務連結串列過長時,會在 trx_sys->mutex 上有較大的消耗;

該 bug 從 MySQL 5.1 的時候被 mrak 大神提出以來,一直到 MySQL 5.7 才被官方完整的解決,其中的解決過程也挺曲折的,另外 Percona 在 5.5 的時候就也推出瞭解決問題的辦法,實現也相對簡單好多,但沒有 MySQL 5.7 方法的徹底,咱們分別看一下這兩種解決方法以及 CDB 核心在這方面的改動。

Percona read view 問題改進

Percona 為了解決上述描述的問題,對trx_sys做了以下修改:

  • 在 trx_sys下維護一個全域性的事務ID的有序集合,事務的 建立 & 銷燬 的同時將事務的 ID 從這個集合中移除;
  • 在 trx_sys下維護一個有序的已分配序列號的事務列表,已記錄擁有最小序列號的事務,供 purge 時使用;
  • 減少不必要的記憶體分配,為每一個 trx_t 快取一個 read_view,read_view 陣列的大小根據建立時的活躍全域性事務 ID 集合做必要的調整;

做了上面的調整後,事務在建立過程中則不需要遍歷 trx_sys->trx_list(version 5.5),直接使用 memcpy 即可獲得活躍事務的ID,並且快取的使用也大大減少了記憶體的不必要分配;

更詳細的資訊及原始碼可以參考 Alexey (sysbench owner, MySQL 另一大神)提交的程式碼,commit message 詳情如下:

commit message

MySQL read view 問題改進

為了解決 read view 問題,5.6 做了以下幾件事情:

  • 將 5.5 的 trx_list 拆分為 ro_trx_list & rw_trx_list, 由於只讀事務不會對資料進行修改,所以在建立檢視的同時就只需要掃描 rw_trx_list 即可;
  • auto-commit-non-locking-ro transactions 的特殊優化;
  • 新增語法 START TRANSACTION READ ONLY 用於聲名事務是隻讀事務;

經過上面的修改,似乎解決了 read view 的問題,但實際卻不然,因為他只是解決了事務連結串列的長度,建立時遍歷&記憶體消耗的開銷是沒有解決的,並且使用上述特性需要修改應用程式,這一點是比較困難的,因此,5.7為了徹底的解決 read view 的效能問題,做了以下事情:

  1. Refactor the MVCC code
  2. Reuse read views for AC-NL-RO selects
  3. Use a pool of read views
  4. Add MVCC class
  5. Use a trx_id to trx_t* map
  6. Keep the active trx_id_ts in a vector.
  7. Pre-allocate a small cache of record and table locks
  8. Avoid extra work when a transaction is tagged as read-only (during commit).
  9. General code cleanup
  10. Get rid of trx_sys_t::ro_trx_list. Adding and removing a transaction from the ro_trx_list    is very costly.

經過了上面的程式碼重構,5.7 中很少看到 trx_sys->mutex 的效能瓶頸,有想更詳細瞭解的同學可以看一下這些內容:

trx_sys->mutex

CDB read view 問題改進

為了解決 Read view 的效能問題,簡單的說 CDB 核心團隊對於Read view 主要做了以下事情:

  • backport percona 的 read view 相關修改到 CDB MySQL中;
  • 參照 5.7 的實現,在 5.6 中將 ro_trx_list 移除;

經過上面的修改徹底的解決了 read_view 的效能問題,在經歷了大量 穩定性測試 & 效能測試 後,目前灰度釋出中。

線上效果

鑑於當前存在的問題,為了解決客戶的燃眉之急,決定上一個新版本,和客戶聯絡後,可以重啟例項,然後進行了替換操作,替換後的效能效果如下,可以看到 cpu 使用率、load、thread running 降低的同時 QPS + TPS 效能上升,至此問題真正覺得問題應該解決了,餘下的就是等客戶的反饋了。

QPS + TPS

QPS + TPS

將監控資料入庫,檢視峰值 & 當時的負載情況,詳情如下:

負載 負載

遺留問題 & 展望

真的完美了嗎,其實不是這樣的,我們還有很多的事情要做,因為在解決問題的過程中,我們通過 pstack & pt-pmp 抓到了很多有用的資訊,有一些是暫時沒有解決的,如:

  • InnoDB內部表鎖衝突嚴重;
  • MDL Lock 即使擴大也存在著不小的影響;
  • 記憶體分配也有一些需要優化的地方;
  • 執行計劃的計算代價比較高;
  • thread running 彪高時沒有可以控制的方法;
  • ….

由於時間問題我們暫時將遇到的問題一一記下,一個一個解決,我們相信 CDB 的核心會越來越強大,在提升效能的同時也不斷的提升穩定性,我們一步一步踏在當下,努力變得更好!

文章出處:DBAplus社群(訂閱號ID:dbaplus)