在 MySQL 中有兩個 kill 命令:一個是 kill query + 線程 id,表示終止這個線程中正在執行的語句;一個是 kill connection + 線程 id,這裏 connection 可缺省,表示斷開這個
線程的鏈接,固然若是這個線程有語句正在執行,也是要先中止正在執行的語句的。mysql
不知道你在使用 MySQL 的時候,有沒有遇到過這樣的現象:使用了 kill 命令,卻沒能斷開這個鏈接。再執行 show processlist 命令,看到這條語句的 Command 列顯示的是Killed。git
你必定會奇怪,顯示爲 Killed 是什麼意思,不是應該直接在 show processlist 的結果裏看不到這個線程了嗎?sql
今天,咱們就來討論一下這個問題。數據庫
其實大多數狀況下,kill query/connection 命令是有效的。好比,執行一個查詢的過程當中,發現執行時間過久,要放棄繼續查詢,這時咱們就能夠用 kill query 命令,終止這條
查詢語句。後端
還有一種狀況是,語句處於鎖等待的時候,直接使用 kill 命令也是有效的。咱們一塊兒來看下這個例子:瀏覽器
圖 1 kill query 成功的例子緩存
能夠看到,session C 執行 kill query 之後,session B 幾乎同時就提示了語句被中斷。這,就是咱們預期的結果。安全
可是,這裏你要停下來想一下:session B 是直接終止掉線程,什麼都無論就直接退出嗎?顯然,這是不行的。bash
我在第 6 篇文章中講過,當對一個表作增刪改查操做時,會在表上加 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 不掉的例子,也就是咱們在前面第 29 篇文章中提到的innodb_thread_concurrency 不夠用的例子。
首先,執行 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。也就是說,客戶端雖然斷開了鏈接,但實際上服務端上這條語句還在執行過程當中。
在實現上,等行鎖時,使用的是 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,須要等到終止邏輯完成,語句纔算真正完成。
這類狀況,比較常見的場景有如下幾種:
以前有人問過我,若是直接在客戶端經過 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 是一個更容易引發誤會的參數,也是關於客戶端常見的一個誤解。
三、–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 資源限制執行得比較慢,就經過減小系統壓力讓它加速。作完這些操做後,其實你已經沒有辦法再對它作什麼了,只能等待流程本身完成。
最後,我給你留下一個思考題吧。
若是你碰到一個被 killed 的事務一直處於回滾狀態,你認爲是應該直接把 MySQL 進程強行重啓,仍是應該讓它本身執行完成呢?爲何呢?
你能夠把你的結論和分析寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上一篇文章末尾,給你留下的問題是,但願你分享一下誤刪數據的處理經驗。
@蒼茫 同窗提到了一個例子,我以爲值得跟你們分享一下。運維的同窗直接拷貝文本去執行,SQL 語句截斷,致使數據庫執行出錯。
從瀏覽器拷貝文本執行,是一個很是不規範的操做。除了這個例子裏面說的 SQL 語句截斷問題,還可能存在亂碼問題。
通常這種操做,若是腳本的開發和執行不是同一我的,須要開發同窗把腳本放到 git 上,而後把 git 地址,以及文件的 md5 發給運維同窗。
這樣就要求運維同窗在執行命令以前,確認要執行的文件的 md5,跟以前開發同窗提供的md5 相同才能繼續執行。
另外,我要特別點贊一下 @蒼茫 同窗復現問題的思路和追查問題的態度。
@linhui0705 同窗提到的「四個腳本」的方法,我很是推崇。這四個腳本分別是:備份腳本、執行腳本、驗證腳本和回滾腳本。若是可以堅持作到,即便出現問題,也是能夠很快
恢復的,必定能下降出現故障的機率。
不過,這個方案最大的敵人是這樣的思想:這是個小操做,不須要這麼嚴格。
@Knight²º¹⁸ 給了一個保護文件的方法,我以前沒有用過這種方法,不過這確實是一個不錯的思路。
爲了數據安全和服務穩定,多作點預防方案的設計討論,總好過故障處理和過後覆盤。方案設計討論會和故障覆盤會,這兩種會議的會議室氣氛徹底不同。經歷過的同窗必定懂的。