MySQL45講讀書筆記 32講爲什麼還有kill不掉的語句

一 序

  本文屬於極客時間MySQL45講讀書筆記系列。開發同學屬於瞭解系列,更偏向DBA。因爲平時你沒有admin權限去kill。

在MySQL中有兩個kill命令:一個是kill query +線程id,表示終止這個線程中正在執行的語句;一個是kill connection +線程id,這裏connection可缺省,表示斷開這個線程的連接,當然如果這個線程有語句正在執行,也是要先停止正在執行的語句的。

不知道你在使用MySQL的時候,有沒有遇到過這樣的現象:使用了kill命令,卻沒能斷開這個連接。再執行show processlist命令,看到這條語句的Command列顯示的是Killed。

你一定會奇怪,顯示爲Killed是什麼意思,不是應該直接在show processlist的結果裏看不到這個線程了嗎?

今天,我們就來討論一下這個問題。

其實大多數情況下,kill query/connection命令是有效的。比如,執行一個查詢的過程中,發現執行時間太久,要放棄繼續查詢,這時我們就可以用kill query命令,終止這條查詢語句。

還有一種情況是,語句處於鎖等待的時候,直接使用kill命令也是有效的。我們一起來看下這個例子:

圖1 kill query 成功的例子

可以看到,session C 執行kill query以後,session B幾乎同時就提示了語句被中斷。這,就是我們預期的結果。

收到kill以後,線程做什麼?

但是,這裏你要停下來想一下:session B是直接終止掉線程,什麼都不管就直接退出嗎?顯然,這是不行的。

當對一個表做增刪改查操作時,會在表上加MDL讀鎖。所以,session B雖然處於blocked狀態,但還是拿着一個MDL讀鎖的。如果線程被kill的時候,就直接終止,那之後這個MDL讀鎖就沒機會被釋放了。

這樣看來,kill並不是馬上停止的意思,而是告訴執行線程說,這條語句已經不需要繼續執行了,可以開始“執行停止的邏輯了”。

其實,這跟Linux的kill命令類似,kill -N pid並不是讓進程直接停止,而是給進程發一個信號,然後進程處理這個信號,進入終止邏輯。只是對於MySQL的kill命令來說,不需要傳信號量參數,就只有“停止”這個命令。

實現上,當用戶執行kill query thread_id_B時,MySQL裏處理kill命令的線程做了兩件事:

  1. 把session B的運行狀態改成THD::KILL_QUERY(將變量killed賦值爲THD::KILL_QUERY);

  2. 給session B的執行線程發一個信號。

爲什麼要發信號呢?

因爲像圖1的我們例子裏面,session B處於鎖等待狀態,如果只是把session B的線程狀態設置THD::KILL_QUERY,線程B並不知道這個狀態變化,還是會繼續等待。發一個信號的目的,就是讓session B退出等待,來處理這個THD::KILL_QUERY狀態。

上面的分析中,隱含了這麼三層意思:

  1. 一個語句執行過程中有多處“埋點”,在這些“埋點”的地方判斷線程狀態,如果發現線程狀態是THD::KILL_QUERY,纔開始進入語句終止邏輯;

  2. 如果處於等待狀態,必須是一個可以被喚醒的等待,否則根本不會執行到“埋點”處;

  3. 語句從開始進入終止邏輯,到終止邏輯完全完成,是有一個過程的。

到這裏你就知道了,原來不是“說停就停的”。

接下來,我們再看一個kill不掉的例子

 

首先,執行set global innodb_thread_concurrency=2,將InnoDB的併發線程上限數設置爲2;然後,執行下面的序列:

圖2 kill query 無效的例子

可以看到:

  1. sesssion C執行的時候被堵住了;

  2. 但是session D執行的kill query C命令卻沒什麼效果,

  3. 直到session E執行了kill connection命令,才斷開了session C的連接,提示“Lost connection to MySQL server during query”,

  4. 但是這時候,如果在session E中執行show processlist,你就能看到下面這個圖。

圖3 kill connection之後的效果

這時候,id=12這個線程的Commnad列顯示的是Killed。也就是說,客戶端雖然斷開了連接,但實際上服務端上這條語句還在執行過程中。

爲什麼在執行kill query命令時,這條語句不像第一個例子的update語句一樣退出呢?

在實現上,等行鎖時,使用的是pthread_cond_timedwait函數,這個等待狀態可以被喚醒。但是,在這個例子裏,12號線程的等待邏輯是這樣的:每10毫秒判斷一下是否可以進入InnoDB執行,如果不行,就調用nanosleep函數進入sleep狀態。

也就是說,雖然12號線程的狀態已經被設置成了KILL_QUERY,但是在這個等待進入InnoDB的循環過程中,並沒有去判斷線程的狀態,因此根本不會進入終止邏輯階段。

而當session E執行kill connection 命令時,是這麼做的,

  1. 把12號線程狀態設置爲KILL_CONNECTION;

  2. 關掉12號線程的網絡連接。因爲有這個操作,所以你會看到,這時候session C收到了斷開連接的提示。

那爲什麼執行show processlist的時候,會看到Command列顯示爲killed呢?其實,這就是因爲在執行show processlist的時候,有一個特別的邏輯:

如果一個線程的狀態是KILL_CONNECTION,就把Command列顯示成Killed。

所以其實,即使是客戶端退出了,這個線程的狀態仍然是在等待中。那這個線程什麼時候會退出呢?

答案是,只有等到滿足進入InnoDB的條件後,session C的查詢語句繼續執行,然後纔有可能判斷到線程狀態已經變成了KILL_QUERY或者KILL_CONNECTION,再進入終止邏輯階段。

到這裏,我們來小結一下。

這個例子是kill無效的第一類情況,即:線程沒有執行到判斷線程狀態的邏輯。跟這種情況相同的,還有由於IO壓力過大,讀寫IO的函數一直無法返回,導致不能及時判斷線程的狀態。

另一類情況是,終止邏輯耗時較長。這時候,從show processlist結果上看也是Command=Killed,需要等到終止邏輯完成,語句纔算真正完成。這類情況,比較常見的場景有以下幾種:

  1. 超大事務執行期間被kill。這時候,回滾操作需要對事務執行期間生成的所有新數據版本做回收操作,耗時很長。
  2. 大查詢回滾。如果查詢過程中生成了比較大的臨時文件,加上此時文件系統壓力大,刪除臨時文件可能需要等待IO資源,導致耗時較長。
  3. DDL命令執行到最後階段,如果被kill,需要刪除中間過程的臨時文件,也可能受IO資源影響耗時較久。

之前有人問過我,如果直接在客戶端通過Ctrl+C命令,是不是就可以直接終止線程呢?

答案是,不可以。

這裏有一個誤解,其實在客戶端的操作只能操作到客戶端的線程,客戶端和服務端只能通過網絡交互,是不可能直接操作服務端線程的。

而由於MySQL是停等協議,所以這個線程執行的語句還沒有返回的時候,再往這個連接裏面繼續發命令也是沒有用的。實際上,執行Ctrl+C的時候,是MySQL客戶端另外啓動一個連接,然後發送一個kill query 命令。

所以,你可別以爲在客戶端執行完Ctrl+C就萬事大吉了。因爲,要kill掉一個線程,還涉及到後端的很多操作。

另外兩個關於客戶端的誤解

在實際使用中,我也經常會碰到一些同學對客戶端的使用有誤解。接下來,我們就來看看兩個最常見的誤解。

第一個誤解是:如果庫裏面的表特別多,連接就會很慢。

有些線上的庫,會包含很多表(我見過最多的一個庫裏有6萬個表)。這時候,你就會發現,每次用客戶端連接都會卡在下面這個界面上。

圖4 連接等待

而如果db1這個庫裏表很少的話,連接起來就會很快,可以很快進入輸入命令的狀態。因此,有同學會認爲是表的數目影響了連接性能。

每個客戶端在和服務端建立連接的時候,需要做的事情就是TCP握手、用戶校驗、獲取權限。但這幾個操作,顯然跟庫裏面表的個數無關。

但實際上,正如圖中的文字提示所說的,當使用默認參數連接的時候,MySQL客戶端會提供一個本地庫名和表名補全的功能。爲了實現這個功能,客戶端在連接成功後,需要多做一些操作:

  1. 執行show databases;

  2. 切到db1庫,執行show tables;

  3. 把這兩個命令的結果用於構建一個本地的哈希表。

在這些操作中,最花時間的就是第三步在本地構建哈希表的操作。所以,當一個庫中的表個數非常多的時候,這一步就會花比較長的時間。

也就是說,我們感知到的連接過程慢,其實並不是連接慢,也不是服務端慢,而是客戶端慢。

圖中的提示也說了,如果在連接命令中加上-A,就可以關掉這個自動補全的功能,然後客戶端就可以快速返回了。

這裏自動補全的效果就是,你在輸入庫名或者表名的時候,輸入前綴,可以使用Tab鍵自動補全表名或者顯示提示。

實際使用中,如果你自動補全功能用得並不多,我建議你每次使用的時候都默認加-A。

其實提示裏面沒有說,除了加-A以外,加–quick(或者簡寫爲-q)參數,也可以跳過這個階段。但是,這個–quick是一個更容易引起誤會的參數,也是關於客戶端常見的一個誤解。

你看到這個參數,是不是覺得這應該是一個讓服務端加速的參數?但實際上恰恰相反,設置了這個參數可能會降低服務端的性能。爲什麼這麼說呢?

MySQL客戶端發送請求後,接收服務端返回結果的方式有兩種:

  1. 一種是本地緩存,也就是在本地開一片內存,先把結果存起來。如果你用API開發,對應的就是mysql_store_result 方法。

  2. 另一種是不緩存,讀一個處理一個。如果你用API開發,對應的就是mysql_use_result方法。

MySQL客戶端默認採用第一種方式,而如果加上–quick參數,就會使用第二種不緩存的方式。

採用不緩存的方式時,如果本地處理得慢,就會導致服務端發送結果被阻塞,因此會讓服務端變慢。關於服務端的具體行爲,我會在下一篇文章再和你展開說明。

那你會說,既然這樣,爲什麼要給這個參數取名叫作quick呢?這是因爲使用這個參數可以達到以下三點效果:

  • 第一點,就是前面提到的,跳過表名自動補全功能。
  • 第二點,mysql_store_result需要申請本地內存來緩存查詢結果,如果查詢結果太大,會耗費較多的本地內存,可能會影響客戶端本地機器的性能;
  • 第三點,是不會把執行命令記錄到本地的命令歷史文件。

所以你看到了,–quick參數的意思,是讓客戶端變得更快。

小結

在今天這篇文章中,我首先和你介紹了MySQL中,有些語句和連接“kill不掉”的情況。

這些“kill不掉”的情況,其實是因爲發送kill命令的客戶端,並沒有強行停止目標線程的執行,而只是設置了個狀態,並喚醒對應的線程。而被kill的線程,需要執行到判斷狀態的“埋點”,纔會開始進入終止邏輯階段。並且,終止邏輯本身也是需要耗費時間的。

所以,如果你發現一個線程處於Killed狀態,你可以做的事情就是,通過影響系統環境,讓這個Killed狀態儘快結束。

比如,如果是第一個例子裏InnoDB併發度的問題,你就可以臨時調大innodb_thread_concurrency的值,或者停掉別的線程,讓出位子給這個線程執行。

而如果是回滾邏輯由於受到IO資源限制執行得比較慢,就通過減少系統壓力讓它加速。

做完這些操作後,其實你已經沒有辦法再對它做什麼了,只能等待流程自己完成。

************

通常都是dba查看了慢sql,採取執行kill的,開發同學需要了解下這個過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章