MySQL · 捉蟲動態 · 信號處理機製分析
背景
在 AliSQL 上麵有人提交了一個 bug,在使用主備的時候 service stop mysql 不能關閉主庫,一直顯示 shutting down mysql …,到底怎麼回事呢,先來看一下 service stop mysql 是怎麼停止數據庫的。配置 MySQL 在係統啟動時啟動需要把 MYSQL_BASEDIR/support-files 目錄下的腳本 mysql.sever 放到 /etc/init.d/ 目錄下,腳本來控製 mysqld 的啟動和停止。看一下腳本中的代碼 :
if test -s "$mysqld_pid_file_path"
then
mysqld_pid=`cat "$mysqld_pid_file_path"`
if (kill -0 $mysqld_pid 2>/dev/null)
then
echo $echo_n "Shutting down MySQL"
kill $mysqld_pid
# mysqld should remove the pid file when it exits, so wait for it.
wait_for_pid removed "$mysqld_pid" "$mysqld_pid_file_path"; return_value=$?
...
實際上的關閉動作就是向 mysqld 進程發送一個 kill pid 的信號,也就是 TERM , wait_for_pid 函數中就是不斷檢測 $MYSQL_DATADIR 下麵的 pid 文件是否存在,並且打印 ‘.’,所以上述問題應該是 mysqld 沒有正確處理接收到的信號。
信號處理機製
多線程信號處理
進程中的信號處理是異步的,當信號發送給進程之後,就會中斷進程當前的執行流程,跳到注冊的對應信號處理函數中,執行完畢後再返回進程的執行流程。在多線程信號處理中,一般采用一個單獨的線程阻塞的等待信號集,然後處理信號,重新阻塞等待。線程的信號處理有以下幾個特點:
- 每個線程都有自己的信號屏蔽字(單個線程可以屏蔽某些信號)
- 信號的處理是整個進程中所有線程共享的(某個線程修改信號處理行為後,也會影響其它線程)
- 進程中的信號是遞送到單個線程的,如果一個信號和硬件故障相關,那麼該信號就會被遞送到引起該事件的線程,否是是發送到任意一個線程。
int pthread_sigmask(int how, const sigset_t * restrict set, sigset_t *restrict oset);
在進程中使用 sigprocmask 設置信號屏蔽字,在線程中使用 pthread_sigmask,他們的基本相同,pthread_sigmask 工作在線程中,失敗時返回錯誤碼,而 sigprocmask 會設置 errno 並返回 -1。參數 how 控製設置屏蔽字的行為,值為 SIG_BLOCK(把信號集添加到現有信號集中,取並集), SIG_SET_MASK(設置信號集為 set), SIG_UNBLOCK(從信號集中移除 set 中的信號)。set 表示需要操縱的信號集合。oset 返回設置之前的信號屏蔽字,如果設置 set 為 NULL,可以通過 oset 獲得當前的信號屏蔽字。
int sigwait(const sigset_t \*restrict set, int \*restrict sig)
sigwait 將會掛起調用線程,直到接收到 set 中設置的信號,具體的信號將會通過 sig 返回,同時會從 set 中刪除 sig 信號。 在調用 sigwait 之前,必須阻塞那些它正在等待的信號,否則在調用的時間窗口就可能接收到信號。
int pthread_kill(pthread_t thread, int sig)
發送信號到指定線程,如果 sig 為 0,可以用來判斷線程是否還活著。
man pthread_sigmask 裏麵給了一個例子:
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <signal.h>
6 #include <errno.h>
7
8 /* Simple error handling functions */
9
10 #define handle_error_en(en, msg) \
11 do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
12
13 static void *
14 sig_thread(void *arg)
15 {
16 sigset_t *set = (sigset_t *) arg;
17 int s, sig;
18
19 for (;;) {
20 s = sigwait(set, &sig);
21 if (s != 0)
22 handle_error_en(s, "sigwait");
23 printf("Signal handling thread got signal %d\n", sig);
24 }
25 }
26
27 int main(int argc, char *argv[])
28 {
29 pthread_t thread;
30 sigset_t set;
31 int s;
32 /* Block SIGINT; other threads created by main() will inherit
33 * a copy of the signal mask. */
32 /* Block SIGINT; other threads created by main() will inherit
33 * a copy of the signal mask. */
34
35 sigemptyset(&set);
36 sigaddset(&set, SIGQUIT);
37 sigaddset(&set, SIGUSR1);
38 s = pthread_sigmask(SIG_BLOCK, &set, NULL);
39 //s = sigprocmask(SIG_BLOCK, &set, NULL);
40 if (s != 0)
41 handle_error_en(s, "pthread_sigmask");
42
43 s = pthread_create(&thread, NULL, &sig_thread, (void *) &set);
44 if (s != 0)
45 handle_error_en(s, "pthread_create");
46
47 /* Main thread carries on to create other threads and/or do
48 * other work */
49
50 pause(); /* Dummy pause so we can test program */
51 return 0;
52 }
執行一下:
$ ./a.out &
[1] 5423
$ kill -QUIT %1
Signal handling thread got signal 3
$ kill -USR1 %1
Signal handling thread got signal 10
$ kill -TERM %1
[1]+ Terminated ./a.out
測試了一下,把上麵代碼的 pthread_sigmask 替換成 sigprocmask ,同樣能夠正確執行,說明線程也能夠繼承原進程的屏蔽字,不過還是盡量使用 pthread_sigmask, 表述清楚點,而且說不定還有其它坑。
MySQL 信號處理
MySQL 是典型的多線程處理,它的信號處理形式和上一小節介紹的差不多,在 mysqld 啟動的時候調用 my_init_signal 初始化信號屏蔽字,把需要信號處理線程處理的信號屏蔽起來,然後啟動信號處理函數,入口是 signal_hand 。
在 my_init_signal 函數中,設置 SIGSEGC, SIGABORT, SIGBUS, SIGILL, SIGFPE 的處理函數為 handle_fatal_signal,把 SIGPIPE,SIGQUIT, SIGHUP, SIGTERM, SIGTSTP 加入到信號屏蔽字裏,調用 sigprocmask 和 pthread_sigmask 設置屏蔽字。這一係列動作是在 mysql 啟動其它輔助線程之前完成的動作,意圖很明顯,就是讓之後的線程都繼承設置的信號屏蔽字,把所有的信號交給信號處理線程去處理。
signal_hand 函數首先把需要處理的信號放到信號集合裏去,然後完成 create_pid_file ,data 目錄下的 pid 文件實際上是由信號處理線程創建的。接著等待 mysqld 完成啟動,各個線程之間需要同步,核心代碼是一個死循環,通過 my_sigwait 調用 sigwait 阻塞的等待信號的到來。我們目前主要關心 SIGTERM 的處理,和 SIGQUIT, SIGKILL 處理方式相同,都是調用 kill_server 關閉整個數據庫。
Bug Fix
文中開頭的鏈接中提到 loose-rpl_semi_sync_master_enabled = 0 關閉就不會有問題, 如果為 1 就會出現無法關閉的情況,順著這個線索尋找,rpl_semi_sync_master_enabled 在主備使用 semisync 情況下控製啟動 Master 節點的 Ack Receiver 線程,初始化階段的調用堆棧為:
init_common_variables
|
|----- ReplSemiSyncMaster::initObject
|
|----- Ack_receiver::start
而 init_common_variables 的調用是在 my_init_signal 之前,也就是 Ack Receiver 線程沒有辦法繼承信號屏蔽字,不會屏蔽 SIGTERM 信號。在 my_init_signal 中還有一段這樣的代碼:
/* Fix signals if blocked by parents (can happen on Mac OS X) */
....
sa.sa_handler = print_signal_warning;
sigaction(SIGTERM, &sa, (struct sigaction\*) 0);
...
對於信號的修改的作用於整個進程的,也就是說之前啟動的 Ack Receiver 線程沒有信號屏蔽字,而且注冊了信號處理函數。當 SIGTERM 發生後,信號處理線程和 Ack Receiver 線程都可以接收信號處理,信號被隨機的分發(測試高概率都是發給 Ack Receiver),print_signal_warning 僅僅打印信息到 errlog,就出現了無法關閉 mysqld 的情況了。
修改也比較簡單,把 initObject 的操作放到 my_init_signal 之後就好,注意不能把 init_common_variables 整個移到 my_init_signal 之前,因為 my_init_signal 裏麵還有要初始化的變量呢。
最後更新:2017-10-21 09:04:51