线上数据库连接池爆满问题排查

Sherwin.Wei Lv7

线上数据库连接池爆满问题排查

回答重点

如果出现了线上连接池爆满的情况,一般情况需要先“止损”,也就是重启服务。

现在一般都会用云上的服务,会有监控,对应监控能查到历史数据库的情况,包括连接池的占用情况。重启后我们快速分析定位事故反生之前一段时间 SQL 的执行情况,连接池的占用情况。

查看当时是否有大量的突发请求,例如做了运营活动,那么很可能请求数上来会导致数据库压力过大,处理缓慢使得连接池占用久导致连接数满了。这种情况应该之前预先压测,并且通过限流等手段控制服务的整体压力。

查看是否有慢 SQL 导致长时间连接的占用,需要特别注意近期上线的功能,快速定位查找可能影响的功能点进行代码修复。

查看连接池配置是否合理,大部分默认的连接池的最大连接数可能就只有 8 或者 10,这种情况下需要适当提高最大连接数,具体连接数需要根据具体业务压测后配置。

常见数据库查询性能优化

  • 查看慢查询日志:分析数据库慢查询日志,查看是否有查询性能较差的 SQL 语句,导致连接长期占用。
  • 优化查询:对于慢查询,可以优化 SQL、增加索引、减少全表扫描等,提升查询效率,减少数据库连接的占用时间。
  • 数据库锁竞争:确认是否有长时间持有锁的 SQL 查询,避免长时间的锁竞争导致连接池中的连接无法释放。

面对突发流量应对措施

  • 限流:限制每秒请求的数量,避免瞬时流量过大导致连接池耗尽。
  • 请求排队:通过消息队列等将请求异步化,减轻数据库直接访问的压力。可以将需要访问数据库的操作转到后台处理,而不是即时返回。(业务上需要做开关,开关打开后就异步处理,正常情况下同步处理)
  • 服务降级:对于某些非核心功能,考虑返回默认值或缓存数据,避免直接查询数据库,减轻高峰期的压力

扩展知识

一次简单的案例

某个早晨,我刚到公司,就接到钉钉报警:“线上连接池爆了!”,当时直接一个重启大法,一切恢复正常。

那么到底是什么原因呢?


重启之后应用确实是恢复了正常,说明是某个突发的情况导致连接池爆了

既然线上已经止损了,我们就可以安心的来排查排查这个问题。

连接池满无非就是…连接都被占用了,一般有两种情况会导致连接池满了:

  1. 很多长事务,执行的慢,导致长时间占用连接,然后别的请求都hang住了
  2. 很多短事务,执行的快,但是并发太高,即使时间短,但是架不住量大,得得得得得的就堵住了

这次事故产生的原因就是第二种情况!

DBA 当时发来一条 sql,说就是这条 sql 被频繁地执行。

我从 kibana 上面搜索了一下这条语句的执行情况,发现其实不止早上8点多有高峰,凌晨竟然有更高峰!

企业微信截图_2e73d1f2-5019-441b-96dd-3efb2c1f483a.png

然后再看看这个 sql 执行的频率,这时间排的整整齐齐的,一丝都不带变的。

企业微信截图_5af94196-06dd-4f86-a4fb-434730a7db26.png

并且这条 sql 也不复杂,就是一个带主键的单表查询,表也不会很大。

所以我断定这次突发的情况就是:高并发下频繁地请求数据库导致的

那现在问题来了,为啥在某个时候会频繁的请求这条 sql ?是人性的扭曲还是道德的沦丧?

我直接定位了这条 sql 的请求代码,发现这个查询其实是先走缓存,缓存找不到才会去查数据库

我仔细看了看代码,确定了这个业务逻辑是用来给前端展示任务进度的。

就是后台会跑一个任务,前端需要实时展示一个进度条,这样用户使用的时候才不会干着急。

我和前端同事确认了一下,按理前端应该 2s 才会请求一次进度,所以即使是直接查询数据库,也不至于一个用户如此高频地调用请求。

所以我先甩了个锅,你前端代码有问题,并没有遵循 2s 去查,快去看看代码!

然后我继续查询为啥这个缓存会消失的问题。

按理来说,具体逻辑是这样的:

  1. 用户触发任务,会塞入进度为 0 的值至缓存中,同时更新任务的状态至数据库。
  2. 缓存的过期时间我设置了一天
  3. 后台任务在执行的时候会实时更新进度
  4. 前端调用接口查询进度

按照这个逻辑,缓存不可能在查询的时候不存在的啊!

然后我就开始疑神疑鬼了,难道是 redis 抽了把这个 key 删了?不至于啊看了看缓存负载也不高。

难道是调用的 redis client 接口有 bug?过期时间没给我整对?

然后我模拟了一下,执行了一次任务,查询了一下缓存里的进度,我直接好家伙!

企业微信截图_51de9ce4-bf60-423d-bea6-f037f74ea6c1.png

可以看到过期时间竟然只有 5 分钟?我明明设置的是一天啊!

我兴致勃勃地深入了调用的 redis client 源码,想着好家伙,来素材了难道!

看了半天,我反应了过来,觉得不可能有问题,要有问题不应该只有这个功能会出现这个情况,别的早都爆了。

所以我又把目光移向了更新进度的那个后台任务!是不是这个 b 把缓存的过期时间改了?

由于这个任务不是我写的,于是我就去找了负责这个任务的同事,果不其然!

他执行任务更新进度的时候,过期时间设置的值都为 5 分钟 !

伪代码如下:

1
2
3
4
while(任务没结束) {
执行逻辑
更新缓存中的任务进度++,设置过期时间为5分钟
}

他这样的设置过期时间也没毛病,因为当任务结束了也就是进度到 100% 了之后,不会再有获取任务进度的行为,所以 5 分钟就让它过期可以的。

而我之所以设置 1 天,是想着如果发生点啥问题可以保留一下案发现场的数据看看。

那按照这个逻辑看下来,应该是没问题的,为啥会发生这个情况?

我猜想了一下,心里有了个 B 树。

我问他这个任务是不是有可能会有阻塞的情况,导致超过 5 分钟才会更新缓存,这样在这个阻塞时间内,前 5 分钟更新的缓存就过期了,缓存里就没这个键了,此时的查询就会直接命中数据库,也就重现了上面的那个情况了!

再来看下伪代码:

1
2
3
4
5
while(任务没结束) {
//有时候执行的时长超过了5分钟,缓存已过期
执行逻辑
更新缓存中的任务进度++,设置过期时间为5分钟
}

他回答到有可能,因为这个任务会频繁调用第三方的接口,并且会包含一些很复杂的合并逻辑,所以有可能超过5分钟才会继续更新缓存。

好了,终于破案了!这也解释了为什么这个状况是偶发的,因为第三方接口是不是会不稳定,就可能阻塞超过了五分钟。

总结一下:

  1. 前端代码有 bug ,导致频繁查询接口(相当于攻击的频率了)
  2. 后端任务更新缓存的过期时间为 5 分钟,但由于业务比较复杂且第三方接口不稳定,可能处理逻辑耗费的时间为 6 分钟,导致上一次更新的缓存已经过期,使得中间有一段缓存空缺的时间
  3. 由于缓存空缺,且前端频繁查询,两者合一导致频繁查询数据库
  4. 所以连接池爆了

解决办法也很简单:让前端排查下代码的问题然后修复下,并且也将缓存过期的时间延长至10分钟,一阶段任务的执行时间几乎不可能超过 10 分钟。

最后

好了,讲完了,这次的问题不难排查,根据对应的现象定位到相应的代码,然后再进行前后端业务场景的分析即可。

其实所有的排查都是如此,先止损(看情况不妙就得先重启),定位代码,分析情况。

有些难排查的得打 log,有些难重现的还得持续观察好几天。

虽说出了问题总是不好的,但是大家要把握还这样的机会,及时记录,这都是以后的谈资

Comments