<audio id="audio" title="08 | 一个几乎每个系统必踩的坑儿：访问数据库超时" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/71/7f/71c4e6d1e585326f93bfa777d3fb9e7f.mp3"></audio>

你好，我是李玥。

每一个创业公司，它的系统随着公司的发展一起成长的过程中，都难免会发生一些故障或者是事故，严重的会影响业务。搞技术的同学管这个叫：坑儿，分析解决问题的过程，称为：填坑儿。而访问数据库超时这个坑儿，是我见过的被踩的次数最多的一个坑儿，并且这个坑儿还在被不停地踩来踩去。

今天这节课，我和你分享一个典型的数据库超时案例。我也希望你通过和我一起分析这个案例，一是，吸取其中的经验教训，日后不要再踩类似的坑儿；二是，如果遇到类似的问题，你能掌握分析方法，快速地解决问题。最重要的是，学习存储系统架构设计思想，在架构层面限制故障对系统的破坏程度。

## 事故排查过程

我们一起来看一下这个案例。

每一个做电商的公司都梦想着做社交引流，每一个做社交的公司都梦想着做电商将流量变现。我的一个朋友他们公司做社交电商，当年很有前途的一个创业方向，当时也是有很多创业公司在做。

有一天他找到我，让我帮他分析一下他们系统的问题。这个系统从圣诞节那天晚上开始，每天晚上固定十点多到十一点多这个时段，大概瘫痪一个小时左右的时间，过了这个时段系统自动就恢复了。系统瘫痪时的现象就是，网页和App都打不开，请求超时。

这个系统的架构是一个非常典型的小型创业公司的微服务架构。系统的架构如下图：

<img src="https://static001.geekbang.org/resource/image/b7/18/b7edc46baa597b4bd6a25ee5c744b318.png" alt="">

整个系统托管在公有云上，Nginx作为前置网关承接前端所有请求，后端按照业务，划分了若干个微服务分别部署。数据保存在MySQL中，部分数据用Memcached做了前置缓存。数据并没有按照微服务最佳实践的要求，做严格的划分和隔离，而是为了方便，存放在了一起。

这样的存储设计，对于一个业务变化极快的创业公司来说，是合理的。因为它的每个微服务，随时都在随着业务改变，如果做了严格的数据隔离，反而不利于应对需求变化。

听了我朋友对问题的描述，我的第一反应是，每天晚上十点到十一点这个时段，是绝大多数内容类App的访问量高峰，因为这个时候大家都躺在床上玩儿手机。初步判断，这个故障是和访问量有关系的，看下面这个系统每天的访问量的图，可以印证这个判断。

<img src="https://static001.geekbang.org/resource/image/b6/f7/b6bd0e5d44075011680003338ff4bef7.png" alt="">

**基于这个判断，排查问题的重点应该放在那些服务于用户访问的功能上。**比如说，首页、商品列表页、内容推荐这些功能。

在访问量峰值的时候，请求全部超时，随着访问量减少，**系统能自动恢复，基本可以排除后台服务被大量请求打死的可能性**，因为如果进程被打死了，一般是不会自动恢复的。排查问题的重点应该放在MySQL上。观察下面这个MySQL的CPU利用率图，发现问题：

<img src="https://static001.geekbang.org/resource/image/c7/d7/c73f64774a451cc6ce74d6b99535f0d7.png" alt="">

从监控图上可以看出来，故障时段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利用率这个图。

<img src="https://static001.geekbang.org/resource/image/c3/1e/c330355300eca211e5b1fad50709e91e.png" alt="">

把这个图放大后，发现一些规律：

1. CPU利用率，以20分钟为周期，非常规律的波动；
1. 总体的趋势与访问量正相关。

那我们是不是可以猜测一下，对MySQL的CPU利用率的“贡献”来自两部分：红线以下的部分，是正常处理日常访问请求的部分，它和访问量是正相关的。红线以上的部分，来自某一个以20分钟为周期的定时任务，和访问量关系不大。

<img src="https://static001.geekbang.org/resource/image/2e/2f/2ebd674e2f5ef41065ca8eb3589eb62f.png" alt="">

排查整个系统，没有发现有以20分钟为周期的定时任务，继续扩大排查范围，排查周期小于20分钟的定时任务，最终定位了问题。

App的首页聚合了非常多的内容，像精选商品、标题图、排行榜、编辑推荐等等。这些内容包含了很多的数据库查询。当初设计的时候，给首页做了一个整体的缓存，缓存的过期时间是10分钟。但是需求不断变化，首页需要查询的内容越来越多，导致查询首页的全部内容越来越慢。

通过检查日志发现，刷新一次缓存的时间竟然要15分钟。缓存是每隔10分钟整点刷一次，因为10分钟内刷不完，所以下次刷新就推迟到了20分钟之后，这就导致了上面这个图中，红线以上每20分钟的规律波形。

由于缓存刷新慢，也会很多请求无法命中缓存，请求直接穿透缓存打到了数据库上面，这部分请求给上图红线以下的部分，做了很多“贡献”。

找到了问题原因，做针对性的优化，问题很快就解决了。新版本上线之后，再没有出现过“午夜宕机”。

<img src="https://static001.geekbang.org/resource/image/68/7f/6886630263c150d8af3b5a2ff97eb67f.png" alt="">

对比优化前后MySQL的CPU利用率，可以明显地看出优化效果。

## 如何避免悲剧重演

到这里问题的原因找到了，问题也圆满解决了。单从这个案例来看，问题的原因在于，开发人员犯了错误，编写的SQL没有考虑数据量和执行时间，缓存的使用也不合理。最终导致在忙时，大量的查询打到MySQL上，MySQL繁忙无法提供服务。

作为系统的开发人员，对于这次事故，我们可以总结两点经验：

第一，在编写SQL的时候，一定要小心谨慎地仔细评估。先问自己几个问题：

- 你的SQL涉及到的表，它的数据规模是多少？
- 你的SQL可能会遍历的数据量是多少？
- 尽量地避免写出慢SQL。

第二，能不能利用缓存减少数据库查询次数？在使用缓存的时候，还需要特别注意的就是缓存命中率，要尽量避免请求命中不了缓存，穿透到数据库上。

以上两点，是开发人员需要总结的问题。不过你想没想过，谁能保证，整个团队的所有开发人员以后不再犯错误？保证不了吧？那是不是这种的悲剧就无法避免了呢？

其实，还是有办法的。不然，那些大厂，几万开发人员，每天会上线无数的Bug，系统还不得天天宕机？而实际情况是，大厂的系统都是比较稳定的，基本上不会出现全站无法访问这种情况。

靠的是什么？靠的是架构。

优秀的系统架构，可以在一定程度上，减轻故障对系统的影响。针对这次事故，我给这个系统在架构层面，提了两个改进的建议。

第一个建议是，上线一个定时监控和杀掉慢SQL的脚本。这个脚本每分钟执行一次，检测上一分钟内，有没有执行时间超过一分钟（这个阈值可以根据实际情况调整）的慢SQL，如果发现，直接杀掉这个会话。

这样可以有效地避免一个慢SQL拖垮整个数据库的悲剧。即使出现慢SQL，数据库也可以在至多1分钟内自动恢复，避免数据库长时间不可用。代价是，可能会有些功能，之前运行是正常的，这个脚本上线后，就会出现问题。但是，这个代价还是值得付出的，并且，可以反过来督促开发人员更加小心，避免写出慢SQL。

第二个建议是，做一个简单的静态页面的首页作为降级方案，只要包含商品搜索栏、大的品类和其他顶级功能模块入口的链接就可以了。在Nginx上做一个策略，如果请求首页数据超时的时候，直接返回这个静态的首页作为替代。这样后续即使首页再出现任何的故障，也可以暂时降级，用静态首页替代。至少不会影响到用户使用其他功能。

这两个改进建议都是非常容易实施的，不需要对系统做很大的改造，并且效果也立竿见影。

当然，这个系统的存储架构还有很多可以改进的地方，比如说对数据做适当的隔离，改进缓存置换策略，做数据库主从分离，把非业务请求的数据库查询迁移到单独的从库上等等，只是这些改进都需要对系统做比较大的改动升级，需要从长计议，在系统后续的迭代过程中逐步地去实施。

## 小结

这节课，我和你一起分析了一个由于慢SQL导致的全站故障的案例。在“破案”的过程中，有一些很有用的经验，这些经验对于后续你自己“破案”时会非常有用。比如说：

1. 根据故障时段在系统忙时，推断出故障是跟支持用户访问的功能有关。
1. 根据系统能在流量峰值过后自动恢复这一现象，排除后台服务被大量请求打死的可能性。
1. 根据CPU利用率曲线的规律变化，推断出可能和定时任务有关。

在故障复盘阶段，除了对故障问题本身做有针对性的预防和改进以外，更重要的是，在系统架构层面进行改进，让整个系统更加健壮，不至于因为某一个小的失误，就导致全站无法访问。

我给系统提出的第一个自动杀慢SQL的建议，它的思想是：系统的关键部分要有自我保护机制，避免外部的错误影响到系统的关键部分。第二个首页降级的建议，它的思想是：当关键系统出现故障的时候，要有临时的降级方案，尽量减少故障带来的影响。

这些架构上的改进，虽然并不能避免故障，但是可以很大程度上减小故障的影响范围，减轻故障带来的损失，希望你能仔细体会，活学活用。

## 思考题

课后请你想一下，以你个人的标准，什么样的SQL算是慢SQL？如何才能避免写出慢SQL？欢迎你在留言区与我交流互动。

感谢你的阅读，如果你觉得今天的内容对工作有所帮助，也欢迎把它分享给你的朋友。
