之前的文章已经说过:在一主一备的双M架构里,主备切换只需要把客户端流量切到备库;而在一主多从架构里,主备切换除了要把客户端流量切到备库外,还需要把从库接到新主库上。
主备切换有两种场景,分别是主动切换和被动切换,其中被动切换往往是因为主库出问题由HA系统发起。那么怎么判断主库出问题了呢?
select 1判断
select 1成功返回,能说明这个库的进程还在,但不能说明主库没有问题。比如如下场景:
sql
set global innodb_thread_concurrency=3;
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t values(1,1)

innodb_thread_concurrency参数的作用是控制InnoDB并发线程上限。当并发线程数达到这个值,InnoDB在接收新请求时就会进入等待状态,直到有线程退出。
所以场景里session D的select 1能执行成功,但实际实例已无法查询。
InnoDB将该参数的默认值设为0,但不限制并发线程数实际上是不行的,因为一个机器的CPU核数有限,线程全进来会有很大的上下文切换成本。通常情况下,建议把该值设为64-128之间的值。
这里可能会产生几个疑问。第一,线上的并发连接数动不动就上千了,设置为128有什么用?
这是搞混了并发连接和并发查询。在show processlist结果里,看到的几千个连接指的是并发连接,而当前正在执行的语句才是并发查询。并发连接数达到几千个影响并不大,只是多占一些内存,而并发查询高会造成CPU占用高,因此需要加以控制。
第二,当出现同一行热点更新,由于会等待锁,128是否会很快消耗完,系统是否会挂?
这也是不会的,在线程进入锁等待后,并发线程的计数会减1,即等行锁(包括间隙锁)的线程是不算在128里的。
这个设计是必须的,否则可能整个系统锁死。假设处于锁等待的线程也占并发线程的计数,设想:
-
线程1执行
begin;update t set c=c+1 where id=1
,启动事务trx1,然后保持该状态。此时线程处于空闲状态,不算在并发线程里; -
线程2到线程129执行
update t set c=c+1 where id=1
,由于等行锁而进入等待状态,这样就有128个线程处于等待状态; -
如果处于锁等待状态的线程计数不减1,InnoDB会认为线程数用满,阻止其他语句进入引擎执行,那么线程1将不能提交事务,而其他128个线程又处于锁等待,整个系统堵住。
该例子里InnoDB不能响应任何请求,但CPU占用却是0,明显不合理。
而如果真正执行查询,比如之前例子三个事务的select sleep(100) from t
,是要算进并发线程的计数的。
该部分主要说明,使用select 1会有问题,因此使用select 1进行判断的逻辑需要修改。
查表判断
为了能检测并发线程数过多导致的系统,需要找一个访问InnoDB的场景。可以在系统库里建一个表(比如命名为check),里面只放一行数据,然后定期执行:
sql
select * from mysql.check;
使用该方法,可以检测出由于并发线程过多导致的数据库不可用的情况。但假如binlog所在磁盘的空间占用率达到100%,那么所有更新语句和事务提交的commit语句都会被堵住,但系统仍能正常读数据,此时这种方法失效。
更新判断
可以把上面的查询改为更新,比如:
sql
update mysql.check set t_modified=now();
节点可用性检测包括主库和备库,由于一般会把主库A和备库B的主备关系设为双M结构,所以在备库B上执行的检测命令也要发回给主库A,此时如果都用相同的更新命令就可能出现行冲突,导致主备同步停止。所以,此时表里不能只有一行数据:
sql
mysql> CREATE TABLE `check` (
`id` int(11) NOT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
/* 检测命令 */
insert into mysql.check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
更新判断存在"判定慢"的问题。所有的检测逻辑都需要一个超时时间N,执行一条更新语句,超过N秒后还不返回,就认为系统不可用。设想一个日志盘的IO利用率已经是100%的场景,此时整个系统响应很慢,已经需要做主备切换了。但我们检测使用的update命令需要的资源很少,是有可能在拿到IO资源时提交成功,并且在超时时间N秒未到达之前就返回给检测系统的。
那么就会出现:业务系统上正常SQL已经执行很慢,但检测系统由于update命令没超时得到"系统正常结论的情况。
之所以出现这个现象,根本原因是上面说的方法都是基于外部检测的,天然有随机性的问题,因为外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题。而且,如果运气不好,可能第一次轮询还不能发现,就会导致切换慢的问题。
内部统计
针对磁盘利用率的问题,如果MySQL可以告诉我们内部每一次IO请求的时间,那判断数据库是否出问题的方法就会可靠许多。
MySQL 5.6版本以后提供的performance_schema库,就在file_summary_by_event_name表里统计了每次IO请求的时间。该表里有很多行数据,先看event_name='wait/io/file/innodb/innodb_log_file'这一行:
该行表示统计的是redo log的写入时间,第一列EVENT_NAME表示统计的类型,接下来三组数据显示的是redo log操作时间统计。
第一组五列是所有IO类型的统计,COUNT_STAR是所有IO的总次数,下面四列是具体统计项。
第二组六列是读操作的统计,SUM_NUMBER_OF_BYTES_READ统计的是总共从redo log里读了多少字节。
第三组六列是写操作的统计,第四组是对其他类型数据的统计。
在performance_schema库的file_summary_by_event_name表里,binlog对应的是event_name = "wait/io/file/sql/binlog"这一行。各个字段的统计逻辑,与redo log的各个字段完全相同。
由于每次操作数据库,performance_schema都需要额外统计这些信息,所以打开这个统计功能是有性能损耗的,建议只打开自己需要的项进行统计。可以通过下面的方法打开或关闭某个具体项的统计,比如要打开redo log的时间监控:
sql
mysql> update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';
假设已经开启了redo log和binlog两个统计信息,如何把这个信息用到实例状态诊断上呢?
可以通过MAX_TIMER的值来判断数据库是否出问题。比如设定阈值,单次IO请求时间超过200毫秒属于异常,然后使用类似下面这条语句作为检测逻辑:
sql
mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;
发现异常后,取到需要的信息,再通过下面的语句:
sql
mysql> truncate table performance_schema.file_summary_by_event_name;
把之前的统计信息清空。这样如果后面的监控中,再次出现这个异常,就可以加入监控累积值了。