Contents

数据库连接池该设置多大?记一次由连接池引发的事故。

本文主要记录了一次由 Redis 连接池设置不当引发的事故及其排查和解决过程,同时也记录了数据库连接池的正确设置姿势。

1. 结论

内存数据库

像 Redis、Memcache 等内存数据库,因为所有请求直接在内存中完成所以处理速度很快。

具体计算公式如下:

最大连接数 = 最大并发量 / (1000ms / 每次请求耗时ms)

所以我们需要知道集群最大并发量和应用中每次请求Redis来回的耗时。

Redis官网 提供了压测工具。

root@redis://usr/sbin# redis-benchmark -q -n 100000
PING_INLINE: 66269.05 requests per second
PING_BULK: 68634.18 requests per second
SET: 68399.45 requests per second
GET: 67567.57 requests per second
INCR: 68259.38 requests per second
LPUSH: 67704.80 requests per second
RPUSH: 68259.38 requests per second
LPOP: 68399.45 requests per second
RPOP: 68399.45 requests per second
SADD: 68399.45 requests per second
HSET: 67567.57 requests per second
SPOP: 66844.91 requests per second
LPUSH (needed to benchmark LRANGE): 50505.05 requests per second
LRANGE_100 (first 100 elements): 65146.58 requests per second
LRANGE_300 (first 300 elements): 67204.30 requests per second
LRANGE_500 (first 450 elements): 67340.07 requests per second
LRANGE_600 (first 600 elements): 67294.75 requests per second
MSET (10 keys): 61576.36 requests per second

在我自己的小霸王服务器上都能用 六七万并发,所以单节点10W妥妥的有了。

假设每次请求:来回网络延迟 + redis 处理耗时 为 1ms。

那么 最大连接数 = 10W / (1000ms / 1ms) = 100,即同时用 100个连接就能达到 Redis QPS峰值,发挥出全部性能。

建议:最好能自己测试一下,实在测不了建议设置在 200 作用。

一般 Redis可用连接数都是按W计算的,稍微分配大一点也没什么问题,如果太小了就会无法发挥出 Redis 全部性能。

磁盘数据库

像 MySQL、PostgreSQL、Oracle 这样将数据存在磁盘上的数据库,由于磁盘I/O的存在,请求处理比较慢,连接池不能设置得像内存数据库那么大。

PostgreSQL 提供了一个公式:

connections = ((core_count * 2) + effective_spindle_count)

如果说你的服务器 CPU 是 4核 的,连接池大小应该为 ((4*2)+1)=9。取个整, 我们就设置为 10 吧。

具体测试数据:HikariCP 测试数据

2. 问题背景

最近处理了一个线上事故,就是由数据库连接池设置不当引发的。

具体为,某一天的某一个时间点,客户开始反应接口响应很慢:

  • 大部分请求会直接阻塞到超时。
  • 小部分请求是响应比较慢;

然后就开始了排查之旅。

3. 排查过程

寻找问题

发现问题后,立马查看了监控、日志、链路追踪信息,具体如下:

服务器监控

  • CPU、内存 飙升到 80% 左右,CPU 平常只有 20~30%在,而这些服务基本上是不会使用到太多内存的。
  • 负载直接到了 60,还只是4核CPU的机器,可以简单理解为:一般负载超过CPU核数就比较高了。

https://github.com/lixd/blog/raw/master/images/redis/connections/ecs-cpu.png

https://github.com/lixd/blog/raw/master/images/redis/connections/ecs-mem.png

https://github.com/lixd/blog/raw/master/images/redis/connections/ecs-load.png

日志

  • 服务A:调用服务B超时
  • 服务B:调用服务C超时
  • 服务C:调用服务N超时 + 小部分 redis: connection pool timeout。
  • …省略部分服务
  • 服务N:大量 redis: connection pool timeout

链路追踪

  • 大部分接口都是到了超时时间(3S)才返回。

定位问题

将各个服务日志连起来查看后,问题都指向了 Redis,于是立马去查了 Redis 监控,结果发现一切正常;

然后就去查了 redis 驱动的源码,看下这个错误提示是什么情况下出现的:

redis: connection pool timeout

相关代码如下:

// Get 函数用于从Pool取一个 conn
func (p *ConnPool) Get() (*Conn, error) {
	if p.closed() {
		return nil, ErrClosed
	}

	err := p.waitTurn()
	if err != nil {
		return nil, err
	}
// 省略
}
func (p *ConnPool) waitTurn() error {
   select {
   case p.queue <- struct{}{}:
      return nil
   default:
      timer := timers.Get().(*time.Timer)
      timer.Reset(p.opt.PoolTimeout)

      select {
      case p.queue <- struct{}{}:
         if !timer.Stop() {
            <-timer.C
         }
         timers.Put(timer)
         return nil
      case <-timer.C:
         timers.Put(timer)
         atomic.AddUint32(&p.stats.Timeouts, 1)
         return ErrPoolTimeout
      }
   }
}

其中 waitTurn 函数返回的 ErrPoolTimeout 定义如下:

var ErrPoolTimeout = errors.New("redis: connection pool timeout")

那么问题就很明显了,从 Redis 连接池里拿 conn 的时候超时,原因可能有下面几个:

  • 1)连接池设置太小,连接被其他Goroutine拿去使用了,没有来得及归还;
  • 2)Redis 性能瓶颈,导致每次请求要很长时间;
  • 3)网络延迟过大,同样有可能导致每次请求要很长时间。

由于 Redis 监控一切正常,同时应用是在内网环境,网络延迟也特别低,所以是第一点可能性更高。

然后立马查看了配置文件,果然,Redis 连接池只分配了 10个,然后超时时间居然是 10秒。将 Redis 连接池数量调大之后,一切问题都解决了。

具体分析

既然知道问题是 Redis 连接池设置太小了,那么一切都解释得通了。

  • 1)连接池过小导致大部分请求阻塞在获取连接这一步,一直阻塞到超时,打印错误日志并返回;
  • 2)该超时时间 10s 远大于设置的接口超时时间 3s,所以在阻塞 3s 后时候上游接口已经超时返回了;
  • 3)最终导致每个接口需要阻塞3s才返回。
  • 4)提现在服务器上就是:CPU、内存、负载飙升。

之前由于并发量不是很高,10 个连接池也刚好能处理过来,最近并发量上升后该问题就暴露出来了。

为什么有小部分请求能响应?

由于是部署在 Kubernetes 集群中的,同时为每个 Pod 配置了 资源上限request 和 limit。cpu 都还好,到达上限后只会让应用跑得慢一点。但是内存就不一样了,超过之后直接 OOM,Pod 被强制 KIll 掉。

Pod 重启后最初到达的部分请求可以获取到 Redis 连接,能正常返回。后续请求只能阻塞到超时了。

补上两个 Redis 的 QPS 统计信息:

调整连接池大小前:

https://github.com/lixd/blog/raw/master/images/redis/connections/redis-qps-before.png

调整连接池大小后:

https://github.com/lixd/blog/raw/master/images/redis/connections/redis-qps-after.png

可以看到,性能确实是被连接池给限制了,所以出现了这次事故。

4. 小结

  • 1)连接池对性能影响很大,不能随意设置。
  • 2)K8s 中 Pod 资源限制需要合理分配,特别是内存,可以先设置大一点,然后根据统计数据逐渐调整到合适的值。
  • 3)APM 系统很重要
    • logging:日志收集系统让你在出问题时不用去每台服务器上慢慢翻日志。
    • tracing:链路追踪系统可以让你清楚的看到系统间各个服务的调用情况。
    • metrics:监控系统各个指标,便于分析问题。
  • 4)配置中心:微服务一定要有一个配置中心,统一存放配置,否则很难维护。

5. 参考

https://redis.io/topics/benchmarks

https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing

https://zhuanlan.zhihu.com/p/105845455