Оптимизируем JDBC connection pool: гайд по HikariCP 2026

Страницы:  1

Ответить
 

Professor Seleznov


Привет, Хабр! HikariCP вполне можно назвать де-факто стандартом JDBC connection pooling в современной JVM-экосистеме: он используется по умолчанию в Spring Boot, часто выбирается в Java, Kotlin и Scala-проектах, активно поддерживается и хорошо знаком DevOps/SRE-командам по метрикам и поведению.
pic
Типичная конфигурация пула выглядит примерно следующим образом:

Пример конфигурации

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)
И конфиг:

Пример HOCON-конфига

db.jdbc {
dataSource {
url = ${?DATASOURCE_URL}
user = ${?DB_USER}
password = ${?DB_PASSWD}
}
connectionPool {
maxSize = 32
maxSize = ${?DB_CONNECTION_SIZE}
}
}
Вроде не ужас. HikariCP есть, размер пула вынесен в переменную окружения, пароль не захардкожен в боевом конфиге, SSL-сертификат поддерживается. Можно жить.
Но если смотреть на это пристальнее, то возникают вопросы:
  • Почему пул именно 32 или 42 или 52?
  • Что будет, если база или сетевой балансировщик закрывает соединения раньше, чем Hikari?
  • Сколько ждать свободное соединение: 30 секунд по умолчанию или меньше?
  • Видим ли мы метрики пула?
  • Работает ли leakDetectionThreshold = 1000, или мы просто успокаиваем себя красивой строчкой?
Погнали разбираться!
-
1. HikariCP: что это вообще такое

Определение

HikariCP это JDBC connection pool.
JDBC тут ключевое слово. HikariCP не привязан к одному языку или фреймворку. Это библиотека для JVM, которая управляет JDBC-соединениями. Поэтому она одинаково естественно живёт в Java, Kotlin, Scala, Groovy, Clojure и вообще в любом языке, который в итоге работает поверх JVM и JDBC.

Что делает пул

  • заранее открывает несколько соединений к базе;
  • выдаёт соединение коду, когда надо выполнить запрос;
  • принимает соединение обратно после close();
  • следит за жизненным циклом соединений;
  • ограничивает количество одновременных походов в базу;
  • даёт метрики, чтобы понять, где приложение упёрлось в БД.
Без пула каждый запрос мог бы создавать новое физическое соединение. Это дорого: TCP, TLS, аутентификация, настройка сессии, иногда ещё и прокси между приложением и базой. С пулом приложение берёт уже готовое соединение, работает и возвращает его обратно.

Пул не ускоряет сам SQL, он ограничивает и упорядочивает доступ к базе

Главная мысль, которую легко пропустить: пул не ускоряет сам SQL. Он ограничивает и упорядочивает доступ к базе. Хорошо настроенный пул делает систему стабильнее. Плохо настроенный пул умеет очень красиво положить базу, особенно если у вас 20 pod’ов и в каждом maximumPoolSize = 50.

-
2. Где используют HikariCP

Java

В Java-мире HikariCP давно стал вариантом “по умолчанию”. В Spring Boot он используется как дефолтный JDBC pool, если в classpath есть spring-boot-starter-jdbc или spring-boot-starter-data-jpa.

Kotlin

В Kotlin всё то же самое. Если проект на Spring Boot или Ktor с JDBC/Exposed, HikariCP обычно подключается через стандартную конфигурацию.

Scala

В Scala его можно встретить рядом с Quill, Slick, ScalikeJDBC, ZIO, Akka. Смысл тот же: HikariDataSource передают в контекст выбранной database-библиотеки или регистрируют в обёртке над DataSource.

Groovy

В Groovy это Grails, Spring Boot, plain JDBC-скрипты и сервисы.

Clojure

В Clojure HikariCP используют через next.jdbc, hikari-cp и похожие обёртки.

Python, Go, Node.js и Ruby

В Python, Go, Node.js и Ruby HikariCP обычно не используют, потому что там нет JDBC как базового слоя. Там свои пулы: database/sql в Go, SQLAlchemy QueuePool в Python, pg.Pool в Node.js и так далее.

-
3. Версии на апрель 2026
По Maven Central и репозиторию проекта актуальная основная линия HikariCP: 7.0.x, последняя версия, которую я нашёл, 7.0.2.

Важный момент по совместимости

Современный артефакт HikariCP рассчитан на Java 11+. Старые Java 8-проекты не надо механически тащить на 7.x. Для них нужно отдельно смотреть совместимую ветку и ограничения своего фреймворка.

Крупные вехи

  • 3.4.5 - старая, но ещё встречается в живых проектах. Релиз 2020 года.
  • 4.x - эпоха Java 8/11-перехода, появился keepaliveTime.
  • 5.x - уже более современная линия для новых JVM-стеков.
  • 6.x - много внутренних исправлений, включая работу с credentials и JDBC lifecycle.
  • 7.x - актуальная major-линия, Java 11+, исправления вокруг reflection, credentials provider, interruption в pool filling loop.

Осторожно с virtual threads

А теперь ложка дёгтя. В апреле 2026 я бы аккуратно смотрел на HikariCP 7.0.2 в проектах с virtual threads. Есть открытые обсуждения про CPU saturation в сценариях с большим количеством virtual threads, где соединения активно берутся и возвращаются в пул. Это не значит “не обновляться никогда”. Это значит: если у вас Java 21, virtual threads и высокий DB-throughput, обновление надо прогонять под нагрузкой, а не только через unit-тесты.
Для обычного JVM-сервиса без virtual threads история проще: смотрим совместимость JVM и фреймворка, обновляемся на поддерживаемую версию, запускаем нагрузочный тест.
-
4. Минимальная настройка в разных JVM-языках
Начнём с голого HikariCP, без Spring Boot магии.
Java

Пример настройки HikariCP на Java

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public final class DataSourceFactory {
public static DataSource create() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setPoolName("app-postgres");
config.setMaximumPoolSize(16);
config.setMinimumIdle(16);
config.setConnectionTimeout(5_000);
config.setValidationTimeout(2_000);
config.setMaxLifetime(25 * 60_000);
config.setKeepaliveTime(2 * 60_000);
config.setAutoCommit(true);
return new HikariDataSource(config);
}
}
Kotlin

Пример настройки HikariCP на Kotlin

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import javax.sql.DataSource
fun dataSource(): DataSource {
val config = HikariConfig().apply {
jdbcUrl = System.getenv("DB_URL")
username = System.getenv("DB_USER")
password = System.getenv("DB_PASSWORD")
poolName = "app-postgres"
maximumPoolSize = 16
minimumIdle = 16
connectionTimeout = 5_000
validationTimeout = 2_000
maxLifetime = 25 * 60_000
keepaliveTime = 2 * 60_000
isAutoCommit = true
}
return HikariDataSource(config)
}
Scala

Пример настройки HikariCP на Scala

import com.zaxxer.hikari.{HikariConfig, HikariDataSource}
object DataSourceFactory {
def create(): HikariDataSource = {
val config = new HikariConfig()
config.setJdbcUrl(sys.env("DB_URL"))
config.setUsername(sys.env("DB_USER"))
config.setPassword(sys.env("DB_PASSWORD"))
config.setPoolName("app-postgres")
config.setMaximumPoolSize(16)
config.setMinimumIdle(16)
config.setConnectionTimeout(5000)
config.setValidationTimeout(2000)
config.setMaxLifetime(25 * 60 * 1000)
config.setKeepaliveTime(2 * 60 * 1000)
config.setAutoCommit(true)
new HikariDataSource(config)
}
}
Синтаксис разный, смысл один: HikariCP это один и тот же HikariConfig и HikariDataSource, просто обёрнутые в привычный язык.
-
5. Если у вас Spring Boot
В Spring Boot обычно не надо руками создавать HikariDataSource. Достаточно настроек:

Пример настройки HikariCP в Spring Boot

spring:
datasource:
url: jdbc:postgresql://db.example.local:5432/app
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
pool-name: app-postgres
maximum-pool-size: 16
minimum-idle: 16
connection-timeout: 5000
validation-timeout: 2000
max-lifetime: 1500000
keepalive-time: 120000
auto-commit: true
register-mbeans: true

Полезные метрики HikariCP

Если включён Spring Boot Actuator и Micrometer, метрики Hikari обычно можно получить без большого шаманства. Самые полезные:
  • hikaricp.connections.active - сколько соединений сейчас занято;
  • hikaricp.connections.idle - сколько свободно;
  • hikaricp.connections.pending - сколько потоков ждут соединение;
  • hikaricp.connections.timeout - сколько раз приложение не дождалось соединения;
  • hikaricp.connections.acquire - сколько времени занимает получение соединения;
  • hikaricp.connections.usage - как долго соединение держат.
Без метрик настройка пула превращается в догадки: кажется, что всё работает, пока не начались таймауты и очередь за соединениями.
-
6. Главные параметры HikariCP

maximumPoolSize

Максимальное количество соединений в пуле. Включает и занятые, и свободные.
Если пул достиг этого размера, а свободных соединений нет, поток будет ждать до connectionTimeout. Потом получит ошибку.
Самая частая ошибка: ставить maximumPoolSize по количеству пользователей или HTTP-потоков. База не становится быстрее от того, что вы открыли к ней 100 соединений. Часто становится медленнее: контекстные переключения, конкуренция за CPU, locks, память, лишняя нагрузка на планировщик.

minimumIdle

Минимальное количество свободных соединений, которое Hikari старается держать.
Если minimumIdle не задан, HikariCP по умолчанию делает его равным maximumPoolSize. Получается fixed-size pool: все соединения создаются и держатся тёплыми.
Для серверных приложений с предсказуемой нагрузкой это часто нормальный вариант. Меньше сюрпризов на всплесках. Если хотите динамический пул, задавайте minimumIdle ниже maximumPoolSize, но понимайте цену: новые соединения могут создаваться прямо во время нагрузки.

connectionTimeout

Сколько ждать свободное соединение из пула.
Дефолт HikariCP: 30 секунд. Для многих backend-сервисов это слишком долго. Если запрос 30 секунд стоит в очереди за соединением, пользователь уже давно не счастлив, а ваш HTTP thread занят и держит ресурсы.
Я обычно начинаю с 2-5 секунд для online API и отдельно думаю про batch/background jobs.

validationTimeout

Сколько ждать проверки соединения. Дефолт: 5 секунд.
Если база или сеть деградирует, длинные проверки могут добавить неприятную задержку. 1-2 секунды часто достаточно, но надо смотреть на сеть и географию.

idleTimeout

Сколько idle-соединение может лежать в пуле перед удалением.
Работает только если minimumIdle < maximumPoolSize. Если пул fixed-size, параметр почти не участвует в жизни.

maxLifetime

Максимальный возраст соединения.
Это один из самых важных параметров для прода. Он должен быть меньше таймаута, после которого соединение может прибить база, proxy, load balancer, firewall или managed cloud-инфраструктура.
Если инфраструктура закрывает соединения через 30 минут, ставить maxLifetime = 30m плохо. Лучше 25-29 минут, в зависимости от конкретного окружения. Hikari сам добавляет небольшое размазывание по времени, чтобы не убивать весь пул разом.

keepaliveTime

Периодическая проверка idle-соединений, чтобы инфраструктура не считала их мёртвыми.
Появился не в самых древних версиях, поэтому если у вас HikariCP 3.4.5, пора посмотреть changelog и совместимость. В современных версиях дефолт около 2 минут. Значение должно быть меньше maxLifetime.

leakDetectionThreshold

Через сколько миллисекунд Hikari логирует предупреждение, что соединение слишком долго не вернули в пул.
Дефолт: 0, то есть выключено. Минимальное значение для включения: 2000 мс.
И вот тут возвращаемся к конфигу из начала статьи:
config.setLeakDetectionThreshold(1000)
Выглядит как “ловим утечки быстрее”. На деле в современных HikariCP значение меньше 2 секунд не считается валидным. То есть такая настройка может быть просто отключена валидацией. Обидный случай: строчка есть, а пользы нет.
В проде я бы не держал короткий leak detection постоянно. Он может шуметь на нормальных длинных запросах. Лучше включать временно при расследовании или ставить порог выше ожидаемого p99 использования соединения.

-
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 физических ядер, SSD
pool target ~= (8 * 2) + 1 = 17 активных соединений
Звучит мало. Особенно если у вас 500 RPS.
Но соединения к базе это не пользователи и не HTTP-запросы. Если запрос к базе занимает 20 мс, то 10 соединений теоретически могут прокачать сотни операций в секунду. А если запрос занимает 2 секунды, пул на 100 соединений не спасёт. Он просто даст ста людям одновременно страдать внутри базы.

Есть ещё один полезный способ думать&#58; Little's Law

Есть ещё один полезный способ думать: Little’s Law.
нужные соединения ~= RPS * среднее время работы с БД
Если сервис делает 300 DB-операций в секунду, а среднее время удержания соединения 30 мс:
300 * 0.03 = 9 соединений
Добавили запас, проверили p95/p99, прогнали нагрузочный тест. Получили стартовую настройку.
Это только стартовое значение, а не окончательная настройка. После нагрузочного теста и работы в проде надо смотреть на метрики:
  • заняты почти все соединения в пуле;
  • потоки часто ждут свободное соединение;
  • растёт количество ошибок по connection timeout;
  • увеличивается время получения соединения из пула;
  • при этом сама база не перегружена по CPU, IO и блокировкам.
Если pending растёт, а база уже перегружена, увеличение пула сделает хуже. Это классика: приложение начинает давить сильнее ровно в тот момент, когда базе нужна передышка.

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

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

Один из самых частых продовых сюрпризов:
maximumPoolSize = 32
replicas = 12
итого потенциально 384 соединения к базе
А ещё есть миграции, админки, BI, cron jobs, ручные подключения, read-only сервисы. Потом кто-то удивляется, почему PostgreSQL внезапно говорит too many connections.
Размер пула надо считать не только на один процесс, а на всю систему:
db_connection_budget = 160
service_replicas = 8
pool_per_replica = 160 / 8 = 20

Если используете HPA (Horizontal Pod Autoscaler&#41;, например PgBouncer

Если есть HPA (Horizontal Pod Autoscaler), берите максимальное количество pod’ов, а не комфортное среднее в обеденное время.
И да, иногда правильный ответ не “увеличить HikariCP”, а поставить PgBouncer или другой внешний pooler. Особенно когда много приложений, много коротких запросов и PostgreSQL страдает от числа backend-процессов.
PgBouncer это лёгкий внешний пулер соединений для PostgreSQL. Он стоит между приложениями и базой, принимает много клиентских подключений и переиспользует меньшее число реальных соединений к PostgreSQL.
Проще говоря, HikariCP управляет соединениями внутри одного JVM-приложения, а PgBouncer помогает ограничить общее количество соединений к базе на уровне инфраструктуры.

-
9. Что бы я поменял в настройке из начала статьи
Стартовая конфигурация была такая:

Фрагмент стартовой Scala-конфигурации

config.setMaximumPoolSize(connectionPoolMaxSize)
config.setLeakDetectionThreshold(1000)
config.setAutoCommit(true)
И 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_PASSWORD}
}
connectionPool {
poolName = "app-postgres-main"
maxSize = 16
minIdle = 16
connectionTimeoutMs = 5000
validationTimeoutMs = 2000
maxLifetimeMs = 1500000
keepaliveTimeMs = 120000
leakDetectionThresholdMs = 0
registerMbeans = true
}
}
И код:

Пример более полной Scala-конфигурации

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 - все могут иметь своё мнение о том, сколько живёт idle TCP connection. Если 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-канале.
Успешных вам релизов!-Источник
 
Loading...
Error