首页 关于数据库连接池大小的设置
文章
取消

关于数据库连接池大小的设置

关于数据库连接池大小的设置


配置数据库连接池大小是开发人员经常出错的问题。在配置连接池时需要理解的原则有几个,可能是反直觉的。


1. 1 万个客户端同时访问

想象一下,你有一个网站虽然可能不是 Facebook 规模,但仍然经常有 10,000 个用户同时发出数据库请求 - 每秒约占 20,000 个交易。你的连接池应该有多大?你可能会感到惊讶的是,这个问题不是多大,而是多么小!

我们可以在没有任何其他更改的情况下,单独减少连接池大小会将应用程序的响应时间从约 100 毫秒减少到约 2 毫秒,这可是超过50倍的改进。

2. 单纯修改连接池为什么可以提高程序响应时间呢?

我们似乎最近在计算的其他部分已经理解,越少越好。为什么只有 4 个线程的 nginx Web 服务器可以大大超过 100 个进程的 Apache Web 服务器?如果你回想一下计算机科学 101,这不是很明显吗?

即使是具有一个 CPU 内核的计算机也可以“同时”支持数十个或数百个线程。但是我们都应该知道,这只是操作系统的一个技巧,虽然是时间切片的神奇之处。实际上,单个核心一次只能执行一个线程; 然后操作系统切换上下文,并且该核心执行另一个线程的代码,依此类推。给定单个 CPU 资源是一个基本的计算法则,顺序执行 A 和 B 总是比通过时间切片“同时” 执行 A 和 B 更快。一旦线程数超过 CPU 核心数,通过添加更多线程就会变得更慢,而不是更快。

这几乎是真的……

3. 面对有限资源

它不像上面说的那么简单,但它很接近。还有一些其他因素在起作用。当我们查看数据库的主要瓶颈是什么时,它们可以归纳为三个基本类别:CPU,磁盘,网络。我们可以在那里添加内存,但与磁盘和网络相比,带宽有几个数量级的差异。

如果我们忽略磁盘和网络,那将很简单。在具有 8个 计算核心的服务器上,将连接数设置为8将提供最佳性能,并且由于上下文切换的开销,超出此范围的任何内容都将开始减慢。但我们不能忽视磁盘和网络。数据库通常将数据存储在磁盘上传统上由金属旋转板组成,读/写头安装在步进电机驱动臂上。读/写磁头一次只能在一个地方(读/写单个查询的数据),并且必须“寻找”到新位置以读取/写入不同查询的数据。因此,存在寻道时间成本,以及旋转成本,其中磁盘必须等待数据在盘片上“再次出现”以进行读/写。缓存当然有帮助,但原则仍然适用。

在此期间(“I/O 等待”),连接/查询/线程被“阻塞”等待磁盘。在此期间,操作系统可以通过为另一个线程执行更多代码来更好地使用该 CPU 资源。因此,由于线程在 I/O 上被阻塞,我们实际上可以通过使大量连接/线程大于物理计算核心数来完成更多工作。

还有多少?我们将看到。还有多少还取决于磁盘子系统的问题,因为较新的 SSD 驱动器没有“寻道时间”成本或旋转因素来处理。不要误以为“ SSD 速度更快,因此我可以拥有更多线程”。那恰好是向后 180 度。更快,没有搜索,没有旋转延迟意味着更少的阻塞,因此更少的线程[更接近核心数量]将比更多线程更好地执行。 当阻塞创建执行机会时,更多线程仅执行得更好。

网络类似于磁盘。通过以太网接口通过线路写出数据也会在发送/接收缓冲区填满和停止时引入阻塞。一个 10-Gig 接口将比千兆以太网停止,它将停止不到 100 兆位。但就资源阻塞而言,网络排名第三,有些人经常在计算中忽略它。

这是上下文切换的一个图表

您可以在上面的 PostgreSQL 基准测试中看到,TPS 速率开始在大约 50 个连接处变平。在 Oracle 中,连接从 2048 下降到 96,我们会说即使 96 也可能太高,除非你正在看 16 或 32 核的 CPU。

4. 公式

下面的公式由 PostgreSQL 项目提供,作为起点,但我们相信它将在很大程度上适用于数据库。您应该测试您的应用程序,即模拟预期的负载,并在此起点周围尝试不同的池设置:

connections = ((core_count * 2) + effective_spindle_count)

多年来在很多基准测试中表现良好的公式是为了获得最佳吞吐量,活动连接的数量应该在某处接近 ((core_count * 2) + effective_spindle_count)。 核心数不应包括超线程,即使启用了超线程也是如此。 如果有效的主核数为零活动数据集已完全缓存,并接近实际的主轴数随着缓存命中率下降。 ……到目前为止还没有任何分析配方与 SSD 的配合效果如何。

猜猜那意味着什么?你的带有一个硬盘的小型 4 核 i7 服务器应运行以下连接池: 9 = ((4 * 2) + 1)。称之为 10 一个很好的圆数。看起来很低?尝试一下,我们打赌您可以轻松处理 3000 个前端用户在这样的设置下以 6000 TPS 运行简单查询。如果您运行负载测试,您可能会看到 TPS 速率开始下降,并且前端响应时间开始攀升,因为您将连接池推迟 10(在给定硬件上)。

5. Axiom:你想要一个小型池,其中包含等待连接的线程

如果您有 10,000 个前端用户,那么连接池数为 10,000 将是剪切精神错乱。1000 仍然可怕。甚至 100 个连接,矫枉过正。您最多需要一个包含几十个连接的小池,并且您希望池中的其他应用程序线程等待连接。如果池被正确调整,则它被设置为数据库能够同时处理的查询数量的限制 - 这几乎不会超过 `(CPU核数 * 2),如上所述。

我们永远不会对我们遇到的内部Web应用程序感到惊讶,有几十个前端用户执行定期活动,以及一个100个连接的连接池。不要过度配置您的数据库。

6. 连接池锁

对于获得许多联系的单一参与者而言,已经提出了“连接池锁”的前景。这主要是应用程序级问题。是的,增加池大小可以减轻这些情况下的锁定,但是我们建议您首先检查在扩展池之前可以在应用程序级别执行的操作。

为避免死锁而计算池大小是一个相当简单的资源分配公式:

pool_size = Tn x (Cm - 1) + 1

其中 Tn 是最大线程数,Cm 是单个线程持有的最大同时连接数。

例如,想象三个线程(Tn=3),每个线程需要四个连接来执行某些任务(Cm=4).。确保永远不会出现死锁所需的池大小是:

pool_size = 3 x (4 - 1) + 1 = 10

另一个例子,你最多有8个线程(Tn=8),每个线程需要三个连接来执行某项任务(Cm=3)。确保永远不会出现死锁所需的池大小是:

pool_size = 8 x (3 - 1) + 1 = 17

这不一定是最佳池大小,而是避免死锁所需的最小值。

在某些环境中,使用JTA(Java事务管理器)可以显着减少将同一连接返回 getConnection() 到当前事务中已持有 Connection 的线程所需的连接数。

7. 警告

池大小最终非常特定于部署。

例如,具有长时间运行事务和非常短事务混合的系统通常最难用任何连接池进行调整。在这些情况下,创建两个池实例可以很好地工作(例如,一个用于长时间运行的作业,另一个用于“实时”查询)。

在主要运行较长事务的系统中,所需连接数通常存在“外部”约束 - 例如,只允许一次运行一定数量的作业的作业执行队列。在这些情况下,作业队列大小应该是“正确大小”以匹配池(而不是相反)。(原文连接)。

本文由作者按照 CC BY 4.0 进行授权