数据库内核月报 - 2017 / 10

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 里面还有要初始化的变量呢。