MySQL45讲
实践篇
29 | 如何判断一个数据库是不是出问题了?
select 1 判断
select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题。
set global innodb_thread_concurrency=3; # 控制 InnoDB 的并发线程上限
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 设置成 3,表示 InnoDB 只允许 3 个线程并行执行。前三个 session 中的 sleep(100),使得这三个语句都处于“执行”状态,以此来模拟大查询。
session D 里面,select 1 执行成功,但查询表 t 的语句被堵住。也就是说,如果这时候用 select 1 来检测实例是否正常的话,是检测不出问题的。
在 InnoDB 中,innodb_thread_concurrency 这个参数的默认值是 0,表示不限制并发线程数量。但是,不限制并发线程数肯定是不行的。因为,一个机器的 CPU 核数有限,线程全冲进来,上下文切换的成本就会太高。通常情况下,建议把 innodb_thread_concurrency 设置为 64~128 之间的值。
并发连接和并发查询,并不是同一个概念。在 show processlist 的结果里,看到的几千个连接,指的就是并发连接。而 “当前正在执行”的语句,才是并发查询。
并发连接数达到几千个影响并不大,就是多占一些内存而已。应该关注的是并发查询,因为并发查询太高才是 CPU 杀手。这也是需要设置 innodb_thread_concurrency 参数的原因。
疑问:如果把 innodb_thread_concurrency 设置为 128 的话,那么当出现同一行热点更新的问题时,并发线程数是不是很快就被消耗完,这样整个系统是不是就挂了?
实际上,在线程进入锁等待以后,并发线程的计数会减一,也就是说等行锁(也包括间隙锁)的线程是不算在并发线程数里。
MySQL 这样设计的意义:因为,进入锁等待的线程不吃 CPU;更重要的是,必须这么设计,才能避免整个系统锁死。
虽然说等锁的线程不算在并发线程数里,但如果它在真正地执行查询,就比如上面例子中前三个事务中的 select sleep(100) from t,还是要算进并发线程的计数。
查表判断
为了能够检测 InnoDB 并发线程数过多导致的系统不可用情况,需要找一个访问 InnoDB 的场景。一般的做法是,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行:
select * from mysql.health_check;
更新事务要写 binlog,而一旦 binlog 所在磁盘的空间占用率达到 100%,那么所有的更新语句和事务提交的 commit 语句就都会被堵住。但是,系统这时候还是可以正常读数据的。
更新判断(相对比较常用的方案)
在查表判断的基础上增加一个 timestamp 字段,用来表示最后一次执行检测的时间。这条更新语句类似于:
update mysql.health_check set t_modified=now();
备库的检测也是要写 binlog 的。由于一般会把数据库 A 和 B 的主备关系设计为双 M 结构,所以在备库 B 上执行的检测命令,也要发回给主库 A。但是,如果主库 A 和备库 B 都用相同的更新命令,就可能出现行冲突,也就是可能会导致主备同步停止。所以,mysql.health_check 这个表就不能只有一行数据。
为了让主备之间的更新不产生冲突,可以在 mysql.health_check 表上存入多行数据,并用 A、B 的 server_id 做主键。
CREATE TABLE `health_check` (
`id` int(11) NOT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
/* 检测命令 */
insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
疑问:更新语句,如果失败或者超时,就可以发起主备切换,为什么还会有判定慢的问题?
所有的检测逻辑都需要一个超时时间 N。执行一条 update 语句,超过 N 秒后还不返回,就认为系统不可用。
假设一个日志盘的 IO 利用率已经是 100%。这时候,整个系统响应非常慢,需要做主备切换。
IO 利用率 100% 表示系统的 IO 是在工作的,每个请求都有机会获得 IO 资源,执行自己的任务。而检测使用的 update 命令,需要的资源很少,所以可能在拿到 IO 资源的时候就可以提交成功,并且在超时时间 N 秒未到达之前就返回给检测系统。检测系统发现update 命令没有超时,于是就得到了“系统正常”的结论。
之所以会出现这个现象,根本原因是上面说的所有方法,都是基于外部检测。外部检测天然有一个问题,就是随机性。
外部检测都需要定时轮询,所以系统可能已经出了问题,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题。而且,如果运气不好的话,可能第一次轮询还不能发现,这就会导致切换慢的问题。
内部统计
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、MIN、AVG、MAX,顾名思义指的就是总和、最小值、平均值和最大值。
- 第二组六列,是读操作的统计。
- 最后一列 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 都需要额外地统计这些信息,所以打开这个统计功能有性能损耗。 如果打开所有的 performance_schema 项,性能大概会下降 10% 左右。所以,建议只打开自己需要的项进行统计。 可以通过下面的方法打开或者关闭某个具体项的统计。
如果要打开 redo log 的时间监控,可以执行这个语句:
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 毫秒属于异常,然后使用类似下面这条语句作为检测逻辑:
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;
发现异常后,取到需要的信息,再通过下面这条语句把之前的统计信息清空。这样如果后面的监控中,再次出现这个异常,就会被加入监控累积值。
truncate table performance_schema.file_summary_by_event_name;