You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
12 KiB
Markdown

2 years ago
# 08 | 一个几乎每个系统必踩的坑儿:访问数据库超时
你好,我是李玥。
每一个创业公司,它的系统随着公司的发展一起成长的过程中,都难免会发生一些故障或者是事故,严重的会影响业务。搞技术的同学管这个叫:坑儿,分析解决问题的过程,称为:填坑儿。而访问数据库超时这个坑儿,是我见过的被踩的次数最多的一个坑儿,并且这个坑儿还在被不停地踩来踩去。
今天这节课,我和你分享一个典型的数据库超时案例。我也希望你通过和我一起分析这个案例,一是,吸取其中的经验教训,日后不要再踩类似的坑儿;二是,如果遇到类似的问题,你能掌握分析方法,快速地解决问题。最重要的是,学习存储系统架构设计思想,在架构层面限制故障对系统的破坏程度。
## 事故排查过程
我们一起来看一下这个案例。
每一个做电商的公司都梦想着做社交引流,每一个做社交的公司都梦想着做电商将流量变现。我的一个朋友他们公司做社交电商,当年很有前途的一个创业方向,当时也是有很多创业公司在做。
有一天他找到我让我帮他分析一下他们系统的问题。这个系统从圣诞节那天晚上开始每天晚上固定十点多到十一点多这个时段大概瘫痪一个小时左右的时间过了这个时段系统自动就恢复了。系统瘫痪时的现象就是网页和App都打不开请求超时。
这个系统的架构是一个非常典型的小型创业公司的微服务架构。系统的架构如下图:
![](https://static001.geekbang.org/resource/image/b7/18/b7edc46baa597b4bd6a25ee5c744b318.png)
整个系统托管在公有云上Nginx作为前置网关承接前端所有请求后端按照业务划分了若干个微服务分别部署。数据保存在MySQL中部分数据用Memcached做了前置缓存。数据并没有按照微服务最佳实践的要求做严格的划分和隔离而是为了方便存放在了一起。
这样的存储设计,对于一个业务变化极快的创业公司来说,是合理的。因为它的每个微服务,随时都在随着业务改变,如果做了严格的数据隔离,反而不利于应对需求变化。
听了我朋友对问题的描述我的第一反应是每天晚上十点到十一点这个时段是绝大多数内容类App的访问量高峰因为这个时候大家都躺在床上玩儿手机。初步判断这个故障是和访问量有关系的看下面这个系统每天的访问量的图可以印证这个判断。
![](https://static001.geekbang.org/resource/image/b6/f7/b6bd0e5d44075011680003338ff4bef7.png)
**基于这个判断,排查问题的重点应该放在那些服务于用户访问的功能上。**比如说,首页、商品列表页、内容推荐这些功能。
在访问量峰值的时候,请求全部超时,随着访问量减少,**系统能自动恢复,基本可以排除后台服务被大量请求打死的可能性**因为如果进程被打死了一般是不会自动恢复的。排查问题的重点应该放在MySQL上。观察下面这个MySQL的CPU利用率图发现问题
![](https://static001.geekbang.org/resource/image/c7/d7/c73f64774a451cc6ce74d6b99535f0d7.png)
从监控图上可以看出来故障时段MySQL的CPU利用率一直是100%。这种情况下MySQL基本上处于一个不可用的状态执行所有的SQL都会超时。
MySQL这种CPU利用率高的现象绝大多数情况都是由慢SQL导致的所以我们优先排查慢SQL。MySQL和各大云厂商提供的RDS都能提供慢SQL日志分析慢SQL日志是查找类似问题原因最有效的方法。
一般来说慢SQL的日志中会有这样一些信息SQL、执行次数、执行时长。通过分析慢SQL找问题并没有什么标准的方法主要还是依靠经验。
首先你需要知道的一点是当数据库非常忙的时候它执行任何一个SQL都很慢。所以并不是说慢SQL日志中记录的这些慢SQL都是有问题的SQL。大部分情况下导致问题的SQL只是其中的一条或者几条。不能简单地依据执行次数和执行时长进行判断但是单次执行时间特别长的SQL仍然是应该重点排查的对象。
通过分析这个系统的慢SQL日志首先找到了一个特别慢的SQL。
这个SQL支撑的功能是一个红人排行榜这个排行榜列出粉丝数最多的TOP10红人。
```
select fo.FollowId as vid, count(fo.id) as vcounts
from follow fo, user_info ui
where fo.userid = ui.userid
and fo.CreateTime between
str_to_date(?, '%Y-%m-%d %H:%i:%s')
and str_to_date(?, '%Y-%m-%d %H:%i:%s')
and fo.IsDel = 0
and ui.UserState = 0
group by vid
order by vcounts desc
limit 0,10
```
**这种排行榜的查询,一定要做缓存**。在这个案例中,排行榜是新上线的功能,可能忘记做缓存了,通过增加缓存可以有效地解决问题。
给排行榜增加了缓存后,新版本立即上线。本以为问题就此解决了,结果当天晚上,系统仍然是一样的现象,晚高峰各种请求超时,页面打不开。
再次分析慢SQL日志排行榜的慢SQL不见了说明缓存生效了。日志中的其他慢SQL查询次数和查询时长分布的都很均匀也没有看出明显写的有问题的SQL。
回过头来再看MySQL CPU利用率这个图。
![](https://static001.geekbang.org/resource/image/c3/1e/c330355300eca211e5b1fad50709e91e.png)
把这个图放大后,发现一些规律:
1. CPU利用率以20分钟为周期非常规律的波动
2. 总体的趋势与访问量正相关。
那我们是不是可以猜测一下对MySQL的CPU利用率的“贡献”来自两部分红线以下的部分是正常处理日常访问请求的部分它和访问量是正相关的。红线以上的部分来自某一个以20分钟为周期的定时任务和访问量关系不大。
![](https://static001.geekbang.org/resource/image/2e/2f/2ebd674e2f5ef41065ca8eb3589eb62f.png)
排查整个系统没有发现有以20分钟为周期的定时任务继续扩大排查范围排查周期小于20分钟的定时任务最终定位了问题。
App的首页聚合了非常多的内容像精选商品、标题图、排行榜、编辑推荐等等。这些内容包含了很多的数据库查询。当初设计的时候给首页做了一个整体的缓存缓存的过期时间是10分钟。但是需求不断变化首页需要查询的内容越来越多导致查询首页的全部内容越来越慢。
通过检查日志发现刷新一次缓存的时间竟然要15分钟。缓存是每隔10分钟整点刷一次因为10分钟内刷不完所以下次刷新就推迟到了20分钟之后这就导致了上面这个图中红线以上每20分钟的规律波形。
由于缓存刷新慢,也会很多请求无法命中缓存,请求直接穿透缓存打到了数据库上面,这部分请求给上图红线以下的部分,做了很多“贡献”。
找到了问题原因,做针对性的优化,问题很快就解决了。新版本上线之后,再没有出现过“午夜宕机”。
![](https://static001.geekbang.org/resource/image/68/7f/6886630263c150d8af3b5a2ff97eb67f.png)
对比优化前后MySQL的CPU利用率可以明显地看出优化效果。
## 如何避免悲剧重演
到这里问题的原因找到了问题也圆满解决了。单从这个案例来看问题的原因在于开发人员犯了错误编写的SQL没有考虑数据量和执行时间缓存的使用也不合理。最终导致在忙时大量的查询打到MySQL上MySQL繁忙无法提供服务。
作为系统的开发人员,对于这次事故,我们可以总结两点经验:
第一在编写SQL的时候一定要小心谨慎地仔细评估。先问自己几个问题
* 你的SQL涉及到的表它的数据规模是多少
* 你的SQL可能会遍历的数据量是多少
* 尽量地避免写出慢SQL。
第二,能不能利用缓存减少数据库查询次数?在使用缓存的时候,还需要特别注意的就是缓存命中率,要尽量避免请求命中不了缓存,穿透到数据库上。
以上两点,是开发人员需要总结的问题。不过你想没想过,谁能保证,整个团队的所有开发人员以后不再犯错误?保证不了吧?那是不是这种的悲剧就无法避免了呢?
其实还是有办法的。不然那些大厂几万开发人员每天会上线无数的Bug系统还不得天天宕机而实际情况是大厂的系统都是比较稳定的基本上不会出现全站无法访问这种情况。
靠的是什么?靠的是架构。
优秀的系统架构,可以在一定程度上,减轻故障对系统的影响。针对这次事故,我给这个系统在架构层面,提了两个改进的建议。
第一个建议是上线一个定时监控和杀掉慢SQL的脚本。这个脚本每分钟执行一次检测上一分钟内有没有执行时间超过一分钟这个阈值可以根据实际情况调整的慢SQL如果发现直接杀掉这个会话。
这样可以有效地避免一个慢SQL拖垮整个数据库的悲剧。即使出现慢SQL数据库也可以在至多1分钟内自动恢复避免数据库长时间不可用。代价是可能会有些功能之前运行是正常的这个脚本上线后就会出现问题。但是这个代价还是值得付出的并且可以反过来督促开发人员更加小心避免写出慢SQL。
第二个建议是做一个简单的静态页面的首页作为降级方案只要包含商品搜索栏、大的品类和其他顶级功能模块入口的链接就可以了。在Nginx上做一个策略如果请求首页数据超时的时候直接返回这个静态的首页作为替代。这样后续即使首页再出现任何的故障也可以暂时降级用静态首页替代。至少不会影响到用户使用其他功能。
这两个改进建议都是非常容易实施的,不需要对系统做很大的改造,并且效果也立竿见影。
当然,这个系统的存储架构还有很多可以改进的地方,比如说对数据做适当的隔离,改进缓存置换策略,做数据库主从分离,把非业务请求的数据库查询迁移到单独的从库上等等,只是这些改进都需要对系统做比较大的改动升级,需要从长计议,在系统后续的迭代过程中逐步地去实施。
## 小结
这节课我和你一起分析了一个由于慢SQL导致的全站故障的案例。在“破案”的过程中有一些很有用的经验这些经验对于后续你自己“破案”时会非常有用。比如说
1. 根据故障时段在系统忙时,推断出故障是跟支持用户访问的功能有关。
2. 根据系统能在流量峰值过后自动恢复这一现象,排除后台服务被大量请求打死的可能性。
3. 根据CPU利用率曲线的规律变化推断出可能和定时任务有关。
在故障复盘阶段,除了对故障问题本身做有针对性的预防和改进以外,更重要的是,在系统架构层面进行改进,让整个系统更加健壮,不至于因为某一个小的失误,就导致全站无法访问。
我给系统提出的第一个自动杀慢SQL的建议它的思想是系统的关键部分要有自我保护机制避免外部的错误影响到系统的关键部分。第二个首页降级的建议它的思想是当关键系统出现故障的时候要有临时的降级方案尽量减少故障带来的影响。
这些架构上的改进,虽然并不能避免故障,但是可以很大程度上减小故障的影响范围,减轻故障带来的损失,希望你能仔细体会,活学活用。
## 思考题
课后请你想一下以你个人的标准什么样的SQL算是慢SQL如何才能避免写出慢SQL欢迎你在留言区与我交流互动。
感谢你的阅读,如果你觉得今天的内容对工作有所帮助,也欢迎把它分享给你的朋友。