确定最佳连接池大小的最佳方法

banq


在本文中,我们将了解使用FlexyPool自动递增池策略确定最佳连接池大小的最佳方法。

现在,根据通用可扩展性定律,数据库系统的最大吞吐量是在有限数量的数据库连接下实现的。如果我们将连接数增加到超过该特定数字,吞吐量就会变差。

因此,考虑到这一点,我们的目标是确保每个应用程序节点都配置其连接池以使用正确数量的数据库连接,以便在将所有连接加在一起时,我们不会超过前面提到的最佳数据库连接数。

测试应用程序
为了演示 FlexyPool 如何帮助我们确定最佳连接池大小,我们假设我们有一个可以将资金从一个账户转移到另一个账户的转账服务。

@Service
public class TransferService {

    private final AccountRepository accountRepository;

    public TransferService(
            @Autowired AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transfer(
            String sourceAccount,
            String destinationAccount,
            long amount) {
        if(accountRepository.getBalance(sourceAccount) >= amount) {
            accountRepository.addToBalance(sourceAccount, (-1) * amount);

            accountRepository.addToBalance(destinationAccount, amount);
        }
    }
}

为了确定执行 64 个并发传输需要使用多少个数据库连接,我们可以使用以下测试用例:

long startNanos = System.nanoTime();
int threadCount = 64;
 
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
 
for (int i = 0; i < threadCount; i++) {
    new Thread(() -> {
        try {
            startLatch.await();

            transferService.transfer("Alice-123", "Bob-456", 5L);
        } catch (Exception e) {
            LOGGER.error(
"Transfer failed", e);
        } finally {
            endLatch.countDown();
        }
    }).start();
}
 
LOGGER.info(
"Starting threads");
startLatch.countDown();
endLatch.await();
 
LOGGER.info(
   
"The {} transfers were executed on {} database connections in {} ms",
    threadCount,
    hikariDataSource.getMaximumPoolSize(),
    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)
);

如果我们使用默认设置,那么 HikariCP 将最多使用 10 个连接:

HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDataSource(dataSourceProvider().dataSource());
hikariConfig.setAutoCommit(false);
HikariDataSource poolingDataSource = new HikariDataSource(hikariConfig);

当运行集成测试用例时,我们会在日志中打印以下消息:

The 64 transfers were executed on 10 database connections in 149 ms

由于操作传输的线程数大于最大池大小,HikariCP 将很快增长到10数据库连接的最大池大小。

如果我们将最大池大小设置为与传输线程数匹配并重新运行我们的测试用例:

hikariConfig.setMaximumPoolSize(64);

总传输时间将更长,因为现在争用从连接池转移到数据库,数据库必须根据REPEATABLE_READ隔离级别序列化所有事务:

The 64 transfers were executed on 64 database connections in 272 ms

但是,在我们的例子中,我们实际上不能10为该服务使用数据库连接,因为我们可能在系统中运行多个其他服务,并且数据库主节点具有最大连接数48。


如何确定最佳连接池大小
为了确定最佳连接池大小,我们可以使用FlexyPoolDataSource代理来包装HikariDataSource并配置IncrementPoolOnTimeoutConnectionAcquisitionStrategy以根据需要增加池的大小:

HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDataSource(dataSourceProvider().dataSource());
hikariConfig.setAutoCommit(false);
hikariConfig.setMaximumPoolSize(1);
hikariConfig.setConnectionTimeout(100);
HikariDataSource poolingDataSource = new HikariDataSource(hikariConfig);
 
int maxOverflowPoolSize = 10;
int connectionAcquisitionThresholdMillis = 25;
 
FlexyPoolDataSource<HikariDataSource> dataSource = new FlexyPoolDataSource<>(
    new FlexyPoolConfiguration.Builder<>(
        getClass().getSimpleName(),
        poolingDataSource,
        HikariCPPoolAdapter.FACTORY)
    .build(),
    new IncrementPoolOnTimeoutConnectionAcquisitionStrategy.Factory<>(
        maxOverflowPoolSize,
        connectionAcquisitionThresholdMillis
    )
);

我们首先将 HiakriCP 的最大池大小设置为仅一个连接,如果连接获取时间超过 25 毫秒(例如,connectionAcquisitionThresholdMillis),我们将通过从缓冲区借用来增加 HikariCP 池大小直到10最大连接数(例如,maxOverflowPoolSize)。

使用此 FlexyPool 配置运行我们的测试用例时,我们可以看到只需要 4 个数据库连接即可执行 64 次传输:

The 64 transfers were executed on 4 database connections in 289 ms

因此,让我们用一个简单的 HikariCP 配置替换 FlexyPool 配置,该配置使用最大池大小为4:

HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDataSource(dataSourceProvider().dataSource());
hikariConfig.setAutoCommit(false);
hikariConfig.setMaximumPoolSize(4);
HikariDataSource poolingDataSource = new HikariDataSource(hikariConfig);

当我们重新运行测试用例时,我们得到以下结果:

The 64 transfers were executed on 4 database connections in 128 ms

很棒吧?

结论
通过 FlexyPool IncrementPoolOnTimeoutConnectionAcquisitionStrategy,我们可以发现特定用例所需的最佳连接池大小,以适应零星的连接获取延迟(例如,在我们的案例中为 25 毫秒)。 通过使用 JMeter 进行集成测试或功能测试,我们可以确定连接池大小所需的最佳连接数,以便及时处理特定负载。