閱讀757 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

  上一篇:go  程序員的業餘生活之健身篇
  下一篇:go  PgSQL · 應用案例 · 經營、銷售分析係統DB設計之共享充電寶