天天看點

MySQL實戰第三十二講-為什麼還有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 成功的例子:

MySQL實戰第三十二講-為什麼還有kill不掉的語句?

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

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

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

我在 第 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中的序列。

如下 圖 2 所示為kill query 無效的例子:

MySQL實戰第三十二講-為什麼還有kill不掉的語句?

可以看到:

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 之後的效果:

MySQL實戰第三十二講-為什麼還有kill不掉的語句?

這時候,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 所示為連接配接等待的狀态:

MySQL實戰第三十二講-為什麼還有kill不掉的語句?

而如果 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 資源限制執行得比較慢,就通過減少系統壓力讓它加速。

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

最後,我給你留下一個思考題吧。

如果你碰到一個被 killed 的事務一直處于復原狀态,你認為是應該直接把 MySQL 程序強行重新開機,還是應該讓它自己執行完成呢?為什麼呢?

問題解答:

因為重新開機之後該做的復原動作還是不能少的,是以從恢複速度的角度來說,應該讓它自己結束。

當然,如果這個語句可能會占用别的鎖,或者由于占用 IO 資源過多,進而影響到了别的語句執行的話,就需要先做主備切換,切到新主庫提供服務。

切換之後别的線程都斷開了連接配接,自動停止執行。接下來還是等它自己執行完成。這個操作屬于我們在文章中說到的,減少系統壓力,加速終止邏輯。