数据库连接池的大小设置成多大合适呢?
场景:
对 Oracle 数据库进行了压力测试,模拟 9600 个并发线程来操作数据库,每两次数据库操作之间 sleep 550ms
- 线程池大小为 2048,每个请求要在连接池队列里等待 33ms,获得连接之后,执行SQL需要耗时77ms, CPU 消耗维持在 95% 左右;
- 连接池的大小降低到 1024,其他测试参数不变,获取连接等待时长基本不变,但是 SQL 的执行耗时降低了!
- 连接池的大小降低到 96,并发数等其他参数不变,每个请求在连接池队列中的平均等待时间为 1ms, SQL 执行耗时为 2ms.
仅仅只是将数据库连接池的大小降低了,就把之前平均 100ms 响应时间缩短到了 3ms,吞吐量指数级上升。
why
Nginx 内部仅仅使用了 4 个线程,其性能就大大超越了 100 个进程的 Apache HTTPD
单核 CPU 的计算机也能“同时”运行着数百个线程。实际,这只不过是操作系统快速切换时间片。
一核 CPU同一时刻只能执行一个线程,然后操作系统切换上下文,CPU 核心快速调度,执行另一个线程的代码,不停反复,给我们造成了所有进程同时运行假象。
其实,在一核 CPU 的机器上,顺序执行A和B永远比通过时间分片切换“同时”执行A和B要快。一旦线程的数量超过了 CPU 核心的数量,再增加线程数系统就只会更慢,而不是更快,因为这里涉及到上下文切换耗费的额外的性能。
其它考虑因素
数据库的性能瓶颈时,大致可归为三类:
- CPU
- 磁盘 IO
- 网络 IO
假设我们不考虑磁盘 IO 和网络 IO,在一个 8 核的服务器上,数据库连接数/线程数设置为 8 能够提供最优的性能,如果再增加连接数,反而会因为上下文切换导致性能下降。
数据库通常把数据存储在磁盘上,而磁盘通常是由一些旋转着的金属碟片和一个装在步进马达上的读写头组成的。读/写头同一时刻只能出现在一个位置,当它需要再次执行读写操作时,它必须“寻址”到另外一个位置才能完成任务。所以说,多次读写增加了额外的寻址耗时和旋转耗时,读写头需要等待磁盘碟片上的目标数据“旋转到位”才能进行读写操作。
使用缓存当然是能够提升性能的,但上述原理仍然适用。
在这段("I/O等待")时间内,线程是处于“阻塞”等待状态,此时操作系统可以将这个空闲的CPU 核心用于服务其他线程。
当你的线程处理的是 I/O 密集型业务时,便可以让线程/连接数设置的比 CPU核心大一些,这样就能够在同样的时间内,完成更多的工作,提升吞吐量。
大小设置成多少合适呢?
取决于磁盘,如果你使用的是 SSD 固态硬盘,它不需要寻址,也不需要旋转碟片。
无需寻址和没有旋回耗时的确意味着更少的阻塞,所以更少的线程(更接近于CPU核心数)会发挥出更高的性能。只有当阻塞密集时,更多的线程数才能发挥出更好的性能。
网络 IO
通过以太网接口读写数据时也会造成阻塞,10G带宽会比1G带宽的阻塞耗时少一些,而 1G 带宽又会比 100M 带宽的阻塞少一些。通常情况下,我们把网络 IO 放在第三顺位来考虑,然而有些人会在性能计算中忽略网络 IO 带来的影响。
上图是 PostgreSQL 的基准性能测试数据,从图中我们可以看到,TPS 在连接数达到 50 时开始变缓。回过头来想下,在上面 Oracle 的性能测试视频中,测试人员们将连接数从 2048 降到了 96,实际上 96 还是太高了,除非你的服务器 CPU 核心数有 16 或 32。
连接数计算公式
连接数 = ((核心数 * 2) + 有效磁盘数)
核心数不应包含超线程(hyper thread),,如果热点数据全被缓存了,那么有效磁盘数实际是0,随着缓存命中率的下降,有效磁盘数也逐渐趋近于实际的磁盘数。
注意,该公式仅适用于机械硬盘
一个小连接池和一个等待连接的线程队列
假设 10000 个并发访问,一个大小为 10 数据库连接池,然后让剩下的业务线程都在队列里等待。
连接池中的连接数量大小应该设置成:数据库能够有效同时进行的查询任务数(通常情况下来说不会高于 2*CPU核心数)。
额外注意:
- 若系统同时混合了长事务和短事务,不该死套公式。应该是创建两个连接池,一个服务于长事务,一个服务于"实时"查询,也就是短事务。
- 一个系统执行一个任务队列,业务上要求同一时间内只允许执行一定数量的任务,这时,我们就应该让并发任务数去适配连接池连接数,而不是连接数大小去适配并发任务数。