Цена соединения. Новое соединение к PostgreSQL: TCP-рукопожатие, TLS, аутентификация, запуск отдельного серверного процесса (fork) с собственной памятью — единицы миллисекунд и мегабайты памяти на сервере. Делать это на каждый запрос — удвоить задержку и нагрузку. Пул (HikariCP, pgxpool, SQLAlchemy pool) открывает соединения заранее и раздаёт их запросам на время транзакции.
Почему пул должен быть маленьким. Интуиция «больше соединений — больше параллелизма» неверна. База выполняет параллельно столько, сколько у неё ядер и дисковой очереди. Лишние соединения:
- конкурируют за CPU — переключения контекста;
- конкурируют за блокировки и буферы — растёт время каждого запроса;
- тратят память на сервере (work_mem на каждый сортирующий запрос!).
Пропускная способность при 50 соединениях часто выше, чем при 500, а задержка — ниже. Отправная формула (HikariCP): соединений ≈ ядра базы × 2 + число дисков. Дальше — по нагрузочным тестам. Ожидание свободного соединения в пуле лучше, чем очередь внутри базы: его видно метрикой и им можно управлять (таймаут на получение соединения).
Сумма пулов. 40 экземпляров приложения × пул 20 = 800 соединений, а PostgreSQL комфортно держит сотни. Решения:
- уменьшить пулы экземпляров;
- PgBouncer (Odyssey, pgcat) между приложением и базой: тысячи клиентских соединений мультиплексируются на десятки серверных. Режим transaction pooling — серверное соединение выдаётся на время транзакции; ограничения — сессионное состояние (prepared statements исторически,
SET, advisory locks, временные таблицы); - закон Литтла для проверки: RPS к базе × время транзакции = нужное число одновременно занятых соединений.
Практика:
- держать соединение только на время транзакции; не делать внешние HTTP-вызовы, пока соединение взято, — пул кончится;
- таймауты: получение соединения из пула, запрос (
statement_timeout), простаивающая транзакция (idle_in_transaction_session_timeout); - метрики пула: занято, ожидающих, время ожидания;
- проверка живости соединений и пересоздание после переключения мастера.
HTTP-клиенты — та же история: keep-alive и пул соединений на хост экономят TCP- и TLS-рукопожатия; один клиент на приложение, а не новый на каждый запрос (иначе TIME_WAIT и исчерпание портов).