Оптимизируем JDBC connection pool HikariCP. Прод, ресурсы и типовые ошибки

от автора

Привет, Хабр!

Это продолжение первой части: там были основы HikariCP, версии, примеры настройки на JVM-языках, Spring Boot и разбор главных параметров пула. Здесь разберём, как считать размер пула, что учитывать в Kubernetes и при нескольких сервисах, как улучшить «типовой» конфиг из начала серии, типовые ошибки, мини-чеклист для прода и ссылки.

Стартуем сразу с п.7, Погнали !

7. Как выбрать размер пула

Известная формула из PostgreSQL wiki и HikariCP wiki

Есть известная формула из PostgreSQL wiki и HikariCP wiki:

connections = (core_count * 2) + effective_spindle_count

core_count — ядра сервера базы, не приложения. Hyper-threading лучше не считать как полноценные ядра.

effective_spindle_count — грубая поправка на дисковую подсистему. Для современных SSD/NVMe часто берут 0 или 1 как стартовое приближение.

Пример:

PostgreSQL: 8 физических ядер, SSDpool target ~= (8 * 2) + 1 = 17 активных соединений

Звучит мало. Особенно если у вас 500 RPS.

Но соединения к базе это не пользователи и не HTTP-запросы. Если запрос к базе занимает 20 мс, то 10 соединений теоретически могут прокачать сотни операций в секунду. А если запрос занимает 2 секунды, пул на 100 соединений не спасёт. Он просто даст ста людям одновременно страдать внутри базы.

Есть ещё один полезный способ думать: Little’s Law

Есть ещё один полезный способ думать: Little’s Law.

нужные соединения ~= RPS * среднее время работы с БД

Если сервис делает 300 DB-операций в секунду, а среднее время удержания соединения 30 мс:

300 * 0.03 = 9 соединений

Добавили запас, проверили p95/p99, прогнали нагрузочный тест. Получили стартовую настройку.

Это только стартовое значение, а не окончательная настройка. После нагрузочного теста и работы в проде надо смотреть на метрики:

  • заняты почти все соединения в пуле;

  • потоки часто ждут свободное соединение;

  • растёт количество ошибок по connection timeout;

  • увеличивается время получения соединения из пула;

  • при этом сама база не перегружена по CPU, IO и блокировкам.

Если pending растёт, а база уже перегружена, увеличение пула сделает хуже. Это классика: приложение начинает давить сильнее ровно в тот момент, когда базе нужна передышка.


8. Несколько приложений и Kubernetes

Размер пула надо считать не только на один процесс, а на всю систему

Один из самых частых продовых сюрпризов:

maximumPoolSize = 32replicas = 12итого потенциально 384 соединения к базе

А ещё есть миграции, админки, BI, cron jobs, ручные подключения, read-only сервисы. Потом кто-то удивляется, почему PostgreSQL внезапно выдает too many connections.

Размер пула надо считать не только на один процесс, а на всю систему:

db_connection_budget = 160service_replicas = 8pool_per_replica = 160 / 8 = 20
Если используйте HPA (Horizontal Pod Autoscaler), например PgBouncer

Если есть HPA (Horizontal Pod Autoscaler), берите максимальное количество pod’ов, а не комфортное среднее в обеденное время.

И да, иногда правильный ответ не “увеличить HikariCP”, а поставить PgBouncer или другой внешний pooler. Особенно когда много приложений, много коротких запросов и PostgreSQL страдает от числа backend-процессов.

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

Проще говоря, HikariCP управляет соединениями внутри одного JVM-приложения, а PgBouncer помогает ограничить общее количество соединений к базе на уровне инфраструктуры.


9. Что бы я поменял в настройке из певрой части статьи

Стартовая конфигурация была такая:

Фрагмент стартовой конфигурации
val config = new HikariConfig()config.setJdbcUrl(url)config.setUsername(user)config.setPassword(password)config.setMaximumPoolSize(connectionPoolMaxSize)config.setLeakDetectionThreshold(1000)config.setAutoCommit(true)sslRootCert.foreach(config.addDataSourceProperty("sslrootcert", _))new HikariDataSource(config)

И maxSize = 32.

Я бы сделал несколько вещей.

Обновил версию HikariCP

Если проект всё ещё на HikariCP 3.4.5, это 2020 год. Для JVM-проекта в 2026 это уже техдолг.

Но обновление зависит от Java:

  • Java 11+ — смотреть HikariCP 7.x или актуальную поддерживаемую линию фреймворка.

  • Java 8 — не прыгать на 7.x, а сначала разобраться с совместимой веткой и планом миграции JVM.

  • Java 21 + virtual threads — обязательно нагрузочное тестирование, особенно если соединения часто создаются и закрываются.

Убрал или исправил leak detection

1000 мс выглядит как ошибка. Я бы сделал так:

if (leakDetectionEnabled) {  config.setLeakDetectionThreshold(10_000)}

И включал бы это через env/config только на время диагностики. Для постоянного прода лучше метрики плюс алерты на pending, timeout, usage.

Задал poolName

Без имени пула метрики и логи хуже читаются.

config.setPoolName("app-postgres-main")

Название должно быть обезличенным, но понятным: сервис, база, роль. Если есть read-only пул, он должен называться иначе.

Явно настроил таймаут ожидания соединения

30 секунд по умолчанию я бы не оставлял.

config.setConnectionTimeout(5000)config.setValidationTimeout(2000)

Для API 5 секунд это уже много. Для background jobs можно задать отдельно.

Настроил maxLifetime и keepaliveTime

Например:

config.setMaxLifetime(25 * 60 * 1000)config.setKeepaliveTime(2 * 60 * 1000)

Но эти числа нельзя копировать без проверки. Надо смотреть реальные таймауты PostgreSQL, cloud DB, firewall, NAT, proxy, PgBouncer, service mesh. maxLifetime должен быть чуть меньше внешнего лимита.

Подумал над minimumIdle

Если сервис online и нагрузка более-менее постоянная, я бы начал с fixed-size:

config.setMaximumPoolSize(poolSize)config.setMinimumIdle(poolSize)

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

Подключил метрики

Для Spring Boot это Actuator/Micrometer. Для ручной конфигурации:

config.setMetricRegistry(meterRegistry);

или JMX:

config.setRegisterMbeans(true);

Я бы не выпускал сервис в прод без графиков:

  • active connections;

  • idle connections;

  • pending threads;

  • connection acquire time;

  • connection usage time;

  • connection timeout count.

Пересчитал maxSize = 32

Не говорю, что 32 плохо. Может быть отлично. Но должно быть понятно, откуда взялось это число.

Минимальный sanity-check:

max_db_connections_for_service / max_replicas

Потом сверка с базой:

(db_cores * 2) + effective_spindle_count

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


10. Пример более полного конфига

В HOCON это могло бы выглядеть так:

Пример более полного HOCON-конфига
db.jdbc {  dataSource {    url = ${?DATASOURCE_URL}    user = ${?DB_USER}    password = ${?DB_PASSWD}  }  connectionPool {    poolName = "app-postgres-main"    maxSize = 16    minIdle = 16    connectionTimeoutMs = 5000    validationTimeoutMs = 2000    maxLifetimeMs = 1500000    keepaliveTimeMs = 120000    leakDetectionThresholdMs = 0    registerMbeans = true  }}

И код:

Пример более полной конфигурации
val config = new HikariConfig()config.setJdbcUrl(jdbc.url)config.setUsername(jdbc.user)config.setPassword(jdbc.password)config.setPoolName(pool.poolName)config.setMaximumPoolSize(pool.maxSize)config.setMinimumIdle(pool.minIdle)config.setConnectionTimeout(pool.connectionTimeoutMs)config.setValidationTimeout(pool.validationTimeoutMs)config.setMaxLifetime(pool.maxLifetimeMs)config.setKeepaliveTime(pool.keepaliveTimeMs)config.setLeakDetectionThreshold(pool.leakDetectionThresholdMs)config.setRegisterMbeans(pool.registerMbeans)config.setAutoCommit(true)sslRootCert.foreach(config.addDataSourceProperty("sslrootcert", _))new HikariDataSource(config)

Опять же, это не “идеальный конфиг для всех”. Просто в нём видно, какие настройки выбраны и зачем.


11. Типовые ошибки

Слишком большой пул

Пул на 100 соединений выглядит солидно только до первого серьёзного инцидента. Потом выясняется, что база тратит слишком много ресурсов на переключение между задачами и ожидание блокировок.

Один конфиг для API и batch

API хочет fail fast. Batch может подождать. API держит соединение миллисекунды. Batch может держать секунды или минуты. Разные профили нагрузки иногда требуют разных пулов или хотя бы разных таймаутов.

Не закрывать соединения

В Java это try-with-resources.

try (Connection connection = dataSource.getConnection()) {    // SQL work}

В Spring — нормальные транзакции и отсутствие ручного удержания connection где попало.

В функциональных JVM-стеках — Resource, ZLayer и другие managed lifecycle-подходы. В Clojure — with-open, если берёте connection руками.

Полагаться только на connectionTestQuery

Для нормальных JDBC4-драйверов HikariCP умеет использовать Connection.isValid(). Ручной SELECT 1 часто не нужен. Иногда нужен, если драйвер старый или странный, но это уже исключение.

Не знать таймауты инфраструктуры

База, firewall, NAT gateway, cloud proxy, PgBouncer, service mesh — всё это может закрывать неактивные соединения по своим правилам. Если Hikari узнаёт о смерти соединения последним, пользователи получают ошибки.


12. Мини-чеклист для прода

Мини-чеклист для прода

Перед тем как считать настройку HikariCP законченной, я бы прошёлся по такому списку:

  • версия HikariCP совместима с Java и фреймворком;

  • maximumPoolSize посчитан на все реплики, а не на один процесс;

  • minimumIdle выбран осознанно: fixed-size или dynamic;

  • connectionTimeout подходит под SLA сервиса;

  • maxLifetime меньше внешних connection timeout’ов;

  • keepaliveTime включён, если инфраструктура режет idle-соединения;

  • leak detection выключен или включается для дебага;

  • есть poolName;

  • есть метрики и алерты;

  • был нагрузочный тест после изменения размера пула;

  • read-only и write-пулы различаются в метриках и логах.


13. Ссылки


Вместо вывода

HikariCP хорош тем, что его легко подключить. И опасен тем же самым.

Поставил зависимость, написал maximumPoolSize = 32, сервис стартует, графики зелёные. Потом добавились реплики, вырос p95, база переехала за прокси, в соседнем сервисе включили batch, и старый “нормальный” конфиг стал миной.

Я бы относился к пулу соединений не как к ускорителю или кэшу, а как к ограничителю нагрузки на базу: сколько одновременных запросов к БД мы разрешаем приложению прямо сейчас.

И если это ограничение выбрано осознанно, базе проще работать стабильно под нагрузкой.


Если вам близки темы разработки, рефакторинга, архитектуры и стартапов буду рад видеть вас в моём Telegram-канале.

Успешных вам релизов!

ссылка на оригинал статьи https://habr.com/ru/articles/1031770/