背景

AliSQL 上面有人提交了一个 bug,在使用主备的时候 service stop mysql 不能关闭主库,一直显示 shutting down mysql …,到底怎么回事呢,先来看一下 service stop mysql 是怎么停止数据库的。配置 MySQL 在系统启动时启动需要把 MYSQL_BASEDIR/support-files 目录下的脚本 mysql.sever 放到 /etc/init.d/ 目录下,脚本来控制 mysqld 的启动和停止。看一下脚本中的代码 :

  1. if test -s "$mysqld_pid_file_path"
  2. then
  3. mysqld_pid=`cat "$mysqld_pid_file_path"`
  4. if (kill -0 $mysqld_pid 2>/dev/null)
  5. then
  6. echo $echo_n "Shutting down MySQL"
  7. kill $mysqld_pid
  8. # mysqld should remove the pid file when it exits, so wait for it.
  9. wait_for_pid removed "$mysqld_pid" "$mysqld_pid_file_path"; return_value=$?
  10. ...

实际上的关闭动作就是向 mysqld 进程发送一个 kill pid 的信号,也就是 TERM , wait_for_pid 函数中就是不断检测 $MYSQL_DATADIR 下面的 pid 文件是否存在,并且打印 ‘.’,所以上述问题应该是 mysqld 没有正确处理接收到的信号。

信号处理机制

多线程信号处理

进程中的信号处理是异步的,当信号发送给进程之后,就会中断进程当前的执行流程,跳到注册的对应信号处理函数中,执行完毕后再返回进程的执行流程。在多线程信号处理中,一般采用一个单独的线程阻塞的等待信号集,然后处理信号,重新阻塞等待。线程的信号处理有以下几个特点:

  • 每个线程都有自己的信号屏蔽字(单个线程可以屏蔽某些信号)
  • 信号的处理是整个进程中所有线程共享的(某个线程修改信号处理行为后,也会影响其它线程)
  • 进程中的信号是递送到单个线程的,如果一个信号和硬件故障相关,那么该信号就会被递送到引起该事件的线程,否是是发送到任意一个线程。
  1. 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 获得当前的信号屏蔽字。

  1. int sigwait(const sigset_t \*restrict set, int \*restrict sig)

sigwait 将会挂起调用线程,直到接收到 set 中设置的信号,具体的信号将会通过 sig 返回,同时会从 set 中删除 sig 信号。 在调用 sigwait 之前,必须阻塞那些它正在等待的信号,否则在调用的时间窗口就可能接收到信号。

  1. int pthread_kill(pthread_t thread, int sig)

发送信号到指定线程,如果 sig 为 0,可以用来判断线程是否还活着。

man pthread_sigmask 里面给了一个例子:

  1. 1 #include <pthread.h>
  2. 2 #include <stdio.h>
  3. 3 #include <stdlib.h>
  4. 4 #include <unistd.h>
  5. 5 #include <signal.h>
  6. 6 #include <errno.h>
  7. 7
  8. 8 /* Simple error handling functions */
  9. 9
  10. 10 #define handle_error_en(en, msg) \
  11. 11 do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
  12. 12
  13. 13 static void *
  14. 14 sig_thread(void *arg)
  15. 15 {
  16. 16 sigset_t *set = (sigset_t *) arg;
  17. 17 int s, sig;
  18. 18
  19. 19 for (;;) {
  20. 20 s = sigwait(set, &sig);
  21. 21 if (s != 0)
  22. 22 handle_error_en(s, "sigwait");
  23. 23 printf("Signal handling thread got signal %d\n", sig);
  24. 24 }
  25. 25 }
  26. 26
  27. 27 int main(int argc, char *argv[])
  28. 28 {
  29. 29 pthread_t thread;
  30. 30 sigset_t set;
  31. 31 int s;
  32. 32 /* Block SIGINT; other threads created by main() will inherit
  33. 33 * a copy of the signal mask. */
  34. 32 /* Block SIGINT; other threads created by main() will inherit
  35. 33 * a copy of the signal mask. */
  36. 34
  37. 35 sigemptyset(&set);
  38. 36 sigaddset(&set, SIGQUIT);
  39. 37 sigaddset(&set, SIGUSR1);
  40. 38 s = pthread_sigmask(SIG_BLOCK, &set, NULL);
  41. 39 //s = sigprocmask(SIG_BLOCK, &set, NULL);
  42. 40 if (s != 0)
  43. 41 handle_error_en(s, "pthread_sigmask");
  44. 42
  45. 43 s = pthread_create(&thread, NULL, &sig_thread, (void *) &set);
  46. 44 if (s != 0)
  47. 45 handle_error_en(s, "pthread_create");
  48. 46
  49. 47 /* Main thread carries on to create other threads and/or do
  50. 48 * other work */
  51. 49
  52. 50 pause(); /* Dummy pause so we can test program */
  53. 51 return 0;
  54. 52 }

执行一下:

  1. $ ./a.out &
  2. [1] 5423
  3. $ kill -QUIT %1
  4. Signal handling thread got signal 3
  5. $ kill -USR1 %1
  6. Signal handling thread got signal 10
  7. $ kill -TERM %1
  8. [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 线程,初始化阶段的调用堆栈为:

  1. init_common_variables
  2. |
  3. |----- ReplSemiSyncMaster::initObject
  4. |
  5. |----- Ack_receiver::start

而 init_common_variables 的调用是在 my_init_signal 之前,也就是 Ack Receiver 线程没有办法继承信号屏蔽字,不会屏蔽 SIGTERM 信号。在 my_init_signal 中还有一段这样的代码:

  1. /* Fix signals if blocked by parents (can happen on Mac OS X) */
  2. ....
  3. sa.sa_handler = print_signal_warning;
  4. sigaction(SIGTERM, &sa, (struct sigaction\*) 0);
  5. ...

对于信号的修改的作用于整个进程的,也就是说之前启动的 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 里面还有要初始化的变量呢。