您好,登錄后才能下訂單哦!
這篇文章主要介紹“MySQL中 kill會話的實現原理”,在日常操作中,相信很多人在MySQL中 kill會話的實現原理問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”MySQL中 kill會話的實現原理”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
先要簡單的梳理一下語句的執行的生命周期:
打個比方我們以如下的執行計劃為列子:
mysql> desc select * from t1 where name='gaopeng'; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 14 | 10.00 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (1.67 sec)
某個客戶端通過mysql客戶端啟動一個進程,通過socket IP:PORT的方式唯一確認一個mysqld服務器進程。
服務器進程mysqld準備好一個線程和這個mysql客戶端進行網絡通信。
mysql客戶端發送命令通過mysql net協議到達mysqld服務器端。
mysqld服務端線程解包,獲取mysql客戶端發送過來的命令。
mysqld服務端線程通過權限認證,語法語義解析,然后經過物理邏輯優化生成一個執行計劃。
loop:
mysqld服務端線程通過這個執行計劃執行語句,首先innodb層會掃描出第一條數據,返回給mysql層進行過濾也就是是否符合條件name='gaopeng';。
如果符合則返回給 mysql客戶端,如果不符合則繼續loop。
直到loop 結束整個數據返回完成。
這里涉及到了一個mysql客戶端進程和一個mysqld服務端線程,他們通過socket進行通信。如果我們要要kill某個會話我們顯然一般是新開起來一個mysql客戶端進程連接到mysqld服務端顯然這個時候又需要開啟一個服務端線程與其對接來響應你的kill命令那么這個時候圖如下:
image.png
如圖我們需要研究的就是線程2到底如何作用于線程1,實際上線程之間共享內存很簡單這是線程的特性決定的,在MySQL中就共享了這樣一個變量THD::killed,不僅線程1可以訪問并且線程2也可以訪問 。實際上這種情況就是依賴在代碼的某些位置做了THD::killed的檢查而實現。先大概先描述一下這種情況kill 會話的過程
線程2將THD::killed 設置
線程1在innodb層做掃描行的時候每行掃描完成后都會去檢查自己的線程是否設置為了KILL_CONNECTION
如果設置為KILL_CONNECTION,那么做相應的終止過程
上面已經描述了一個select語句的kill的流程,但是并非都是這種情況,我稍微總結了一下可能的情況:
正在執行命令,如上的select的情況(非Innodb行鎖等待情況)。
正在執行命令,如DML等待(Innodb行鎖等待情況),需要Innodb層喚醒,代碼則繼續。
正在執行命令,MySQL層進行等待比如sleep命令,需要MySQL層喚醒,代碼則繼續。
空閑狀態正在等待命令的到來。
注意上面的情況都是待殺線程處于的情況,而發起命令的線程只有一種方式,就是調用kill_one_thread函數。下面我將詳細描述一下。對于喚醒操作參考附錄的內容,我這里就默認大家都知道了。
下面是棧幀:
#0 THD::awake (this=0x7ffe7800e870, state_to_set=THD::KILL_CONNECTION) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_class.cc:2206#1 0x00000000015d5430 in kill_one_thread (thd=0x7ffe7c000b70, id=18, only_kill_query=false) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_parse.cc:6859#2 0x00000000015d5548 in sql_kill (thd=0x7ffe7c000b70, id=18, only_kill_query=false) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_parse.cc:6887
kill_one_thread
這是一個主要的函數,他會根據待殺死的my_thread_id也就是我們kill后面跟的值,獲取這個會話的THD結構體然后調用THD::awake函數如下:
tmp= Global_THD_manager::get_instance()->find_thd(&find_thd_with_id);//獲得待殺死的會話的THD結構體tmp->awake(only_kill_query ? THD::KILL_QUERY : THD::KILL_CONNECTION);//調用THD::awake命令我們這里是 THD::KILL_CONNECTION
THD::awake
這是一個主要的函數,這個函數會做將待殺死的會話的THD::killed標記為THD::KILL_CONNECTION,然后關閉socket連接,也就是這里客戶端進程會收到一個類似如下的錯誤:
ERROR 2013 (HY000): Lost connection to MySQL server during query
然后會終止等待進入innodb連接,然后還會做喚醒操作,關于為什么要做喚醒操作我們后面再說如下:
killed= state_to_set; \\這里設置THD::killed 狀態為 KILL_CONNECTIONvio_cancel(active_vio, SHUT_RDWR); \\關閉socket連接,關閉socket連接后則客戶端連接關閉 /* Interrupt target waiting inside a storage engine. */ if (state_to_set != THD::NOT_KILLED) ha_kill_connection(this); \\lock_trx_handle_waitmysql_mutex_lock(current_mutex); mysql_cond_broadcast(current_cond); \\做喚醒操作 mysql_mutex_unlock(current_mutex);
這種情況就是通過在代碼合適的位置檢查返回值完成了,比如下面棧幀:
#0 convert_error_code_to_mysql (error=DB_INTERRUPTED, flags=33, thd=0x7ffe74012f30) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:2064#1 0x00000000019d651e in ha_innobase::general_fetch (this=0x7ffe7493c960, buf=0x7ffe7493cea0 "\377", direction=1, match_mode=0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9907#2 0x00000000019d658b in ha_innobase::index_next (this=0x7ffe7493c960, buf=0x7ffe7493cea0 "\377") at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9929
我們可以在函數ha_innobase::general_fetch中找到這部分代碼如下:
default: error = convert_error_code_to_mysql(ret, m_prebuilt->table->flags, m_user_thd);
這里ret如果等于DB_INTERRUPTED就會進入線程退出邏輯,具體邏輯我們后面再看。
而其中DB_INTERRUPTED則代表是被殺死的終止狀態,由如下代碼設置(所謂的"埋點"):
if (trx_is_interrupted(prebuilt->trx)) { ret = DB_INTERRUPTED;
其中trx_is_interrupted很簡單,代碼如下:
return(trx && trx->mysql_thd && thd_killed(trx->mysql_thd)); 而thd_killed如下: extern "C" int thd_killed(const MYSQL_THD thd) { if (thd == NULL) return current_thd != NULL ? current_thd->killed : 0; return thd->killed; //返回了THD::killed}
我們可以看到thd->killed正是我們前面發起kill線程設置的THD::killed為THD::KILL_CONNECTION,最終這個錯誤會層層返回,最終導致handle_connection循環結束進入終止流程。
這種情況和上面類似也是需要檢查線程的THD::killed狀態是否是THD::KILL_CONNECTION,但是我們知道如果處于pthread_cond_wait函數等待下,那么必須有其他線程對其做喚醒操作代碼才會繼續進行不然永遠會不跑到判斷邏輯,我們先來看一下等待棧幀
#0 0x00007ffff7bca68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0#1 0x0000000001ab1d35 in os_event::wait (this=0x7ffe74011f18) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/include/os0event.h:156#2 0x0000000001ab167d in os_event::wait_low (this=0x7ffe74011f18, reset_sig_count=2) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/os/os0event.cc:131#3 0x0000000001ab1aa6 in os_event_wait_low (event=0x7ffe74011f18, reset_sig_count=0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/os/os0event.cc:328#4 0x0000000001a7305f in lock_wait_suspend_thread (thr=0x7ffe74005190) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/lock/lock0wait.cc:387#5 0x0000000001b391fc in row_mysql_handle_errors (new_err=0x7fffec091c4c, trx=0x7fffd78045f0, thr=0x7ffe74005190, savept=0x0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/row/row0mysql.cc:1312#6 0x0000000001b7c2ea in row_search_mvcc (buf=0x7ffe74010160 "\377", mode=PAGE_CUR_G, prebuilt=0x7ffe74004a20, match_mode=0, direction=0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:6318#7 0x00000000019d5443 in ha_innobase::index_read (this=0x7ffe7400e280, buf=0x7ffe74010160 "\377", key_ptr=0x0, key_len=0, find_flag=HA_READ_AFTER_KEY) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9536
這種情況就需要有一個線程喚醒它,但是這里喚醒是Innodb層和上面的說的MySQL層喚醒還不是一個事情(后面描述),到底由誰來喚醒它呢,我們可以將斷點設置在:
event::broadcast
event::signal
上就可以抓到是誰做的這件事情,原來在Innodb內部會有一個線程專門干這事這個線程如下:
#0 os_event::broadcast (this=0x7ffe74011f18) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/include/os0event.h:166#1 0x0000000001ab1be8 in os_event::set (this=0x7ffe74011f18) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/include/os0event.h:61#2 0x0000000001ab1a3a in os_event_set (event=0x7ffe74011f18) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/os/os0event.cc:277#3 0x0000000001a73460 in lock_wait_release_thread_if_suspended (thr=0x7ffe70013360) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/lock/lock0wait.cc:491#4 0x0000000001a6a80d in lock_cancel_waiting_and_release (lock=0x30b1938) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/lock/lock0lock.cc:6896#5 0x0000000001a736a6 in lock_wait_check_and_cancel (slot=0x7fff0060a2a0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/lock/lock0wait.cc:539#6 0x0000000001a7383d in lock_wait_timeout_thread (arg=0x0) at /root/mysqlc/percona-server-locks-detail-5.7.22/storage/innobase/lock/lock0wait.cc:599#7 0x00007ffff7bc6aa1 in start_thread () from /lib64/libpthread.so.0#8 0x00007ffff6719bcd in clone () from /lib64/libc.so.6
我們稍微檢查一下lock_wait_check_and_cancel的代碼就會看到如下:
if (trx_is_interrupted(trx) || (slot->wait_timeout < 100000000 && (wait_time > (double) slot->wait_timeout || wait_time < 0))) { /* Timeout exceeded or a wrap-around in system time counter: cancel the lock request queued by the transaction and release possible other transactions waiting behind; it is possible that the lock has already been granted: in that case do nothing */ lock_mutex_enter(); trx_mutex_enter(trx); if (trx->lock.wait_lock != NULL && !trx_is_high_priority(trx)) { ut_a(trx->lock.que_state == TRX_QUE_LOCK_WAIT); lock_cancel_waiting_and_release(trx->lock.wait_lock); } lock_mutex_exit(); trx_mutex_exit(trx); }
我們看到關鍵地方trx_is_interrupted做了對THD::KILL_CONNECTION的判斷,當然這個線程還會做Innodb 行鎖超時的喚醒工作,這個線程我們可以看到的如下:
| 35 | 4036 | innodb/srv_lock_timeout_thread | NULL | BACKGROUND | NULL | NULL |
如果對于正在執行的語句,需要回滾的會在隨后做回滾操作如下:
if (thd->is_error() || (thd->variables.option_bits & OPTION_MASTER_SQL_ERROR)) trans_rollback_stmt(thd);
總的說來Innodb中正是通過丁奇老師所說的"埋點"來判斷線程是否已經被殺掉,其"埋點"所做的事情就是檢查線程的THD::killed狀態是否是THD::KILL_CONNECTION,這種埋點是有檢測周期的,不可能每行代碼過后都檢查一次所以我大概總結了一下埋點的檢查位置:
每行記錄返回給MySQL層的時候
如果遇到Innodb行鎖處于pthread_cond_wait狀態下,需要srv_lock_timeout_thread線程先對其喚醒做broadcast操作
實際上可以全代碼搜索什么時候將ret = DB_INTERRUPTED; 的位置就是Innodb層的"埋點"。
這種情況就比較簡單了。在空閑的狀態下,待殺死線程會一直堵塞在socket讀上面,因為發起kill線程會關閉socket通道,待殺死線程可以輕松的感知到這件事情,下面是net_read_raw_loop中截取
/* On failure, propagate the error code. */ if (count) { /* Socket should be closed. */ net->error= 2; /* Interrupted by a timeout? */ if (!eof && vio_was_timeout(net->vio)) net->last_errno= ER_NET_READ_INTERRUPTED; else net->last_errno= ER_NET_READ_ERROR;#ifdef MYSQL_SERVER my_error(net->last_errno, MYF(0)); //這里觸發#endif }
這樣handle_connection循環結束,進入終止流程。這種情況會在release_resources中clean_up做回滾操作
還記得前面我們的發起kill線程調用THD::awake的時候最后會做喚醒操作嗎?和Innodb層行鎖等待一樣,如果不喚醒那么代碼就沒辦法推進,到達不了Innodb層中設置的埋點位置,下面我用sleep為例進行描述。首先我們先來看看sleep的邏輯,實際上在 Item_func_sleep::val_int 函數中還有如下代碼:
timed_cond.set_timeout((ulonglong) (timeout * 1000000000.0));//這里就將sleep的值春如到了timed_cond這個結構體中 mysql_cond_init(key_item_func_sleep_cond, &cond); // pthread_cond_init 初始化 cond mysql_mutex_lock(&LOCK_item_func_sleep); //加鎖 pthread_mutex_lock 對LOCK_item_func_sleep mutex THD::enter_cond thd->ENTER_COND(&cond, &LOCK_item_func_sleep, &stage_user_sleep, NULL); //#define ENTER_COND(C, M, S, O) enter_cond(C, M, S, O, __func__, __FILE__, __LINE__) //這一步cond會傳遞給THD中其他線程也能拿到這個cond了,就可以喚醒它,KILL觸發的時候就需要通過這個條件變量喚醒它 DEBUG_SYNC(current_thd, "func_sleep_before_sleep"); error= 0; thd_wait_begin(thd, THD_WAIT_SLEEP); while (!thd->killed) { error= timed_cond.wait(&cond, &LOCK_item_func_sleep); //這里看是可等待 及sleep 功能實現 調用底層pthread_cond_timedwait函數實現 ,并且可以被條件變量喚醒 if (error == ETIMEDOUT || error == ETIME) break; error= 0; }
這里我們來證明一下,下面是sleep線程的棧幀:
[Switching to Thread 0x7fffec064700 (LWP 4738)] #0 THD::enter_cond (this=0x7ffe70000950, cond=0x7fffec061510, mutex=0x2e4d6a0, stage=0x2d8b630, old_stage=0x0, src_function=0x1f2598c "val_int", src_file=0x1f232e8 "/root/mysqlc/percona-server-locks-detail-5.7.22/sql/item_func.cc", src_line=6057) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_class.h:3395#1 0x00000000010265d8 in Item_func_sleep::val_int (this=0x7ffe70006210) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/item_func.cc:6057#2 0x0000000000fafea5 in Item::send (this=0x7ffe70006210, protocol=0x7ffe70001c68, buffer=0x7fffec0619b0) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/item.cc:7564#3 0x000000000156b10c in THD::send_result_set_row (this=0x7ffe70000950, row_items=0x7ffe700055d8) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_class.cc:5026#4 0x0000000001565708 in Query_result_send::send_data (this=0x7ffe700063a8, items=...) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_class.cc:2932
注意這里結構體cond=0x7fffec061510的地址,最終他會傳遞到THD中,以致于其他線程也能后拿到,我們再來看看THD::awake喚醒的條件變量的地址如下:
[Switching to Thread 0x7fffec0f7700 (LWP 4051)] Breakpoint 2, THD::awake (this=0x7ffe70000950, state_to_set=THD::KILL_CONNECTION) at /root/mysqlc/percona-server-locks-detail-5.7.22/sql/sql_class.cc:2206...... (gdb) n2288 mysql_cond_broadcast(current_cond); (gdb) p current_cond $6 = (mysql_cond_t * volatile) 0x7fffec061510
我們可以到看到也是0x7fffec061510,他們是同一個條件變量,那么也證明了確實是THD::awake最終喚醒了我們的sleep。代碼得以繼續,繼續后會達到"埋點",最終handle_connection循環終止達到終止流程。
最終在handle_connection 的循環達到退出了條件,進行連接終止邏輯如下:
{ while (thd_connection_alive(thd)) // { if (do_command(thd)) break; } end_connection(thd); } close_connection(thd, 0, false, false); thd->get_stmt_da()->reset_diagnostics_area(); thd->release_resources(); ..... thd_manager->remove_thd(thd);//這里從THD鏈表上摘下來,之后 KILLED狀態的線程才沒有了。 Connection_handler_manager::dec_connection_count(extra_port_connection); .... delete thd; if (abort_loop) // Server is shutting down so end the pthread. break; channel_info= Per_thread_connection_handler::block_until_new_connection(); if (channel_info == NULL) break; pthread_reused= true;
這里我們發現會經歷幾個函數end_connection/get_stmt_da()->reset_diagnostics_area()/release_resources 然后來到了thd_manager->remove_thd(thd),最終這個鏈接會被重用。實際上直到release_resources做完我們才會看到show processlist中的狀態消失。可以修改代碼,在release_resources函數前后加上sleep(10)函數來驗證,如下:
sleep(10);thd->release_resources();sleep(10);
得到的測試結果如下:
mysql> show processlist ; kill 31;kill 33;kill 35; +----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+| Id | User | Host | db | Command | Time | State | Info | Rows_sent | Rows_examined | +----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+ | 7 | root | localhost | NULL | Query | 0 | starting | show processlist | 0 | 0 || 31 | root | localhost | NULL | Sleep | 35 | | NULL | 1 | 0 | | 33 | root | localhost | NULL | Sleep | 32 | | NULL | 1 | 0 || 35 | root | localhost | NULL | Sleep | 29 | | NULL | 1 | 0 | +----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+ mysql> show processlist ; +----+------+-----------+------+---------+------+-------------+------------------+-----------+---------------+ | Id | User | Host | db | Command | Time | State | Info | Rows_sent | Rows_examined |+----+------+-----------+------+---------+------+-------------+------------------+-----------+---------------+| 7 | root | localhost | NULL | Query | 0 | starting | show processlist | 0 | 0 | | 31 | root | localhost | NULL | Killed | 44 | cleaning up | NULL | 1 | 0 || 33 | root | localhost | NULL | Killed | 41 | cleaning up | NULL | 1 | 0 | | 35 | root | localhost | NULL | Killed | 38 | cleaning up | NULL | 1 | 0 |+----+------+-----------+------+---------+------+-------------+------------------+-----------+---------------+4 rows in set (0.02 sec) mysql> show processlist ; +----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+| Id | User | Host | db | Command | Time | State | Info | Rows_sent | Rows_examined | +----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+ | 7 | root | localhost | NULL | Query | 0 | starting | show processlist | 0 | 0 |+----+------+-----------+------+---------+------+----------+------------------+-----------+---------------+
可以看到大約10秒后才Killed狀態才消失,而Killed狀態沒有出現20秒因此可以確認是這一步完成后Killed線程才會在show processlist中消失。
kill動作是一個線程作用于另外一個線程,他們之間的橋梁就是THD:killed這個共享變量。
對于Innodb層的如果有行鎖等待那么kill會通過線程srv_lock_timeout_thread將其喚醒,然后繼續代碼邏輯。
對于MySQL層的等待同樣需要喚醒這是kill發起命令線程完成的,然后繼續代碼邏輯
將show processlist中的killed狀態的線程移除是在整個工作完成之后,比如回滾等
kill狀態的響應是通過某些預先設置的檢查點進行的,如果達不到這個檢查點將一直處于Killed狀態
即便檢查點達到,如果在代碼邏輯中出現其他的Mutex鎖問題得不到退出那么Killed狀態一直持續如下的列子(BUG?):
MySQL:kill和show global status命令hang住一列
https://www.jianshu.com/p/70614ae01046
到此,關于“MySQL中 kill會話的實現原理”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。