1. 程式人生 > >大量MySQL表導致服務變慢的問題

大量MySQL表導致服務變慢的問題

背景

有一個業務需要分 1000 個庫,每一個庫中都有 80 個表,總共就是 80000 * 2 個檔案。檔案使用率還挺高,大概是 60000 * 2。

這個業務採用的高可用架構是 MMM,由於叢集機器在硬體檢查時發現有問題,必須要換掉。於是想了一個比較簡單、影響面較小的方法去解決,就是找了另外兩臺機器遷移過去。同時,要求這四臺機器屬於同一個網段,VIP(虛擬 IP 地址)在機器之間可以漂移,這樣業務就不需要修改 IP 地址即可遷移,相當於兩次主從切換過程。

切換方案如圖 16.1 所示。

從圖 16.1 中可以看到,切換過程很簡單,如下三步。

  1. 先將原來的寫節點(db1)與一個新的節點(db3)切換成一套叢集,也就是把在 db2 上面的 VIP(讀流量)切換到 db3 上面,此時 db1 與 db3 組成一套新叢集。
  2. 接著將 db1 和 db3 的角色互換,讓 db3 成為寫節點,db1 成為讀節點。
  3. 最後,再將 db1 讀節點上面的 VIP(讀流量)切換到 db4 上面,此時新的叢集就是 db3 和 db4,db3 為寫節點,已經切換完成。這樣的變更是晚上做的。做好之後,觀察了一段時間,發現沒有什麼問題(因為壓力小),所以覺得事情完成了,睡吧。 第二天上班之後,業務反映說遷移之後,資料庫比原來慢了 10 倍(10 倍啊!感覺不可思議)。詢問了一番,說沒有任何變更,只是做了遷移之後就成這樣了。同時經過觀察,只有寫庫的讀操作變慢了,而讀庫的讀是不慢的。最後,業務已經受不了了,要求切換回去。還好,db1 還正在從 db3 複製,做了一個回退操作,把寫掛在 db1 上面,把讀掛在 db3 上面。神奇的是,問題解決了!好吧,那就先這樣,走出去的路不能回頭,總是要遷出去的,所以先在新舊兩臺機器上面掛著,查明原因後再切換回去(這樣少做一步)。

以上是背景。

問題分析

環境對比

  • db1 寫入時,db1 寫不慢,讀不慢,db3 也不慢。
  • db3 是新的硬體,db1 是老的、有問題的硬體。
  • db3 切換成寫之後,在慢查詢檔案中明顯看到很多慢查詢(使用相同的語句查詢,原來是 50ms,現在是 500ms),和監控是一致的。
  • db1 和 db3 配置檔案有差別,如圖 16.2 所示(左邊是 db3 的,右邊是 db1 的)。

其他方面,環境完全相同,業務方面沒有任何更改,重現慢的現象,只是需要切換而已。
圖 16.3 是切換過程中的監控圖,高起來的就是把流量切換到 db3 的情況,處於低谷的就是切換到 db1 的情況,效果非常明顯,慢得立竿見影,好神奇!

原因分析

  • 從對比中可以得知,db1 是正常的(以前長時間在這個機器上跑,沒有問題),而 db3 是不正常的。這個業務目前是讀多寫少,現在的現象是讀慢。因為寫少,沒有發現慢,就不考慮了。
  • 接著就是硬體的區別,二者都是 PCI-e 卡,老的、壞的概率比較大,從經驗上來看新的會比較好,這是一個值得懷疑的點。但實際上,針對這個問題,找到了新卡的技術人員進行分析,將寫切換到 db3 上之後觀察,發現 IO 非常小,能看到的監控指數都非常正常。(他們也很納悶。)
  • 除此之外,唯一的區別就是二者的配置了,但從圖 16.3 中可以看到,沒有一個引數可以影響到讓資料庫的響應時間是原來的 10 倍。

但上面這些都只是分析,硬體測試之後,沒有發現問題(也不能說就不是硬體的問題,一直吊在那裡)。那隻剩下配置了,所以接下來從這裡入手吧,希望能成功!

那麼,再找一個夜深人靜的夜晚……

案例解決

首先要做的事情是,把 db3 的讀流量切換到 db1,然後把配置完全換成 db1 的配置,將資料庫重啟,然後上線。此時,db1 是寫節點,db3 是讀節點,最神奇的時刻即將到來。

切換之後,經過觀察,竟然沒有問題了。問題已經解決,那麼說明還是上面列出來的配置差別引起的問題。

那麼解決之後,下面的工作就是重複一開始的工作,把 db1 下線,讓 db4 上線。此刻,之前的遷移工作已經完成,線上服務沒有問題。

但……開發同學,能給我半個小時,讓我看看是哪個引數引起的麼? 得到的回答是:“迅速點,就這一次,給你 20 分鐘。”

把最有可能的引數找出來,比如字符集(實際上,上面列出的每一個,我們認為都不會有多大影響),考慮到字符集是不可動態修改的引數,所以先把這個改了。重啟,然後一個一個地動態修改、業務重啟重連等,都沒有發現。修改的這些引數包括:sql_mode、join_buffer_size、max_heap_size、sort_buffer_size,這些都沒有影響。

結果已經說好的只有這一次,那就這樣吧,任務成功完成,問題解決失敗。

然而,這個問題“才下手頭,卻上心頭”,總有一件事放心不下,約吧。 和開發商量了一下,我們想解決這個問題,知道其所以然,防止在其他業務上出現同樣的問題。好吧,再給你們一次機會(來之不易啊)。

那麼,再找一個夜深人靜的夜晚……這次的月亮好像比上次更圓一些,是好日子的徵兆麼?

操作之前,還簡單規劃了一下,下面是當時的一個計劃步驟。

^* 黑體的表示已經專門測試過,沒有影響

步驟如下。

  1. 考慮到先重現問題,首先應該全部使用新配置測試一次,確定問題是否還存在。
  2. 因為重點考慮問題是因為 sql_mode 引起的,所以第二次只將這個引數改為老配置,這樣就可以測試出其他配置組合時有問題,或者是沒有問題,從而得出結論是 sqlmode 的問題。
  3. 如果上面還沒有找到問題原因,那麼就是除了 sql_mode 之外的其他引數組合出了問題(如果沒有,則見鬼了)。此時,通過二分法測試,先測試 innodb_flush_log_at_trx_commit、innodb_open_files、sort_buffer_size 三個引數。
  4. 如果發現上一步有問題,則再進行二分;如果沒有發現問題,則對 sync_binlog、join_buffer_size、tmp_table_size 進行二分。
  5. 如果能走到這裡,那也是醉了。
  6. 再說吧。

按照步驟,一步步地開始做。

  1. 首先使用有問題的配置,測試一遍,發現是老樣子,還是有問題的(真是幸運,問題還存在)。
  2. 把除了 sql_mode 之外的所有引數改成新的,其他都用老配置,測試發現沒有問題。
  3. 做完了,也是沒有問題。
  4. 做完了,還是沒有問題。
  5. 我醉了。

此時當事人已經搞不清楚了,難道是某兩個的組合會導致出現這樣的問題?如果是這樣的話,那情況就太多了,天已經亮了,很累,放棄吧!

就在想放棄的時候,突然有一種新的思路。在有問題的基礎上,把所有經過測試沒有影響的可以動態修改的引數改成與 db1 相同的引數,這樣應該是最少量的可以影響到效能的引數組合了。此時,在 db1 與 db3 例項上分別執行 show variables,全量匯出變數,進行對比,發現有幾個引數的區別(左邊是老的 db1,右邊是新的 db3),如圖 16.4 所示。

此時,我們做了最後的掙扎,已經只剩下這 6 個了,看上去還是不會有什麼影響,有些已經試過了,再隨便試一次吧。二分查詢,從下面開始找了三個引數,下線、重啟、上線……發現問題竟然奇蹟般地存在。而此時只剩下了三個引數,其中一個引數是 sync_binlog,有問題的是 0,肯定不會影響啊。只能定位到剩下的兩個了,可以看到倒數第二個是一個 performance_schema 的引數,配置檔案中沒有設定,是預設的,可以忽略。於是,把問題定位到 open_files_limit 了。

此時,再做最後一次,只剩下 open_files_limit 的區別了。結果還是有問題,說明就是這個引數了。

回過頭來想了一下,其實當初看區別的時候,這兩個引數就在配置檔案中,只是看著 13 萬和 15 萬相差不大,就忽略了。好吧,問題已經解決,找到了原因,天亮了,回家吧。

已經查到了是 open_files_limit 的原因。那麼,究竟為什麼在一個引數相差這麼小的情況下會影響 10 倍的效能呢?查查原始碼!

通過 sysbench,建立 60000 個表,每個表 10000 行,在只讀模式下,發現設定為 130000 時,QPS 可以達到 20000,而設定為 150000 的時候,QPS 只有 4000 左右。問題重現了,就簡單多了。

此外,還有一個額外的發現。如果設定為 150000 之後,重啟資料庫,非常慢,大概需要 1 分鐘,而設定為 130000 之後,只需要 10 秒左右。查看了一下在很慢的過程中 mysqld 的執行緒情況。其中,在啟動的過程中,有一個執行緒長時間都基本處於同一個堆疊,使用 pstack mysqldpid 檢視,如圖 16.5 所示。

還有一個發現就是,當設定 open_files_limit 為相同的時候,performance_schema_max_file_instances 引數也相同了,並且這個引數沒有設定過。那麼,通過原始碼發現,這個引數竟然是通過 open_files_limit 值來設定的。如果 open_files_limit 值設定得比較大(這樣就可以忽略掉其他影響條件,比如 max_connection 等),performance_schema_max_file_instances 的值直接就是從 open_files_limit/0.65 得來的(原始碼對應函式 apply_load_factor)。這樣就知道了,130000/0.65 正好是 200000,150000/0.65 正好是 230770,與圖 16.4 所示相符合。

另外,通過測試發現,如果單獨設定 performance_schema_max_file_instances 為不相同的值,而將 open_files_limit 設定為相同,效能還是不一樣。從而可以確定與 open_files_limit 引數實際上沒有什麼關係,只是 performance_schema_max_file_instances 使用了預設值,它的值就來源於 open_files_limit/0.65 了,這樣間接影響了 performance_schema_max_file_instances 值,突然有種“隔山打牛”的感覺。

還有一個發現,如果將 performance_schema_max_file_instances 設定為 200000、210000、220000、230000、240000、300000 等,效能都是差不多的,唯獨設定為 230770 是有問題的。仔細研究之後發現,performance_schema_max_file_instances 最終影響的是 performance_schema 資料庫中的 file_instances 表,這個表中的資料是通過一個 HASH 表來快取的,而這個引數決定的是該 HASH 表的大小。

後面又做了一個非常無聊的測試,是 performance_schema_max_file_instances 值與 QPS 的對比,如圖 16.6 所示。

至此,一切都豁然開朗了。結合上面非常慢的堆疊,以及將 performance_schema_max_file_instances 設定為不同值的現象,可以確定,這個問題最終是 HASH 演算法的問題。當 HASH 桶大小為一個比較好看的數值時,這個演算法就非常快,而如果是一個比較零碎的值時,演算法就非常慢了,會導致響應時間是原來的 10 倍。

總結

  • 這個問題,確實很詭異,萬萬沒有想到是相差那麼小的一個變數的問題(以至於一開始就被忽略了)。
  • 這個問題,比較少見,只有表比較多的時候才會比較明顯(因為表比較少的時候,演算法相對比較穩定)。
  • 這個問題,查明瞭,其實就是一個演算法方面的 BUG。
  • 這個問題,簡單的規避方法是單獨設定 performance_schema_max_file_instances 值為 0,或者設定 performance_schema_max_file_instances 為一個比較好看的數值,又或者設定 open_files_limit 為 0.65 的整數倍,這樣都不會有問題。
  • 這個問題,雖然影響比較小,但我們對問題的探索精神不能沒有,要了然於胸。
  • 這個問題,到此為止。

文章來自《MySQL運維內參》,作者 | 周彥偉、王竹峰、強昌金等,網路釋出於:高效開發運維。