Загрузка
Все статьи

Kotlin + PostgreSQL: подводные камни HikariCP в микросервисах

HikariCP — пул подключений по умолчанию в Spring Boot и Ktor. На первый взгляд — configure and forget. На практике — неправильный размер пула убивает производительность, leak detection срабатывает с ложными позитивами, а connection timeout в Kubernetes приводит к каскадным сбоям. Рассказываю, как мы настраивали HikariCP для продакшена и что пошло не так.

HikariCP — самый быстрый пул подключений JDBC. Это факт, подтверждённый бенчмарками. Spring Boot использует его по умолчанию, Exposed тоже. На первый взгляд — добавил зависимость, прописал URL базы данных, и всё работает. Но в продакшене «работает» и «работает хорошо» — это разные вещи.

Рассказываю по боли. У нас payment-service начал тормозить под нагрузкой: p99 latency подскочил с 200мс до 4 секунд. Мониторинг показывал, что потоки сервиса ждут подключения из пула. Мы увеличили пул с 10 до 50 — стало хуже. Уменьшили до 5 — тоже хуже. Оказалось, проблема была вообще не в размере пула.

Дефолтные настройки — ловушка

Spring Boot устанавливает maximumPoolSize = 10 по умолчанию. Звучит разумно. Но давайте посчитаем:

10 подключений, 4 потока Tomcat (дефолт) — 40 конкурентных запросов могут обрабатываться одновременно. Но если каждый запрос делает 2–3 запроса в БД, то реально параллельно обслуживаются только 3–5 запросов. Остальные ждут в очереди за подключением.

Формула, которой мы пользуемся сейчас:

optimal_pool_size = ((core_count * 2) + effective_spindle_count)

# Для нашего пода в K8s (2 CPU, SSD):
# optimal = (2 * 2) + 1 = 5
# Но с запасом на пики: 10-15

Ключевое понимание: размер пула должен быть примерно равен количеству ядер CPU, умноженному на 2. Больше — не значит лучше. Больше подключений в пуле = больше контекста переключений в PostgreSQL = медленнее каждый запрос.

Наши production-настройки

Вот реальный application.yml, к которому мы пришли после месяцев экспериментов:

spring:
  datasource:
    url: jdbc:postgresql://pg-primary:5432/payment_db?sslmode=require&tcpKeepAlive=true
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      pool-name: payment-hikari
      maximum-pool-size: 15
      minimum-idle: 5
      idle-timeout: 300000        # 5 минут
      max-lifetime: 600000        # 10 минут
      connection-timeout: 3000    # 3 секунды! Не дефолтные 30
      leak-detection-threshold: 60000  # 60 секунд
      validation-timeout: 1000    # 1 секунда

  jpa:
    properties:
      hibernate:
        connection.provider_disables_autocommit: true

Обратите внимание на connection-timeout: 3000. Дефолт — 30 секунд. 30 секунд ожидания подключения в микросервисе — это вечность. За это время клиент уйдёт по таймауту, балансировщик переключит трафик, появится повторный запрос — и всё это нагнетает нагрузку. 3 секунды — порог, после которого лучше сразу вернуть ошибку и позволить клиенту ретраить.

Leak Detection: друг, который вас бесит

HikariCP умеет детектировать утечки подключений: если подключение взято из пула и не возвращено за N миллисекунд — в лог падает warning с полным stacktrace. Звучит полезно. На практике — мы получили ложные срабатывания на долгих транзакциях.

У нас есть операция «выставить счёт», которая занимает 8–15 секунд: она делает несколько запросов, вызывает внешнее API платёжной системы, обновляет баланс. Это легитимная долгая транзакция, а не утечка.

@Component
class HikariMetricsConfig(
    private val meterRegistry: MeterRegistry
) {
    @PostConstruct
    fun bindHikariMetrics() {
        // HikariCP регистрирует метрики автоматически через Micrometer:
        // hikaricp.connections.active       — в использовании
        // hikaricp.connections.idle          — свободные
        // hikaricp.connections.pending       — ждут подключения
        // hikaricp.connections.max           — лимит пула
        // hikaricp.connections.timeout       — количество таймаутов
    }
}

// Алерт в Grafana: если pending > 0 более 10 секунд — проблема
// Алерт: если timeout rate > 1/мин — пул слишком маленький

Kubernetes и networking: тихий убийца

Самая неожиданная проблема — Kubernetes networking. Когда под перезапускается, Kubernetes убивает TCP-соединения, но HikariCP об этом не знает. Поднимается с кучей мёртвых подключений в пуле. Первый запрос после рестарта — exception: connection is closed.

Решение — включить TCP keepalive и правильно настроить max-lifetime:

# PostgreSQL сторона (postgresql.conf):
tcp_keepalives_idle = 60
tcp_keepalives_interval = 10
tcp_keepalives_count = 6
idle_in_transaction_session_timeout = 600000  # 10 мин

# HikariCP сторона:
# max-lifetime ДОЛЖЕН быть меньше idle_in_transaction_session_timeout
hikari:
  max-lifetime: 540000  # 9 минут (на минуту меньше PG таймаута)
  keepalive-time: 120000  # 2 минуты

Также критически важно: max-lifetime должен быть случайным в пределах 10%, чтобы все подключения не переустанавливались одновременно. HikariCP делает это автоматически.

Мониторинг: что смотреть в Grafana

Вот метрики, которые спасали нам жизнь:

// 1. hikaricp_connections_active — сколько подключений сейчас работает
// 2. hikaricp_connections_idle — сколько свободно в пуле
// 3. hikaricp_connections_pending — сколько потоков ждут подключение
// 4. hikaricp_connections_max — лимит пула
// 5. hikaricp_connections_timeout_total — счётчик таймаутов
// 6. hikaricp_connections_creation_seconds — время создания подключения

// Алерты:
// 1. connections.pending > 0 в течение 30 сек → WARN
// 2. connections.timeout_total rate > 0.5/min → CRITICAL
// 3. connections.active / max > 0.8 → WARN
// 4. connections.creation_seconds p99 > 2s → WARN

Итог

HikariCP — отличный пул, но дефолтные настройки опасны для продакшена. Три вещи, которые нужно сделать обязательно: уменьшить connection-timeout до 2–3 секунд, настроить max-lifetime меньше таймаутов PostgreSQL, и вынести метрики пула в мониторинг с алертами. И помните: bigger pool is not better pool.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

12 июня 2026 11 мин

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин