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

时间: 2024-09-05 06:56:40

MySQL · 捉虫动态 · 信号处理机制分析的相关文章

MySQL · 捉虫动态 · MySQL 外键异常分析

外键约束异常现象 如下测例中,没有违反引用约束的插入失败. create database `a-b`; use `a-b`; SET FOREIGN_KEY_CHECKS=0; create table t1(c1 int primary key, c2 int) engine=innodb; create table t2(c1 int primary key, c2 int) engine=innodb; alter table t2 add foreign key(c2) referen

MySQL · 捉虫动态 · 备库1206错误问题说明

问题背景 一个用户自建MySQL,出现备库复制中断的问题,报错为slave sql thread 错误,The total number of locks exceeds the lock table size. 报错代码 这个报错在代码中的抛错逻辑为: if UT_LIST_GET_LEN(buf_pool->free) + UT_LIST_GET_LEN(buf_pool->LRU) < buf_pool->curr_size / 4 文字解释是:如果buffer pool中的

MySQL · 捉虫动态 · 并行复制外键约束问题二

背景 并行复制可以大大提高备库的 binlog 应用速度,内核月报也多次对并行复制特性进行介绍,感兴趣的朋友可以回顾下:5.6 并行复制实现分析.5.6 并行复制恢复实现 和 5.6并行复制事件分发机制. 在早期的内核月报,有一篇 并行复制外建约束问题,介绍阿里在 5.5 版本中自己实现并行复制时遇到的外键约束问题,本文接着前作继续介绍并行复制外键约束问题,这次场景不一样,并且目前官方 5.6 最新版本(5.6.30)中也有这个问题. 问题描述 一般情况的复制是 A->B 这样一主一备,本文要描

MySQL · 捉虫动态 · left-join多表导致crash

有一天小编胡乱写SQL, left join了30张表, 结果导致了Mysql server gone away- 我们来看看crash堆栈 <signal handler called> base_list_iterator::next update_ref_and_keys make_join_statistics JOIN::optimize mysql_execute_select 可以看出, 在产生执行计划过程中crash了. 追查 堆栈表明, update_ref_and_keys

MySQL · 捉虫动态 · show binary logs 灵异事件

问题背景 最近在运维 MySQL 中遇到一个神奇的问题,分享给大家.现象是这样的,show binary logs 没有返回结果,flush binary logs 后也不行, 但是 binlog 是正常工作的,show master staus 是有输出的. mysql> show binary logs; Empty set (0.00 sec) mysql> show master status\G *************************** 1. row *********

MySQL · 捉虫动态 · order by limit 造成优化器选择索引错误

问题描述 bug 触发条件如下: 优化器先选择了 where 条件中字段的索引,该索引过滤性较好: SQL 中必须有 order by limit 从而引导优化器尝试使用 order by 字段上的索引进行优化,最终因代价问题没有成功. 复现case 表结构 create table t1( id int auto_increment primary key, a int, b int, c int, key iabc (a, b, c), key ic (c) ) engine = innod

MySQL · 捉虫动态 · MySQL DDL BUG

背景 MySQL保存了两份元数据,一份在server层,保存在FRM文件中,另外一份在引擎层,比如InnoDB的数据字典中,这样也就造成了DDL语句经常导致元数据不一致的情况,下面介绍两个近期出现的因为DDL产生的bug. rename 外键引用的column BUG复现过程 CREATE TABLE t1 (a INT NOT NULL, b INT NOT NULL, INDEX idx(a)) ENGINE=InnoDB; CREATE TABLE t2 (a INT KEY, b INT

MySQL · 捉虫动态 · 删除索引导致表无法打开

问题背景 最近线上遇到一个问题,用户重启实例后发现有张表打不开了,经调研后发现是用户之前的霸蛮操作导致的,下面给出复现步骤: create table t1 (id int not null primary key, name varchar(100) not null) engine=innodb; create table t2 (id int not null primary key, fid int not null, name varchar(100) not null, CONSTR

MySQL · 捉虫动态 · 唯一键约束失效

唯一键是数据库设计中常用的索引类型,主要用于约束数据,不允许出现重复的键值记录.可以想象,如果唯一键约束失效了,将可能产生可怕的逻辑错误.本文主要讨论下最近MySQL爆出来的两个唯一键约束失效导致二级索引corruption的问题. 问题一: 检查重复键加锁逻辑不当 影响版本:MySQL 5.6.21之前,5.6.12之后的版本 介绍分析 在5.6.12之前的版本中,当插入一条带唯一约束的记录时,如果表上已经存在了这条记录,或者有一条标记删除的相同键值记录时,就需要对这条记录加S GAP (类型