1. 程式人生 > >簡單SQL也很慢?資料庫端到端效能問題的解決思路探討

簡單SQL也很慢?資料庫端到端效能問題的解決思路探討

作者介紹

田冬雪,美團點評資料庫架構師,7年資料庫自動化運維經驗。目前負責美團點評基礎技術研究、資料庫高可用架構優化、資料庫運維自動化推進,美團點評工具平臺融合等。

作為綜合性多業務的“網際網路+生活服務”平臺,美團點評對資料庫的穩定執行有較高的要求,小概率的效能抖動(包括慢SQL)都會造成一定的可用性損失。本文將從過去幾年遇到的一些效能問題中,挑選了一個較為棘手的案例,探究端到端資料庫效能問題的解決思路,為DBA同學在解決類似問題時提供一種參考。

問題描述

在一段時間內不斷有開發同學反饋,線上應用程式獲取資料超時,通過CAT監控系統發現這些應用的SQL 99line都比較高,這在一定程度上影響了對應業務的QoS,比如達不到99.99%的業務可用性(超時被定義為不可用)。這些問題出現在很多業務場景中,是一個普遍性問題。通過CAT監控系統、SQL樣本、慢查詢系統等進一步瞭解,發現這類SQL有如下特徵:
  • 基本上都是以主鍵或唯一鍵為條件的簡單查詢,查詢後的結果集及掃描的行數都比較小;
  • 查詢的表的資料總量也很小,最小的表甚至只有幾千行;
  • 時間達到了幾百ms,甚至1s;
  • 資料庫的slow log裡沒有記錄這類SQL。

下圖為CAT相關監控資料的樣本,以xxx-service這個service為例:

99line的監控資料,有很多SQL的返回時間超過100ms以上。

監控資料

SQL的絕對數量在2016年9月6日當天為 :3788。

SQL

具體到某個SQL,甚至達到了929ms。

FB_Coach的表結構如下:

可看到最多641條記錄,還有聯合索引。

索引

概要分析

要想定位到原因,必須通過排除法找到該SQL到底慢在哪個階段,這樣才能縮小範圍。接下來我們來分析慢SQL的花費時間組成。從下圖可看出,時間主要由3部分組成:

  • App Server:發出SQL請求的時間,接收返回結果的時間
  • 網路:SQL請求包及查詢結果在網路上花費的時間
  • MySQL Server:發出SQL到查詢結果整個過程花費的時間

我們可以通過抓包工具獲取每個階段花費的時間,從而定位到底慢在哪個階段。

問題解決思路迭代

思路1:確認哪個過程花費的時間最多
方法:分別在APP Server與MySQL Server部署TcpDump抓包工具,得到資料包在4個監測點的“到達時間”。為了方便,把如下4個Wireshark分析結果(對TcpDump抓取日誌分析)按4個方位標註:
  • APP Server 發出SQL(左上)
  • MySQL Server 收到SQL(右上)
  • MySQL Server 將查詢發出(右下)
  • APP Server 收到查詢結果(左下)

從資料可以準確的看出時間主要花費在MySQL內部,具體時間為22.569285000-21.962634000=0.6066509999999994(秒),約為606ms。

抓包結果:慢在MySQL Server端。

MySQL

思路2:一條SQL進入MySQL Server到查詢結果輸出分哪些階段?

方法:將MySQL內部對SQL查詢的流程進行梳理,採用排除法定位問題。要把經典圖拿出來說事了,以下基礎知識主要來自於《高效能MySQL》,“拿來主義”一下。

MySQL

首先可以看到,MySQL主要有三個元件:連線/執行緒處理、MySQL Server層、儲存引擎層。
  • 最上層主要進行連線處理、授權認證、安全等;
  • 第二層包括查詢解析、分析、優化(這三個是解決問題最關心的)、快取管理、所有內建函式、儲存過程、觸發器、檢視,似乎扯得有點遠;
  • 第三層包含了主要的儲存引擎層,MySQL Server層(第二層)通過“儲存引擎API”向儲存引擎層儲存和提取資料,此層主要是資料儲存相關。

接下來通過一個客戶端請求查詢資料,看看MySQL主要做哪些工作吧。

每個客戶端(可能理解為App負責連線資料庫的元件,我們叫DAL)連線到MySQL伺服器程序後會擁有一個執行緒,這個連線的所有查詢都會在該執行緒中去執行,同時伺服器會快取執行緒,以減少建立或銷燬執行緒的開銷和頻繁的上下文切換。

當客戶連線到MySQL伺服器時,伺服器會分配一個執行緒,之後進行許可權認證,認證通過後,MySQL就開始解析該SQL查詢,並建立內部資料資料結構(解析樹),然後對其各種優化,最後呼叫儲存引擎API獲取或儲存需要的資料,最後將查詢結果返回給客戶端。

通過以上“背書”,我們大概瞭解了一個SQL請求的執行過程,那到底慢在哪個階段呢?

通過“慢SQL特點”的第4條知道,“資料庫的slow log裡沒有記錄這類SQL”,那慢SQL發生的階段就可以排除了。

MySQL slow log是記錄SQL執行過程花費的時間,記錄的時間從“SQL解析”到“儲存引擎”返回資料整個過程,所以可以排除該SQL是慢在第二層和第三層,那麼只能是把時間花費在第一層了?和執行緒相關?

結果:很可能慢在MySQL執行緒管理上。

思路3:是建立執行緒慢?thread cache不夠用,需要頻繁的建立執行緒?

方法:檢視當時資料庫的狀態值

資料庫

可以看到,當時空閒的thread很多,監控圖也沒有抖動,所以並沒有頻繁地建立執行緒。慢SQL產生的時間點,空閒的thread很多,並沒有進行大量的執行緒建立。

那問題到底出現在和執行緒相關的哪個環節呢? 先把所有和thread相關的引數列出來。

thread_cache_size
thread_concurrency
thread_handling
thread_pool_high_prio_mode
thread_pool_high_prio_tickets
thread_pool_idle_timeout
thread_pool_max_threads
thread_pool_oversubscribe
thread_pool_size
thread_pool_stall_limit
thread_stack
thread_statistics

一眼看過去,大部分是和Thread-Pool相關。同時意識到這些問題是隨著升級到MySQL 5.6產生的,5.6引入了Thread-Pool功能。

結果:看來MySQL5.6的Thread-Pool有很大嫌疑了。

思路4:關閉MySQL 5.6的Thread-Pool,確認一下問題

方法:調整MySQL引數 thread_handling = pool-of-threads—- → thread_handling = One-Connection-Per-Thread。

結論:關閉Thread-Pool功能後,減少78%的慢SQL,側面證明是Thread-Pool的問題。

以下是具體的證據,以xxx-service這個service為例:開啟Thread-Pool功能(2016年9月6日當天資料)。

99line佔比:有好多超過100ms的SQL。

慢SQL數量:3788

關閉Thread-Pool功能後(2016年9月13日當天資料)。

99line佔比:已經看不到超過100ms的sql了,都在10ms以內。

慢SQL數量:818

那麼關閉Thread-Pool ?答案很顯然,不能!Thread-Pool是MySQL5.6重要的功能,能夠保證MySQL資料庫高併發下的效能穩定。

思路5:調優Thread-Pool相關引數

方法:深入瞭解Thread-Pool的工作原理,查詢可能產生慢SQL的引數。

結果:找到了相關引數(thread_pool_stall_limit),並且效果明顯,慢SQL數量從最初的3788減少到63,幾乎全部消滅掉。

以xxx-service這個service為例,調整後的效果,2016年9月20日當天的資料:

99line佔比:

慢SQL數量:63

ok,效果有了,總結一下

問題分析

1、基本原理沒有引入Thread-Pool前,MySQL使用的是one thread per connection,一旦connection增加到一定程度,MySQL的效能將急劇下降甚至被壓跨。引入Thread-Pool後將會解決上述問題,同時會減少MySQL內部的執行緒數(節省記憶體)及頻繁建立執行緒的開銷(節省CPU)。2、Thread-Pool是如何工作的?在MySQL內部有一個專用的thread用來監聽資料庫連線請求,當一個新的請求過來,如果採用以前的模型(one-thread-per-connection),main listener(這是主執行緒中的listener,為了避免與thread group 中的listener混淆,我們稱之為“Main listener”)將從thread cache中取出1個thread或建立1個新的thead立即處理該連線請求,由該thread完成該連線的整個生命週期;而如果採用Thread-Pool模型,這個連線請求將會被隨機放到一個thread group(thread pool由多個thread group 組成)的佇列中,之後該thread group中worker thread從佇列中取出並建立連線,一旦連線建立,該連線對應的socket控制代碼將與該thread group中的listener關聯起來,之後該連線將在該thread group中完成它的生命週期。接下來我們來說說Thread Group 。Thread Group是Thread-Pool的核心元件,所有的操作都是發生在thread group。Thread-Pool由多個(數量由引數thread_pool_size來決定,預設等於cpu個數)thrad group組成。一個連線請求被隨機地繫結到一個thread group,每個thread group獨立工作,並且佔用一個核的CPU。所以thread group都會最大限度地保持一個thread處於ACTIVE狀態,並且最好只有一個,因為太多就有可能壓跨資料庫。Thread Group中的thread一般有4個狀態:
  • TP_STATE_LISTENER
  • TP_STATE_IDLE
  • TP_STATE_ACTIVE
  • TP_STATE_WAITING

當一個執行緒作為listener執行時就處於“TP_STATE_LISTENER”,它通過epoll的方式監聽聯接到該Thread Group的所有連線,當一個socket就緒後,listener將決定是否喚醒一個thread或自己處理該socket。此時如果Thread Group的佇列為空,它將自己處理該socket並將狀態更改為“ACTIVE”,之後該thread 在MySQL Server內部處理“工作”,當該執行緒遇到鎖或非同步IO(比如將資料頁讀入到buffer pool)這些wait時,該thread將通過回撥函式的方式告訴thread pool,讓其把自己標記為“WAITING”狀態。

此時,假設佇列中有了新的socket準備就緒,是立即建立新的執行緒還是等待剛才的執行緒執行結束呢?

由於Thread-Pool最初設計的目標是保持一定數量的執行緒處於“ACTIVE”狀態,具體的實現方式就是控制thread group的數量和thread group內部處於”ACTIVE”狀態的thread的數量。控制thread group內部的ACTIVE狀態的數量,方法就是最大限度地保證處於ACTIVE狀態的執行緒個數是1。很顯然,當前thread group中有一個處於WAITING狀態的thread了,如果再啟用一個新的執行緒並且處於ACTIVE狀態,剛才的執行緒由WAITING變為ACTIVE狀態時,此時將會有2個“ACTIVE”狀態的執行緒,和最初的目標似乎相背,但顯然也不能讓後續就緒的socket一直等待下去,那應該怎麼處理?

那麼此時需要一個權衡了,提供了這樣的一個方法:對正在ACTIVE或WAITING狀態的執行緒啟用一個計數器,超過計數器後將該thread標記為stalled,然後thread group建立新的thread或喚醒sleep的thread處理新的sokcet,這樣將是一個很好的權衡。超時時間該引數thread_pool_stall_limit來決定,預設是500ms。

如果一個執行緒無事可做,它將保持空閒狀態(TP_STATE_WAITING)一定時間(thread_pool_idle_timeout引數決定,預設是60秒)後“自殺”。

3、和我們遇到的具體問題相關的點

假設上文提到的由“ACTIVE”轉化為“WAITING”狀態的執行緒(標記為“執行緒A”)所執行的“SQL”可能是一個標準的慢SQL(命名為SQLA,執行時間較長),那麼後續有連線請求分配到了同一個thread group,那麼新連線的SQL(命名SQLB)需要等待執行緒A結束;如果SQLA執行時間超過500ms,該thread group建立新的worker執行緒來處理SQLB。

不管哪種情況,SQLB都會線上程等待上花費很多時間,此時SQLB就是CAT監控系統上看到的慢SQL。又因為SQLA不一定都是慢SQL,所以SQLB也不是每次線上程等待上花費較多的時間,這就吻合我們看到的現象“一定比例的慢SQL”。

 解決方法

找到問題了,那麼解決辦法就簡單了。調整thread_pool_stall_limit=10,這樣就強迫被SQLA更快被標記為stalled,然後建立新的執行緒來處理SQLB。

帶來的價值

  • 以xxx-service為例,減少了98.3%的慢SQL,效果很明顯;
  • 該問題的解決讓百個產品線從中受益,業務可用性超過了99.99%。

總結

首先我們分析了慢SQL的特點及該SQL花費的時間組成,通過“時間花費在哪”這一通用方法,不斷把問題範圍縮小,最終通過排除法將問題鎖定在MySQL內部執行緒。對於MySQL內部執行緒,我們通過對引數“全量掃描”,發現了與MySQL 5.6新開啟的引數有關,粗略確定了Thread-Pool是導致慢SQL問題的。之後通過關閉Thread-Pool進一步確認是開啟該功能引起的。之後我們不斷調整引數和閱讀大量相關的資料,最終將問題解決。通過以上問題的解決,我們可以學到一些端到端的效能問題解決思路:定位問題
  • 劃分問題的邊界

每個問題總有它的邊界。當我們無法一眼看出來問題的邊界在哪裡時,就需要不斷的通過排除法縮小邊界,在特定的邊界內就用特定的專業知識來定位問題。

  •  蒐集關鍵資料得出靠譜結論

比如生產環境中會有各種資料,包含監控資料、臨時部署工具獲取的資料,充分利用這些資料支撐我們的結論。

  • 對問題產生的時間或影響範圍進行上下文聯想

很多問題是隨著一些改變產生的,就像軟體的生命週期一樣,受到各種環境的變化影響。通過問題產生的上下去尋找問題的原因,可以發現大部分問題的產生原因。

解決問題

  • 不斷嘗試

有很多人認為,知道問題的原因了,解決問題是比較容易的。其實我認為這個是反的。因為只有清楚知道問題解決了,才能證明問題的原因是對的。在找到問題的原因之前,其實我們已經通過不斷的調整和測試把問題解決了。所以解決問題很關鍵,貌似是廢話。

  • 合理的理論推斷問題產生的原因

問題解決了,原因也找到了,最後一步還要“自圓其說”,這就需要深究技術原理,找到切入點,復現問題了。

解決問題的方法有千萬種,這裡列舉了其中一種,希望能夠幫助到大家。

參考文獻:

  • https://my.oschina.net/andylucc/blog/820624
  • https://yq.aliyun.com/articles/41078
  • http://blog.chinaunix.net/uid-28364803-id-3427833.html
  • http://blog.chinaunix.net/uid-28364803-id-3431242.html
  • https://www.percona.com/doc/percona-server/5.6/performance/threadpool.html
  • https://mariadb.com/kb/en/mariadb/thread-pool-in-mariadb/
  • https://www.safaribooksonline.com/library/view/high-performance-mysql/9781449332471/ch01.html#mysqlas_logical_architecture

文章來自微信公眾號:DBAplul社群