Опирается на правила: PG-CP-001PG-CP-086 из PostgreSQL Style Guide → раздел HikariCP и connection pooling.

Важно знать

  • PostgreSQL = процесс ОС на каждое соединение (~10MB + context switching).
  • Формула Wooldridge: ((core_count × 2) + spindles). Для SSD = 2N+1 ≈ 10-20.
  • «Больше = больше throughput» — миф. Context-switch + lock contention растут квадратично.
  • max_connections PG (default 100) — бюджет на все инстансы всех сервисов.
  • HikariCP: max = min-idle (не ramp-up), connection-timeout: 3s, max-lifetime: 30m.
  • leak-detection-threshold: 60s — бесплатный мониторинг утечек.
  • PgBouncer — для много инстансов / много сервисов / serverless.
  • PgBouncer transaction mode оптимален, но: prepareThreshold: 0, SET LOCAL, нет LISTEN/NOTIFY.
  • Read-replica — отдельный DataSource + HikariCP, routing через readOnly = true.

PostgreSQL держит один процесс ОС на каждое соединение. UCP формулирует — какой размер пула, как мониторить, когда добавить PgBouncer.

Формула Wooldridge

PG-CP-001..003:

connections = ((core_count × 2) + effective_spindle_count)

Для SSD-серверов с N CPU-ядер оптимум 2N+1 ≈ 10-20.

«Больше соединений = больше throughput» — миф:

  • Context-switch между процессами PG имеет цену.
  • Lock contention растёт квадратично.
  • При пуле 20 на 8-core throughput может быть выше, чем при 100.

Целевой размер: 10-20 per instance.

max_connections PG (default 100) распределяется между всеми инстансами всех сервисов. На 10 инстансов × 20 соединений = 200, уже превышает default. Решение — увеличить max_connections (до 300-500 разумно) или PgBouncer.

HikariCP

PG-CP-010..015:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 20                       # = maximum — нет ramp-up
      connection-timeout: 3000               # 3s ждать соединение
      idle-timeout: 600000                   # 10 мин
      max-lifetime: 1800000                  # 30 мин — обновлять соединения
      leak-detection-threshold: 60000        # 60 сек — алёрт на утечку
      validation-timeout: 5000
      auto-commit: false

maximum = minimum-idle

Пул всегда полный. Динамика «то 5, то 20» создаёт latency-всплески при ramp-up на пиках.

connection-timeout 3 сек

Лучше упасть, чем держать поток. На веб-фронте таймаут запроса 30 сек, но 3 сек ожидания пула — индикатор перегрузки, нужна реакция.

max-lifetime 30 мин

Каждое соединение через 30 мин пересоздаётся. Защита от:

  • Утечек памяти PG (накопленный work_mem, prepared statements).
  • «Тихой» поломки соединений за load balancer-ами.

Должен быть меньше server-side idle_in_transaction_session_timeout и LB-таймаутов.

leak-detection-threshold 60 сек

HikariCP логирует stack trace, если соединение не вернулось в пул за минуту. Признак:

  • Забытый close().
  • @Transactional вокруг долгого HTTP-вызова.
  • Висящая транзакция.

Не отключай! Бесплатный мониторинг.

auto-commit

PG-CP-020..021:

auto-commit: false в Spring Boot — обычно правильно. Spring управляет TX через @Transactional.

auto-commit: true требует доп. round-trip к БД на каждый borrow/return — Spring ставит autocommit в false перед @Transactional и обратно после. На high-throughput заметная latency.

Diagnostic

PG-CP-030..033:

management:
  metrics:
    enable:
      hikaricp: true

Метрики:

  • hikaricp.connections.active — занято.
  • hikaricp.connections.idle — свободно.
  • hikaricp.connections.pending — потоки в очереди. > 0 стабильно — пул мал.
  • hikaricp.connections.usage — гистограмма «сколько секунд держали». p99 > секунды — долгие TX.
  • hikaricp.connections.timeout — счётчик connection-timeout.

Алёрты:

  • pending > 0 минуту → увеличить пул или сократить TX.
  • timeout > 0 → утечка или недостаток пула.

PgBouncer

PG-CP-040..047:

Оправдан когда:

  • Много инстансов одного сервиса (десятки).
  • Много разных сервисов на один кластер.
  • Serverless / lambda — частые коротко-живущие воркеры.
  • Нужно ограничить общее число соединений к PG.

Режимы pooling

РежимКогда возвращаетсяЧто не работает
session (default)После disconnect клиентаНикаких ограничений
transactionПосле каждого COMMIT/ROLLBACKServer-side prepared, SET без LOCAL, advisory locks, LISTEN/NOTIFY
statementПосле каждого SQLТранзакции из >1 statement

transaction — обычный выбор. Максимальная утилизация при минимуме ограничений.

Ограничения transaction-mode

На transaction mode не работают server-side prepared statements:

spring:
  datasource:
    hikari:
      data-source-properties:
        prepareThreshold: 0   # отключить server-side prepared

Или PgBouncer 1.21+ умеет prepared-statements caching сам.

SET команды (без LOCAL) теряются после COMMIT. Используй SET LOCAL.

LISTEN/NOTIFY — connection не сохраняется между транзакциями. Отдельный пул в session mode или другой механизм (Kafka, Redis pub/sub).

Размер пулов с PgBouncer

PG-CP-050:

app_pool : pgbouncer_to_pg = 5:1 или больше

Например, HikariCP пул 50 per instance, PgBouncer держит к PG 20 real-соединений. 50 потоков могут одновременно «думать», к PG идёт максимум 20.

[databases]
mydb = host=postgres-master port=5432 dbname=mydb pool_mode=transaction

[pgbouncer]
pool_mode = transaction
default_pool_size = 20
max_client_conn = 1000
reserve_pool_size = 5

Read-replica

PG-CP-060..061: отдельный пул.

@Configuration
public class DataSourceConfig {
    @Bean @Primary
    @ConfigurationProperties("spring.datasource.master")
    public HikariConfig masterConfig() { return new HikariConfig(); }

    @Bean
    @ConfigurationProperties("spring.datasource.replica")
    public HikariConfig replicaConfig() { return new HikariConfig(); }

    @Bean
    public DataSource routingDataSource(DataSource master, DataSource replica) {
        var routing = new TransactionRoutingDataSource();
        routing.setTargetDataSources(Map.of(
            DataSourceType.READ_WRITE, master,
            DataSourceType.READ_ONLY, replica
        ));
        routing.setDefaultTargetDataSource(master);
        return routing;
    }
}

AbstractRoutingDataSource выбирает по TransactionSynchronizationManager.isCurrentTransactionReadOnly()@Transactional(readOnly = true) идёт на реплику.

Replication lag — обычно ms, под нагрузкой может расти до секунд. Не для read-after-write сценариев («только что создал, сразу читаю»).

Чек-лист

  • [ ] maximum-pool-size = minimum-idle = 10-20.
  • [ ] connection-timeout = 3s.
  • [ ] max-lifetime = 30 мин (меньше LB-таймаута).
  • [ ] leak-detection-threshold = 60s включён.
  • [ ] HikariCP-метрики в Micrometer.
  • [ ] Алёрт на connections.pending > 0 минуту.
  • [ ] Алёрт на connections.timeout > 0.
  • [ ] max_connections PG ≥ суммы всех пулов.
  • [ ] PgBouncer: transaction mode (или явное обоснование session).
  • [ ] PgBouncer + JDBC: prepareThreshold = 0 или PG 1.21+.
  • [ ] Read-replica: отдельный пул + routing через readOnly = true.

Что запрещено

АнтипаттернПравилоЧто взамен
maximum-pool-size: 200PG-CP-080формула Wooldridge
Разные пулы на одну БД для одного appPG-CP-081один DataSource
@Transactional вокруг HTTPPG-CP-082вне TX
Отключение leak-detectionPG-CP-083не скрывать
auto-commit: true с @TransactionalPG-CP-084false
PgBouncer session без причиныPG-CP-085transaction
PgBouncer transaction + server-side preparedPG-CP-086prepareThreshold: 0
Read-replica для read-after-writePG-CP-061master для consistency

Куда дальше

  • PG → HikariCP — нормативные формулировки.
  • Spring @Transactional — REQUIRES_NEW удваивает пул.
  • Уровни изоляции — readOnly routing.
  • Блокировки — lock_timeout.
  • WAL — long transactions.
  • Observability → metrics — HikariCP экспорт.