Настройка пула соединений и пула потоков в Java - PullRequest
3 голосов
/ 12 октября 2019

Приложение Spring с использованием пула Hikari.

Теперь для одного запроса от клиента мне нужно запросить 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 мс.

enter image description here

Но для одновременных запросов среднее время ответа будет быстро увеличиваться, чем больше запросов, тем дольше будет время ответа.

enter image description here

Интересно, как правильно настроить размер пула соединений и пула потоков, чтобы приложение работало эффективно?

Кстати, использование базы данныхRDS в облаке с 4 ЦП 16 ГБ памяти, 2000 макс. Подключений и 8000 макс. IOPS.

Ответы [ 3 ]

2 голосов
/ 14 октября 2019

Возможно, вы захотите подумать еще о нескольких параметрах:
1. Максимальный параметр одновременного запроса для базы данных. Облачные провайдеры имеют разные лимиты одновременных запросов для разных уровней, вы можете проверить свои.

2. Когда вы говорите 50-200 мс, хотя трудно сказать, есть ли в среднем 8 запросов по 50 мс и 2 запроса по 200 мс, или все они почти одинаковы? Почему? Ваш doQuery может быть ограничен запросом, занимающим максимальное время (которое составляет 200 мс), но потоки, занимающие 50 мс, будут освобождены после выполнения своей задачи, сделав их доступными для следующего набора запросов.

3. Что такоеQPS вы ожидаете получить?

Некоторые вычисления: если один запрос занимает 10 потоков, и вы подготовили 100 подключений с пределом 100 одновременных запросов, предполагая 200 мс для каждого запроса, вы можете обрабатывать только 10 запросов одновременно. ,Может быть, немного лучше, чем 10, если большинство запросов занимает 50 мс или около того (но я бы не был оптимистичен).

Конечно, некоторые из этих вычислений приводят к броску, если какой-либо из ваших запросов занимает> 200 мс (сетьзадержка или что-то еще), в этом случае я рекомендую иметь автоматический выключатель, либо в конце соединения (если вам разрешено прерывать запрос после истечения времени ожидания), либо в конце API.

Примечание : макс. Предел соединения - это не то же самое, что макс. Предел одновременного запроса .

Предложение. Поскольку вам нужен ответ менее 500 мс, вы также можете иметьВремя соединения около 100-150 мсек в бассейне. В худшем случае: время ожидания соединения 150 мс + выполнение запроса 200 мс + 100 мс для обработки приложения <500 мс для вашего ответа. Работы. </p>

0 голосов
/ 21 октября 2019

Правильный способ определения размера пула соединений - это оставить его по умолчанию.

С веб-сайта hikari :

Если у вас есть 10 000 внешних пользователей, наличие пула соединений в 10 000 будет безумным сдвигом. 1000 все еще ужасно. Даже 100 соединений, перебор. Вам нужен небольшой пул, состоящий максимум из нескольких десятков соединений, и хотите, чтобы остальные потоки приложений были заблокированы в пуле в ожидании соединений. Если пул правильно настроен, он устанавливается прямо на пределе количества запросов, которые база данных способна обрабатывать одновременно - что редко намного больше, чем (ядра ЦП * 2), как отмечено выше.

Если вы знаете, что каждый запрос будет занимать 10 потоков, то вы захотите отказаться от этого совета и перейти к большему количеству потоков - вероятно, если его число будет меньше 100, это обеспечит достаточную емкость.

Я бы реализовал контроллер следующим образом:

Сделайте ваши запросы асинхронными в классах контроллера / службы с CompletableFuture s и позвольте пулу соединений беспокоиться о том, чтобы его потоки были заняты.

Таким образом, контроллер может выглядеть следующим образом (я адаптирую его из некоторого другого кода, который не работает, как в этом примере, поэтому немного соли с этим кодом):

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 блокирует поток в ожидании возвращения данных из БД.

Чтобы блокировка не влияла на время отклика так сильно, вы можете попробовать стиль реагирующий на пружинную опору . При этом используются асинхронный ввод и обратное давление для согласования производительности ввода-вывода и потребления, в основном это означает, что потоки приложения настолько заняты, насколько это возможно. Это должно остановить это поведение под нагрузкой, когда время отклика увеличивается линейным образом.

Обратите внимание, что если вы идете по реактивному пути, драйверы jdbc по-прежнему блокируются, поэтому у Spring есть большой толчок для создания реактивного драйвера базы данных .

0 голосов
/ 17 октября 2019

Вы можете создать пользовательский исполнитель потоков

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

В конфигурации вы можете настроить bean-компонент ExecutorService, как показано ниже:

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

Это лучший способ создания пользовательского пула потоковисполнитель

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...