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

Connection Pool and thread pool setting in Java

Spring 使用 Hikari 池的应用程序。

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

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。

您可能需要考虑更多参数:
1.数据库的最大并发请求参数。云提供商对不同层的并发请求有不同的限制,您可能需要检查一下。

2、当你说50-200ms的时候,虽然不好说,但平均有8个50ms的请求和2个200ms的请求,还是都差不多?为什么?您的 doQuery 可能会受到查询耗时最长(200 毫秒)的限制,但耗时 50 毫秒的线程将在任务完成后释放,使它们可用于下一组请求。

3. 您期望收到的QPS是多少?

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

当然,如果您的任何查询耗时 >200 毫秒(网络延迟或其他任何情况),其中一些计算就会失败,在这种情况下,我建议您在连接端使用断路器(如果您可以在超时后中止查询)或在 API 结束时。

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

建议:由于您需要 500 毫秒以下的响应,您也可以在池上设置大约 100-150 毫秒的连接超时。最坏情况:150 毫秒连接超时 + 200 毫秒查询执行 + 100 毫秒应用程序处理 < 500 毫秒响应。有效。

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

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);
    }

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

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

来自光website:

If you have 10,000 front-end users, having a connection pool of 10,000 would be shear insanity. 1000 still horrible. Even 100 connections, overkill. You want a small pool of a few dozen connections at most, and you want the rest of the application threads blocked on the pool awaiting connections. If the pool is properly tuned it is set right at the limit of the number of queries the database is capable of processing simultaneously -- which is rarely much more than (CPU cores * 2) as noted above.

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

我会这样实现控制器:

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

所以控制器可能看起来像这样(我从其他一些不能像这个例子那样工作的代码改编这个,所以对这个代码有保留):

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