MySQL实战45讲学习笔记:第二十二讲

 1、引子

不知道你在实际运维过程当中有没有碰到这样的情景:业务高峰期,生产环境的 MySQL 压力太大,无法正常响应,须要短时间内、临时性地提高一些性能。html

我之前作业务护航的时候,就偶尔会碰上这种场景。用户的开发负责人说,无论你用什么方案,让业务先跑起来再说。mysql

但,若是是无损方案的话,确定不须要等到这个时候才上场。今天咱们就来聊聊这些临时方案,并着重说一说它们可能存在的风险sql

2、短链接风暴

正常的短链接模式就是链接到数据库后,执行不多的 SQL 语句就断开,下次须要的时候再重连。若是使用的是短链接,在业务高峰期的时候,就可能出现链接数忽然暴涨的状况。数据库

我在第 1 篇文章《基础架构:一条 SQL 查询语句是如何执行的?》中说过,MySQL 创建链接的过程,成本是很高的。除了正常的网络链接三次握手外,还须要作登陆权限判断和
得到这个链接的数据读写权限。安全

在数据库压力比较小的时候,这些额外的成本并不明显。网络

可是,短链接模型存在一个风险,就是一旦数据库处理得慢一些,链接数就会暴涨。max_connections 参数,用来控制一个 MySQL 实例同时存在的链接数的上限,超过这
个值,系统就会拒绝接下来的链接请求,并报错提示“Too many connections”。对于被拒绝链接的请求来讲,从业务角度看就是数据库不可用。session

在机器负载比较高的时候,处理现有请求的时间变长,每一个链接保持的时间也更长。这时,再有新建链接的话,就可能会超过 max_connections 的限制。架构

碰到这种状况时,一个比较天然的想法,就是调高 max_connections 的值。但这样作是有风险的。由于设计 max_connections 这个参数的目的是想保护 MySQL,若是咱们把
它改得太大,让更多的链接均可以进来,那么系统的负载可能会进一步加大,大量的资源耗费在权限验证等逻辑上,结果多是拔苗助长,已经链接的线程拿不到 CPU 资源去执行
业务的 SQL 请求。运维

那么这种状况下,你还有没有别的建议呢?我这里还有两种方法,但要注意,这些方法都是有损的。工具

3、短连接风暴的处理方法一:先处理掉那些占着链接可是不工做的线程。

max_connections 的计算,不是看谁在 running,是只要连着就占用一个计数位置。对于那些不须要保持的链接,咱们能够经过 kill connection 主动踢掉。这个行为跟事先设置
wait_timeout 的效果是同样的。设置 wait_timeout 参数表示的是,一个线程空闲wait_timeout 这么多秒以后,就会被 MySQL 直接断开链接。

可是须要注意,在 show processlist 的结果里,踢掉显示为 sleep 的线程,多是有损的。咱们来看下面这个例子。

图 1 sleep 线程的两种状态

在上面这个例子里,若是断开 session A 的链接,由于这时候 session A 尚未提交,因此 MySQL 只能按照回滚事务来处理;而断开 session B 的链接,就没什么大影响。所
以,若是按照优先级来讲,你应该优先断开像 session B 这样的事务外空闲的链接。

可是,怎么判断哪些是事务外空闲的呢?session C 在 T 时刻以后的 30 秒执行 showprocesslist,看到的结果是这样的。

图 2 sleep 线程的两种状态,show processlist 结果

图中 id=4 和 id=5 的两个会话都是 Sleep 状态。而要看事务具体状态的话,你能够查information_schema 库的 innodb_trx 表。

图 3 从 information_schema.innodb_trx 查询事务状态

这个结果里,trx_mysql_thread_id=4,表示 id=4 的线程还处在事务中。

所以,若是是链接数过多,你能够优先断开事务外空闲过久的链接;若是这样还不够,再考虑断开事务内空闲过久的链接。

从服务端断开链接使用的是 kill connection + id 的命令, 一个客户端处于 sleep 状态时,它的链接被服务端主动断开后,这个客户端并不会立刻知道。直到客户端在发起下一
个请求的时候,才会收到这样的报错“ERROR 2013 (HY000): Lost connection toMySQL server during query”。

从数据库端主动断开链接多是有损的,尤为是有的应用端收到这个错误后,不从新链接,而是直接用这个已经不能用的句柄重试查询。这会致使从应用端看上去,“MySQL
一直没恢复”。

你可能以为这是一个冷笑话,但实际上我碰到过不下 10 次。

因此,若是你是一个支持业务的 DBA,不要假设全部的应用代码都会被正确地处理。即便只是一个断开链接的操做,也要确保通知到业务开发团队。

4、短连接风暴的处理方法二:减小链接过程的消耗。

有的业务代码会在短期内先大量申请数据库链接作备用,若是如今数据库确认是被链接行为打挂了,那么一种可能的作法,是让数据库跳过权限验证阶段。

跳过权限验证的方法是:重启数据库,并使用–skip-grant-tables 参数启动。这样,整个MySQL 会跳过全部的权限验证阶段,包括链接过程和语句执行过程在内。

可是,这种方法特别符合咱们标题里说的“饮鸩止渴”,风险极高,是我特别不建议使用的方案。尤为你的库外网可访问的话,就更不能这么作了。

在 MySQL 8.0 版本里,若是你启用–skip-grant-tables 参数,MySQL 会默认把 --skip-networking 参数打开,表示这时候数据库只能被本地的客户端链接。可见,MySQL 官方
对 skip-grant-tables 这个参数的安全问题也很重视。

除了短链接数暴增可能会带来性能问题外,实际上,咱们在线上碰到更多的是查询或者更新语句致使的性能问题。其中,查询问题比较典型的有两类,一类是由新出现的慢查询导
致的,一类是由 QPS(每秒查询数)突增致使的。而关于更新语句致使的性能问题,我会在下一篇文章和你展开说明。

5、慢查询性能问题

在 MySQL 中,会引起性能问题的慢查询,大致有如下三种可能:

1. 索引没有设计好;
2. SQL 语句没写好;
3. MySQL 选错了索引。

接下来,咱们就具体分析一下这三种可能,以及对应的解决方案。

一、致使慢查询的第一种多是,索引没有设计好。

这种场景通常就是经过紧急建立索引来解决。MySQL 5.6 版本之后,建立索引都支持Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的状况,最高效的作法就
是直接执行 alter table 语句。

比较理想的是可以在备库先执行。假设你如今的服务是一主一备,主库 A、备库 B,这个方案的大体流程是这样的:

1. 在备库 B 上执行 set sql_log_bin=off,也就是不写 binlog,而后执行 alter table 语句加上索引;

2. 执行主备切换;

3. 这时候主库是 B,备库是 A。在 A 上执行 set sql_log_bin=off,而后执行 alter table语句加上索引。

这是一个“古老”的 DDL 方案。平时在作变动的时候,你应该考虑相似 gh-ost 这样的方案,更加稳妥。可是在须要紧急处理时,上面这个方案的效率是最高的。

二、致使慢查询的第二种多是,语句没写好。

好比,咱们犯了在第 18 篇文章《为何这些 SQL 语句逻辑相同,性能却差别巨大?》中提到的那些错误,致使语句没有使用上索引。

这时,咱们能够经过改写 SQL 语句来处理。MySQL 5.7 提供了 query_rewrite 功能,能够把输入的一种语句改写成另一种模式。

好比,语句被错误地写成了 select * from t where id + 1 = 10000,你能够经过下面的方式,增长一个语句改写规则。

这里,call query_rewrite.flush_rewrite_rules() 这个存储过程,是让插入的新规则生效,也就是咱们说的“查询重写”。你能够用图 4 中的方法来确认改写规则是否生效。

图 4 查询重写效果

三、致使慢查询的第三种可能,就是碰上了咱们在第 10 篇文章《MySQL 为何有时候会选错

索引?》中提到的状况,MySQL 选错了索引

这时候,应急方案就是给这个语句加上 force index。

一样地,使用查询重写功能,给原来的语句加上 force index,也能够解决这个问题。

上面我和你讨论的由慢查询致使性能问题的三种可能状况,实际上出现最多的是前两种,即:索引没设计好和语句没写好。而这两种状况,偏偏是彻底能够避免的。好比,经过下
面这个过程,咱们就能够预先发现问题。

1. 上线前,在测试环境,把慢查询日志(slow log)打开,而且把 long_query_time 设置成 0,确保每一个语句都会被记录入慢查询日志

2. 在测试表里插入模拟线上的数据,作一遍回归测试;

3. 观察慢查询日志里每类语句的输出,特别留意 Rows_examined 字段是否与预期一致。(咱们在前面文章中已经屡次用到过 Rows_examined 方法了,相信你已经动手尝试过
了。若是还有不明白的,欢迎给我留言,咱们一块儿讨论)。

不要吝啬这段花在上线前的“额外”时间,由于这会帮你省下不少故障复盘的时间。

若是新增的 SQL 语句很少,手动跑一下就能够。而若是是新项目的话,或者是修改了原有项目的 表结构设计,全量回归测试都是必要的。这时候,你须要工具帮你检查全部的 SQL
语句的返回结果。好比,你可使用开源工具 pt-query-digest(https://www.percona.com/doc/percona-toolkit/3.0/pt-query-digest.html)。

6、QPS 突增问题

有时候因为业务忽然出现高峰,或者应用程序 bug,致使某个语句的 QPS 忽然暴涨,也可能致使 MySQL 压力过大,影响服务。

我以前碰到过一类状况,是由一个新功能的 bug 致使的。固然,最理想的状况是让业务把这个功能下掉,服务天然就会恢复。

而下掉一个功能,若是从数据库端处理的话,对应于不一样的背景,有不一样的方法可用。我这里再和你展开说明一下。

1. 一种是由全新业务的 bug 致使的。假设你的 DB 运维是比较规范的,也就是说白名单是一个个加的。这种状况下,若是你可以肯定业务方会下掉这个功能,只是时间上没那
么快,那么就能够从数据库端直接把白名单去掉。

2. 若是这个新功能使用的是单独的数据库用户,能够用管理员帐号把这个用户删掉,而后断开现有链接。这样,这个新功能的链接不成功,由它引起的 QPS 就会变成 0。

3. 若是这个新增的功能跟主体功能是部署在一块儿的,那么咱们只能经过处理语句来限制。这时,咱们可使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写
成"select 1"返回。

固然,这个操做的风险很高,须要你特别细致。它可能存在两个反作用:

1. 若是别的功能里面也用到了这个 SQL 语句模板,会有误伤;
2. 不少业务并非靠这一个语句就能完成逻辑的,因此若是单独把这一个语句以 select 1的结果返回的话,可能会致使后面的业务逻辑一块儿失败。

因此,方案 3 是用于止血的,跟前面提到的去掉权限验证同样,应该是你全部选项里优先级最低的一个方案。

同时你会发现,其实方案 1 和 2 都要依赖于规范的运维体系:虚拟化、白名单机制、业务帐号分离。因而可知,更多的准备,每每意味着更稳定的系统。

7、小结

今天这篇文章,我以业务高峰期的性能问题为背景,和你介绍了一些紧急处理的手段。

这些处理手段中,既包括了粗暴地拒绝链接和断开链接,也有经过重写语句来绕过一些坑的方法;既有临时的高危方案,也有未雨绸缪的、相对安全的预案。

在实际开发中,咱们也要尽可能避免一些低效的方法,好比避免大量地使用短链接。同时,若是你作业务开发的话,要知道,链接异常断开是常有的事,你的代码里要有正确地重连
并重试的机制。

DBA 虽然能够经过语句重写来暂时处理问题,可是这自己是一个风险高的操做,作好SQL 审计能够减小须要这类操做的机会。

其实,你能够看得出来,在这篇文章中我提到的解决方法主要集中在 server 层。在下一篇文章中,我会继续和你讨论一些跟 InnoDB 有关的处理方法。

最后,又到了咱们的思考题时间了。

今天,我留给你的课后问题是,你是否碰到过,在业务高峰期须要临时救火的场景?你又是怎么处理的呢?

你能够把你的经历和经验写在留言区,我会在下一篇文章的末尾选取有趣的评论跟你们一块儿分享和分析。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。

8、上期问题时间

前两期我给你留的问题是,下面这个图的执行序列中,为何 session B 的 insert 语句会被堵住。

咱们用上一篇的加锁规则来分析一下,看看 session A 的 select 语句加了哪些锁:

1. 因为是 order by c desc,第一个要定位的是索引 c 上“最右边的”c=20 的行,因此会加上间隙锁 (20,25) 和 next-key lock (15,20]。

2. 在索引 c 上向左遍历,要扫描到 c=10 才停下来,因此 next-key lock 会加到 (5,10],这正是阻塞 session B 的 insert 语句的缘由。

3. 在扫描过程当中,c=20、c=1五、c=10 这三行都存在值,因为是 select *,因此会在主键 id 上加三个行锁。

所以,session A 的 select 语句锁的范围就是:

1. 索引 c 上 (5, 25);
2. 主键索引上 id=1五、20 两个行锁。

这里,我再啰嗦下,你会发现我在文章中,每次加锁都会说明是加在“哪一个索引上”的。由于,锁就是加在索引上的,这是 InnoDB 的一个基础设定,须要你在分析问题的时候要
一直记得。

评论区留言点赞板:

@HuaMax 给出了正确的解释。

@Justin 同窗提了个好问题,<= 究竟是间隙锁仍是行锁?其实,这个问题,你要跟“执行过程”配合起来分析。在 InnoDB 要去找“第一个值”的
时候,是按照等值去找的,用的是等值判断的规则;找到第一个值之后,要在索引内找“下一个值”,对应于咱们规则中说的范围查找。

@信信 提了一个不错的问题,要知道最终的加锁是根据实际执行状况来的。因此,若是一个 select * from … for update 语句,优化器决定使用全表扫
描,那么就会把主键索引上 next-key lock 全加上。

@nero 同窗的问题,提示我须要提醒你们注意,“有行”才会加行锁。若是查询条件没有命中行,那就加 next-key lock。固然,等值判断的时候,
须要加上优化 2(即:索引上的等值查询,向右遍历时且最后一个值不知足等值条件的时候,next-key lock 退化为间隙锁。)。

@小李子、@发条橙子同窗,都提了很好的问题,这期高质量评论不少,你也均可以去看看。

最后,我要为元旦期间还坚持学习的同窗们,点个赞 ^_^

相关文章
相关标签/搜索