Java中的连接池和线程池设置

Posted

技术标签:

【中文标题】Java中的连接池和线程池设置【英文标题】:Connection Pool and thread pool setting in Java 【发布时间】:2020-02-09 15:08:03 【问题描述】:

使用 Hikari 池的 Spring 应用程序。

现在对于来自客户端的单个请求,我必须查询 10 个表(业务需要),然后将结果组合在一起。查询每个表可能需要 50 毫秒到 200 毫秒。为了加快响应时间,我在我的服务中创建了一个FixedThreadPool 来查询不同线程中的每个表(伪代码):

class MyService
    final int THREAD_POOL_SIZE = 20;
    final int CONNECTION_POOL_SIZE = 10;


    final ExecutorService pool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    protected DataSource ds;


    MyClass()
        Class.forName(getJdbcDriverName());
        HikariConfig config = new HikariConfig();
        config.setMaximumPoolSize(CONNECTION_POOL_SIZE);
        ds = new HikariDataSource(config);
    



    public Items doQuery()
        String[] tables=["a","b"......]; //10+ tables
        Items result=new Items();
        CompletionService<Items> executorService = new ExecutorCompletionService<Items>(pool);
        for (String tb : tables) 
            Callable<Item> c = () -> 
                Items items = ds.getConnection().query(tb); ......
                return Items;
            ;
            executorService.submit(c);
        


        for (String tb: tables) 
            final Future<Items> future = executorService.take();
            Items items = future.get();
            result.addAll(items);
        
    

现在对于单个请求,平均响应时间可能为 500 毫秒。

但是对于并发请求,平均响应时间会迅速增加,请求越多,响应时间就会越长。

我想知道如何设置合适的连接池大小和线程池大小以使应用有效工作?

顺便说一句,数据库使用云中的 RDS,具有 4 个 cpu 16GB 内存,最大连接数 2000 和最大 IOPS 8000。

【问题讨论】:

如果您发送的每个请求都需要 10 个到数据库的连接,而您的池中只有 10 个连接,那么一次只能处理 1 个请求。您的线程策略可能会增加 一个 请求的响应时间。但是,如果您期望连续的并发请求,按顺序执行操作,没有这种线程策略,可能会更简单、更高效、需要更少的连接、并具有适当的事务隔离语义,从而返回一致的结果。但可以肯定的是,您可以增加池中的连接数,因为您最多可以有 2000 个。 您是否尝试在数据库中合成结果 @MarmiteBomber 我们不能在数据库中这样做。 @JBNizet:我不知道为什么Your threading strategy might increase the response time for one request。对于单个请求,我的线程策略将使用 10 个线程,每个线程使用池中的一个连接,我认为这可能会减少响应。 对不起,但是用 10 个表做一些事情然后处理数据:这表明数据库应该组合事物和/或一些数据没有以最佳方式组织。此外:summary 数据可以通过数据库触发器来完成。快 10 倍的速度并不能改善索引之类的东西。 (我知道有时无济于事。) 【参考方案1】:

您可能需要考虑更多参数: 1. 数据库的最大并发请求参数。云提供商对不同层的并发请求有不同的限制,您可能需要检查一下。 2、你说50-200ms,虽然很难说,但是平均有8个50ms的请求和2个200ms的请求,还是几乎都差不多?为什么?您的 doQuery 可能会受到最长查询时间(即 200 毫秒)的限制,但需要 50 毫秒的线程将在其任务完成后释放,使其可用于下一组请求。 3. 您期望收到的 QPS 是多少?

一些计算: 如果单个请求占用 10 个线程,并且您已配置 100 个连接和 100 个并发查询限制,假设每个查询 200 毫秒,您一次只能处理 10 个请求。如果大多数查询需要 50 毫秒左右,可能会比 10 好一点(但我不会乐观)。

当然,如果您的任何查询花费 >200 毫秒(网络延迟或其他任何东西),则其中一些计算会被折腾,在这种情况下,我建议您在连接端有一个断路器(如果您是允许在超时后中止查询)或在 API 结束时中止。

注意最大连接数限制最大并发查询数限制不同。

建议:由于需要500ms以下的响应,池上也可以有一个100-150ms左右的connectionTimeout。最坏的情况:150 毫秒连接超时 + 200 毫秒查询执行 + 100 毫秒应用程序处理

【讨论】:

【参考方案2】:

您可以创建自定义线程执行器

public class CustomThreadPoolExecutor extends ThreadPoolExecutor 

    private CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                     long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) 
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    

    /**
     * Returns a fixed thread pool where task threads take Diagnostic Context from the submitting thread.
     */

    public static ExecutorService newFixedThreadPool(int nThreads) 
        return new CustomThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    

在配置中,你可以如下配置ExecutorService bean

@Bean
    public ExecutorService executeService() 
        return CustomThreadPoolExecutor.newFixedThreadPool(10);
    

这是创建自定义线程池执行器的最佳实践

【讨论】:

【参考方案3】:

调整连接池大小的正确方法通常是将其保留为默认值。

来自 hikari website:

如果您有 10,000 个前端用户,那么拥有 10,000 个连接池将是疯狂的。 1000仍然可怕。甚至100个连接,矫枉过正。您需要一个最多包含几十个连接的小池,并且您希望其余的应用程序线程阻塞在等待连接的池中。如果池经过适当调整,它将被设置为数据库能够同时处理的查询数量的限制——很少超过(CPU 核心 * 2),如上所述。

假设您知道每个请求将消耗 10 个线程,那么您想打破这个建议并使用更多线程 - 将其保持在小于 100 的数字可能会提供足够的容量。

我会这样实现控制器:

使用CompletableFutures 在您的控制器/服务类中使您的查询异步,并让连接池担心保持其线程忙碌。

所以控制器可能看起来像这样(我从其他一些不像这个例子那样工作的代码中调整了这个,所以这个代码有点盐):

public class AppController  

    @Autowired private DatabaseService databaseService; 

    public ResponseEntity<Thing> getThing()  
        CompletableFuture<Foo> foo = CompletableFuture.runAsync(databaseService.getFoo());
        CompletableFuture<Bar> bar = CompletableFuture.runAsync(databaseService.getBar());
        CompletableFuture<Baz> baz = CompletableFuture.runAsync(databaseService.getBaz());

        // muck around with the completable future to return your data in the right way
        // this will be in there somewhere, followed by a .thenApply and .join
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(foo, bar, baz);

        return new ResponseEntity<Thing>(mashUpDbData(cf.get()));
        

控制器将生成您允许ForkJoinPool 使用的尽可能多的线程,它们将同时敲击数据库,并且连接池可能会担心保持连接处于活动状态。

但我认为您在小负载下看到响应时间井喷的原因是 JDBC 在等待数据从数据库返回时阻塞了线程的设计。

要阻止对响应时间造成如此巨大影响的阻塞,您可以尝试spring boot reactive 样式。这使用异步 io 和背压来匹配 IO 生产和消费,基本上这意味着应用程序线程尽可能繁忙。这应该在响应时间以线性方式增加的负载下停止该行为。

请注意,如果您确实采用响应式路径,jdbc 驱动程序仍会阻塞,因此 spring 大力推动创建 reactive database driver。

【讨论】:

以上是关于Java中的连接池和线程池设置的主要内容,如果未能解决你的问题,请参考以下文章

Spring Batch 连接池和多线程

线程池和数据库连接池

JAVA多线程 线程池和锁的深度化

JAVA线程池shutdown和shutdownNow的区别

Okhttp的线程池和高并发

性能测试连接池和线程