線上故障 | 一條 SQL引起的血案

歡迎關注公衆號【sharedCode】致力於主流中間件的源碼分析, 能夠直接與我聯繫java

事故現場

202-11-19 系統接收到大量的超時告警, 同時業務羣裏面也有不少客戶反饋服務不可用。mysql

開始排查

首先上grafana上面查看總體的服務狀態,算法

從圖中能夠看出一點問題來,CPU幾乎沒有波動, tomcat線程數急劇上升, 系統的ops急劇降低。 這種是比較典型的資源阻塞類問題,爲了印證這個想法,咱們再看下當時的系統的GC狀況sql

從上面的GC狀況下,咱們能夠看出來,GC仍是比較平穩的,總體的停頓市場也很少,平均在100ms如下,雖然不算好,可是確定不會形成系統有如此大的停頓緩存

服務器出現故障排查方法:tomcat

服務器出現故障,先看CPU,若是CPU持續高漲,那麼確定是服務內部出現了問題,這個時候能夠按照網上的常規解決方法,top -HP pid 查看​耗CPU比較嚴重的線程,而後導出對應的線程棧信息,就 能夠根據實際的業務去分析了, 這種屬於比較直觀的服務器

比較隱晦的資源阻塞問題,此類問題分爲以下兩種:函數

  1. 比較好排查的,即便接口慢,好比接口調用耗費時間久的外部接口,有大量慢SQL,這些都會間接的致使總體吞吐量降低,最終致使tomcat線程池線程池耗盡
  2. 第二種就更加隱晦了,CPU,帶寬,流量,慢SQL,內存各方面都很正常,可是tomcat線程池直線上升,最終服務器資源耗盡。 這種狀況我以前有專門寫過一片文章。《tomcat線程池排查》 這種狀況能夠考慮使用jstack命令,導出堆棧信息,這裏推薦一款工具 gceasy , 能夠清晰的分析出線程的狀態分佈,能夠很好的知道線程都堵在什麼地方了。

經過上面的已知條件和咱們過往的經驗,基本上能夠斷定是有一些接口阻塞致使總體系統處理能力急劇降低。 首先想到的就是慢SQL。工具

登錄到阿里雲的RDS控制檯上,查看RDS的運行情況oop

果不其然,在那個時間段,CPU已經到了100%了,基本上能夠肯定是慢查詢的問題, 在慢查詢的控制檯上,立馬能夠看到當前系統阻塞的SQL。觸目驚心,真的不知道是哪一個兔崽子寫的SQL。

罪魁禍首就是這條SQL

SELECT
	o.* 
FROM
	`jm_order` o
	LEFT JOIN jm_order_unregistered_driver d ON o.number = d.orderNumber 
WHERE
	o.valid = 1 
	AND o.state = 1 
	AND o.driver_uid = 0 
	AND o.agents_uid = 0 
	AND d.driverPhone IS NULL 
	AND o.is_push_regular_car = 0 
	AND o.is_lock = 2 
	AND (
		o.from_date > '2020-11-18' 
		OR (
			o.from_date = '2020-11-18' 
			AND o.from_day >= 16 
		)) 
	AND o.type IN (
	11,
	12)

經過explain關鍵字查詢執行計劃

問題一目瞭然了,看我紅線框起來的地方,這個就是問題所在,咱們能夠分析下這個SQL,這個SQL

裏面有兩張表,使用了left join , 其餘的卻是沒什麼問題,看索引走向以及掃描函數,其實看上去都沒啥問題,不該該耗時這麼久。

可是看紅色框起來的部分Using where; Using join buffer (Block Nested Loop) , 這句話什麼意思?

下面給你們講一下mysql在錶鏈接的時候使用的算法,同時也讓你們理解一下爲何有小表驅動大表的說法

mysql表關聯算法

Simple Nested Loop算法

這個算法,屬於簡單嵌套循環, 說白了就是外層表的結果做爲第一層循環,內層表做爲第二層循環,而後就這樣硬幹,

for (Table t:table) {
  for(Join x:joinTable){
    if(t==x){
      //xxxx ,說明匹配到了數據
      for(){
        // 若是有三種表關聯的話。
      }
    }
  }
}

上面這種暴力關聯的方法,可想而知效率那是差的一逼,基本上mysql官方也不會使用這種方式的。

執行順序:

  1. 先遍歷table1
  2. 遍歷table獲得的結果,逐條遍歷table2
  3. 遍歷完table2以後呢,繼續逐條遍歷table3, 返回最終的結果

執行次數基本上是: table1 * table2 * table3

Block Nested-Loop

這種算法,就是本文中生產環境實際遇到的,mysql默認在沒有創建索引上面使用的算法, 這種作法和簡單嵌套循環有一點不一樣,就是加了 緩存塊 , 減小了循環次數 , 將驅動表的數據緩存到 join Buffer裏面去,而後拿Join Buffer 裏面的數據和內層關聯表進行匹配,

for (Table t:table) {
  // store t in Join buffer ,
  // 當緩衝池滿了,執行匹配
  if(Join Buffer is full){
    for(Join x:joinTable){
    if(t in Buffer){
      //xxxx ,說明匹配到了數據
      for(){
        // 若是有三種表關聯的話。
      }
    }
    clear join buffer // 清空緩存池
  }
  }
  
}

從這裏能夠看到,使用了緩存池的話,減小了不少次數,好比:驅動表100條數據,被驅動表50條數據,那麼若是沒有Join Buffer的話,讀表次數:100 * 50, 加了Join buffer以後,若是Join buffer的大小能夠存儲50條數據,那麼讀表次數就是: 100/50 * 50 , 讀表次數減小了一個數量級的。

須要注意的是,只有在 連表鍵上沒有索引的時候會採用這種方式 , 也就是本文出現的狀況。

Index Nested-loop

索引嵌套循環,簡稱 INL, 說白了就是 連表鍵上有索引,就直接走索引去作嵌套查詢 , 下面我畫一張圖來解釋

驅動表獲得結果以後,是直接去索引樹上找對應的被驅動表的記錄,若是可使用覆蓋索引的話,那麼就不用再作回表了,這種狀況下,效率是至關高的。

得出以上結果,立馬給orderNumber 加上索引,走索引嵌套算法就能夠了, 系統立刻就恢復正常了。

看完上面的文字,這下你們明白了爲啥有小表驅動大表的說法嗎?

歡迎關注公衆號【sharedCode】致力於主流中間件的源碼分析, 能夠直接與我聯繫

相關文章
相關標籤/搜索