我在第25和27篇文章中,和你介绍了主备切换流程。经过这些内容的讲解,你应该已经很清楚了:在一主一备的双 M 架构里,主备切换只须要把客户端流量切到备库;而在一主多
从架构里,主备切换除了要把客户端流量切到备库外,还须要把从库接到新主库上。mysql
主备切换有两种场景,一种是主动切换,一种是被动切换。而其中被动切换,每每是由于主库出问题了,由 HA 系统发起的。sql
这也就引出了咱们今天要讨论的问题:怎么判断一个主库出问题了?数据库
你必定会说,这很简单啊,连上 MySQL,执行个 select 1 就行了。可是 select 1 成功返回了,就表示主库没问题吗?bash
实际上,select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题。如今,咱们来看一下这个场景。服务器
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)
图 1 查询 blockedsession
咱们设置 innodb_thread_concurrency 参数的目的是,控制 InnoDB 的并发线程上限。架构
也就是说,一旦并发线程数达到这个值,InnoDB 在接收到新请求的时候,就会进入等待状态,直到有线程退出。并发
这里,我把 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 之间的值。这时,你必定会有疑问,并发线程上限数设置为 128 够干啥,线上的并发链接数动不
动就上千了。
产生这个疑问的缘由,是搞混了并发链接和并发查询。
并发链接和并发查询,并非同一个概念。你在 show processlist 的结果里,看到的几千个链接,指的就是并发链接。而“当前正在执行”的语句,才是咱们所说的并发查询。
并发链接数达到几千个影响并不大,就是多占一些内存而已。咱们应该关注的是并发查询,由于并发查询过高才是 CPU 杀手。这也是为何咱们须要设置
innodb_thread_concurrency 参数的缘由。
而后,你可能还会想起咱们在第 7 篇文章中讲到的热点更新和死锁检测的时候,若是把innodb_thread_concurrency 设置为 128 的话,那么出现同一行热点更新的问题时,是
不是很快就把 128 消耗完了,这样整个系统是否是就挂了呢?
实际上,在线程进入锁等待之后,并发线程的计数会减一,也就是说等行锁(也包括间隙锁)的线程是不算在 128 里面的。
MySQL 这样设计是很是有意义的。由于,进入锁等待的线程已经不吃 CPU 了;更重要的是,必须这么设计,才能避免整个系统锁死。
为何呢?假设处于锁等待的线程也占并发线程的计数,你能够设想一下这个场景:
1. 线程 1 执行 begin; update t set c=c+1 where id=1, 启动了事务 trx1, 而后保持这个状态。这时候,线程处于空闲状态,不算在并发线程里面。
2. 线程 2 到线程 129 都执行 update t set c=c+1 where id=1; 因为等行锁,进入等待状态。这样就有 128 个线程处于等待状态;
3. 若是处于锁等待状态的线程计数不减一,InnoDB 就会认为线程数用满了,会阻止其余语句进入引擎执行,这样线程 1 不能提交事务。而另外的 128 个线程又处于锁等待状
态,整个系统就堵住了。
下图 2 显示的就是这个状态。
图 2 系统锁死状态(假设等行锁的语句占用并发计数)
这时候 InnoDB 不能响应任何请求,整个系统被锁死。并且,因为全部线程都处于等待状态,此时占用的 CPU 倒是 0,而这明显不合理。因此,咱们说 InnoDB 在设计时,遇到进
程进入锁等待的状况时,将并发线程的计数减 1 的设计,是合理并且是必要的。
虽说等锁的线程不算在并发线程计数里,但若是它在真正地执行查询,就好比咱们上面例子中前三个事务中的 select sleep(100) from t,仍是要算进并发线程的计数的。
在这个例子中,同时在执行的语句超过了设置的 innodb_thread_concurrency 的值,这时候系统其实已经不行了,可是经过 select 1 来检测系统,会认为系统仍是正常的。
所以,咱们使用 select 1 的判断逻辑要修改一下。
为了可以检测 InnoDB 并发线程数过多致使的系统不可用状况,咱们须要找一个访问
InnoDB 的场景。通常的作法是,在系统库(mysql 库)里建立一个表,好比命名为health_check,里面只放一行数据,而后按期执行:
mysql> select * from mysql.health_check;
使用这个方法,咱们能够检测出因为并发线程过多致使的数据库不可用的状况。
可是,咱们立刻还会碰到下一个问题,即:空间满了之后,这种方法又会变得很差使。
咱们知道,更新事务要写 binlog,而一旦 binlog 所在磁盘的空间占用率达到 100%,那么全部的更新语句和事务提交的 commit 语句就都会被堵住。可是,系统这时候仍是能够
正常读数据的。
所以,咱们仍是把这条监控语句再改进一下。接下来,咱们就看看把查询语句改为更新语句后的效果。
既然要更新,就要放个有意义的字段,常见作法是放一个 timestamp 字段,用来表示最后一次执行检测的时间。这条更新语句相似于:
mysql> 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 作主键。
mysql> 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();
因为 MySQL 规定了主库和备库的 server_id 必须不一样(不然建立主备关系的时候就会报错),这样就能够保证主、备库各自的检测命令不会发生冲突。
更新判断是一个相对比较经常使用的方案了,不过依然存在一些问题。其中,“断定慢”一直是让 DBA 头疼的问题。
你必定会疑惑,更新语句,若是失败或者超时,就能够发起主备切换了,为何还会有断定慢的问题呢?
其实,这里涉及到的是服务器 IO 资源分配的问题。
首先,全部的检测逻辑都须要一个超时时间 N。执行一条 update 语句,超过 N 秒后还不返回,就认为系统不可用。
你能够设想一个日志盘的 IO 利用率已是 100% 的场景。这时候,整个系统响应很是慢,已经须要作主备切换了。
可是你要知道,IO 利用率 100% 表示系统的 IO 是在工做的,每一个请求都有机会得到 IO资源,执行本身的任务。而咱们的检测使用的 update 命令,须要的资源不多,因此可能
在拿到 IO 资源的时候就能够提交成功,而且在超时时间 N 秒未到达以前就返回给了检测系统。
检测系统一看,update 命令没有超时,因而就获得了“系统正常”的结论。
也就是说,这时候在业务系统上正常的 SQL 语句已经执行得很慢了,可是 DBA 上去一看,HA 系统还在正常工做,而且认为主库如今处于可用状态。
之因此会出现这个现象,根本缘由是咱们上面说的全部方法,都是基于外部检测的。外部检测自然有一个问题,就是随机性。
由于,外部检测都须要定时轮询,因此系统可能已经出问题了,可是却须要等到下一个检测发起执行语句的时候,咱们才有可能发现问题。并且,若是你的运气不够好的话,可能
第一次轮询还不能发现,这就会致使切换慢的问题。
因此,接下来我要再和你介绍一种在 MySQL 内部发现数据库问题的方法。
针对磁盘利用率这个问题,若是 MySQL 能够告诉咱们,内部每一次 IO 请求的时间,那咱们判断数据库是否出问题的方法就可靠得多了。
其实,MySQL 5.6 版本之后提供的 performance_schema 库,就在file_summary_by_event_name 表里统计了每次 IO 请求的时间。
file_summary_by_event_name 表里有不少行数据,咱们先来看看event_name='wait/io/file/innodb/innodb_log_file’这一行。
图 3 performance_schema.file_summary_by_event_name 的一行
图中这一行表示统计的是 redo log 的写入时间,第一列 EVENT_NAME 表示统计的类型。
接下来的三组数据,显示的是 redo log 操做的时间统计。
第一组五列,是全部 IO 类型的统计。其中,COUNT_STAR 是全部 IO 的总次数,接下来四列是具体的统计项, 单位是皮秒;前缀 SUM、MIN、AVG、MAX,顾名思义指的就是
总和、最小值、平均值和最大值。
第二组六列,是读操做的统计。最后一列 SUM_NUMBER_OF_BYTES_READ 统计的是,总共从 redo log 里读了多少个字节。
第三组六列,统计的是写操做。
最后的第四组数据,是对其余类型数据的统计。在 redo log 里,你能够认为它们就是对fsync 的统计。
在 performance_schema 库的 file_summary_by_event_name 表里,binlog 对应的是event_name = "wait/io/file/sql/binlog"这一行。各个字段的统计逻辑,与 redo log 的
各个字段彻底相同。这里,我就再也不赘述了。
由于咱们每一次操做数据库,performance_schema 都须要额外地统计这些信息,因此咱们打开这个统计功能是有性能损耗的。
个人测试结果是,若是打开全部的 performance_schema 项,性能大概会降低 10% 左右。因此,我建议你只打开本身须要的项进行统计。你能够经过下面的方法打开或者关闭
某个具体项的统计。
若是要打开 redo log 的时间监控,你能够执行这个语句:
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 毫秒属于异常,而后使用相似下面这条语句做为检测逻辑。
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;
发现异常后,取到你须要的信息,再经过下面这条语句:
mysql> truncate table performance_schema.file_summary_by_event_name;
把以前的统计信息清空。这样若是后面的监控中,再次出现这个异常,就能够加入监控累积值了。
今天,我和你介绍了检测一个 MySQL 实例健康状态的几种方法,以及各类方法存在的问题和演进的逻辑。
你看完后可能会以为,select 1 这样的方法是否是已经被淘汰了呢,但实际上使用很是普遍的 MHA(Master High Availability),默认使用的就是这个方法。
MHA 中的另外一个可选方法是只作链接,就是 “若是链接成功就认为主库没问题”。不过据我所知,选择这个方法的不多。
其实,每一个改进的方案,都会增长额外损耗,并不能用“对错”作直接判断,须要你根据业务实际状况去作权衡。
我我的比较倾向的方案,是优先考虑 update 系统表,而后再配合增长检测performance_schema 的信息。
最后,又到了咱们的思考题时间。
今天,我想问你的是:业务系统通常也有高可用的需求,在你开发和维护过的服务中,你是怎么判断服务有没有出问题的呢?
你能够把你用到的方法和分析写在留言区,我会在下一篇文章中选取有趣的方案一块儿来分享和分析。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
上期的问题是,若是使用 GTID 等位点的方案作读写分离,在对大表作 DDL 的时候会怎么样。
假设,这条语句在主库上要执行 10 分钟,提交后传到备库就要 10 分钟(典型的大事务)。那么,在主库 DDL 以后再提交的事务的 GTID,去备库查的时候,就会等 10 分钟才出现。
这样,这个读写分离机制在这 10 分钟以内都会超时,而后走主库。
这种预期内的操做,应该在业务低峰期的时候,确保主库可以支持全部业务查询,而后把读请求都切到主库,再在主库上作 DDL。等备库延迟追上之后,再把读请求切回备库。
经过这个思考题,我主要想让关注的是,大事务对等位点方案的影响。
固然了,使用 gh-ost 方案来解决这个问题也是不错的选择。
@曾剑、@max 同窗提到的备库先作,再切主库的方法也是能够的。