做者:高鵬(網名八怪)
文章末尾有他著做的《深刻理解 MySQL 主從原理 32 講》,深刻透徹理解 MySQL 主從,GTID 相關技術知識。
本文來源:轉載自公衆號-老葉茶館,
(做者簡書: https://www.jianshu.com/p/40b...)
*愛可生開源社區出品,原創內容未經受權不得隨意使用,轉載請聯繫小編並註明來源。
今天遇到一個朋友的線上問題,大概意思就是說,我有一個線上的大事務大概 100G 左右,正在作回滾,當前看起來彷佛影響了線上的業務,而且回滾很慢,是否能夠減輕對線上業務的影響。而且朋友已經取消了雙 1 設置,可是沒有任何改觀。版本:MySQL 5.6
首先咱們須要知道的是,MySQL 並不適合大事務,大概列舉一些 MySQL 中大事務的影響:php
基於如上一些不徹底的列舉,咱們應該在線上儘量的避免大事務。好了咱們下面來進行問題討論。html
前面已經說了,咱們已經取消了雙 1 設置,所謂的雙 1 就是 sync_binlog=1 和 innodb_flush_log_at_trx_commit=1。這兩個參數線上要保證爲 1,前者保證 binlog 的安全,後者保證 redo 的安全,它們在數據庫 crash recovery 的時候起到了關鍵做用,不設置爲雙 1 可能致使數據丟失。具體的參數含義不作過多討論。可是這裏的問題是即使取消了雙 1,沒有任何改觀,所以彷佛說明 IO問題不是主要瓶頸呢?mysql
下面咱們來看幾個截圖:ios
咱們重點觀察 vmstat 的 r 和 b 列發現,IO 隊列沒有什麼問題,而且 wa% 並不大。咱們觀察 iostat 中的 %util 和讀寫數據大小來看問題不大,而且 tps 遠沒達到極限(SSD 盤)。咱們 top -Hu 能夠觀察到 %us 不小,而且有線程已經打滿了(99.4%CPU)一個 CPU 核。 所以咱們能夠將方向轉爲研究 CPU 瓶頸的產生,但願可以對問題有幫助,而後從提供的 perf top 中咱們有以下發現:算法
好了咱們將問題先鎖定到 lock_number_of_rows_locked 這個函數上。sql
朋友用的 5.6,可是我這裏以 5.7.26 的版本進行描述。而後下一節描述 5.6 和 5.7 算法上的關鍵差別。不知道你們是否注意過 show engine innodb status 中的這樣一個標誌:數據庫
這個標記就來自函數 lock_number_of_rows_locked,含義爲當前事務加行鎖的行數。而這個函數包裹在函數 lock_print_info_all_transactions 下面,lock_print_info_all_transactions 函數是打印咱們一般看到 show engine innodb status 中事務部分的核心參數。咱們來看一下簡單的流程:安全
PrintNotStarted print_not_started(file);//創建一個結構體,目的是作not start 事務的打印 ut_list_map(trx_sys->mysql_trx_list, print_not_started); //這個地方打印出那些事務狀態是no start的事務。mysql_trx_list是全事務。 consttrx_t* trx; TrxListIterator trx_iter; //這個迭代器是trx_sys->rw_trx_list 這個鏈表的迭代器 consttrx_t* prev_trx = 0; /* Control whether a block should be fetched from the buffer pool. */ bool load_block = true; bool monitor = srv_print_innodb_lock_monitor && (srv_show_locks_held != 0); while((trx = trx_iter.current()) != 0) { //經過迭代器進行迭代 ,顯然這裏不會有隻讀事務的信息,所有是讀寫事務。 ... /* If we need to print the locked record contents then we need to fetch the containing block from the buffer pool. */ if(monitor) { /* Print the locks owned by the current transaction. */ TrxLockIterator& lock_iter = trx_iter.lock_iter(); if(!lock_trx_print_locks( //打印出鎖的詳細信息 file, trx, lock_iter, load_block))
簡單的說就是先打印哪些處於 not start 的事務,而後打印那些讀寫事務的信息,固然咱們的回滾事務確定也包含在其中了,須要注意的是隻讀事務 show engine 不會打印。對於處於回滾狀態的事務咱們能夠在 show engine 中觀察到以下信息:函數
函數 trx_print_low 能夠看到大部分的信息,這裏就不詳細解釋了。既然如此咱們須要明白 lock_number_of_rows_locked 是如何計算的,下面進行討論。工具
上面咱們說了函數 lock_number_of_rows_locked 函數會打印出當前事務加行鎖的行數。那麼咱們來看一下 5.6 和 5.7 算法的不一樣。
5.7.26
實際上只有以下一句話:
return(trx_lock->n_rec_locks);
咱們能夠看到這是返回了一個計數器,而這個計數器的遞增就是在每行記錄加鎖後完成的,在函數 lock_rec_set_nth_bit 的末尾能夠看到 ++lock->trx->lock.nreclocks,所以這是一種預先計算的機制。所以這樣的計算代價很低,也不會因爲某個事務持有了大量的鎖,而致使計算代價太高。
5.6.22
隨後我翻了一下 5.6.22 的代碼,發現徹底不一樣以下:
for(lock= UT_LIST_GET_FIRST(trx_lock->trx_locks); //使用for循環每一個獲取的鎖結構 lock!= NULL; lock= UT_LIST_GET_NEXT(trx_locks, lock)) { if(lock_get_type_low(lock) == LOCK_REC) { //過濾爲行鎖 ulint n_bit; ulint n_bits = lock_rec_get_n_bits(lock); for(n_bit = 0; n_bit < n_bits; n_bit++) {//開始循環每個鎖結構的每個bit位進行統計 if(lock_rec_get_nth_bit(lock, n_bit)) { n_records++; } } } } return(n_records);
咱們知道循環自己是一種 CPU 密集型的操做,這裏使用了嵌套循環實現。所以若是在 5.6 中若是出現大事務操做了大量的行,那麼獲取行鎖記錄的個數的時候,將會出現高耗 CPU 的狀況。
有了上面的分析咱們很清楚了,觸發的緣由有以下幾點:
這樣當在統計這個大事務行鎖個數的時候,就會進行大量的循環操做。從現象上看就是線程消耗了大量的 CPU 資源,而且處於 perf top 的第一位。
知道了緣由就很簡單了,找出爲頻繁使用 show engine innodb status 的監控工具,隨後業務所有恢復正常,IO 利用率也上升了以下:
固然若是可以使用更新的版本好比 5.7 及 8.0 版本將不會出現這個問題,能夠考慮使用更高版本。分析性能問題須要首先找到性能的瓶頸而後進行集中突破,好比本例中 CPU 資源消耗更加嚴重。也許解決問題就在一瞬間。
最後經過朋友後面查詢的 bug 以下:https://bugs.mysql.com/bug.ph... 發現印風(翟衛翔)已經在多年前提出過了這個問題,而且作出了修改意見,而且這個修改意見官方採納了,也就是上面咱們分析的算法改變。通過印風(翟衛翔)的測試有 bug 中有以下描述:
也就是 CPU 消耗會高達 20%。
下面是 5.7.26 調用棧幀:
最後推薦高鵬的專欄《深刻理解 MySQL 主從原理 32 講》,想要透徹瞭解學習 MySQL 主從原理的朋友不容錯過。