Postgres в проде: соединения и миграции

Почему соединения — дефицитный ресурс, зачем нужен пул и как не положить прод миграцией.

Соединение — дорогая вещь

Каждое подключение к Postgres — отдельный процесс на сервере базы с собственной памятью. Поэтому max_connections — не «поставим 10000 и забудем»: сотни простаивающих соединений съедают память и планировщик.

Классическая ловушка масштабирования: приложение держит пул на 20 соединений — прекрасно. Потом инстансов приложения становится 10, и база получает 200 постоянных подключений. Формула, которую стоит помнить: соединений у базы = размер пула × число инстансов (плюс воркеры, кроны и аналитики с psql).

Пулы

Первый уровень — пул в самом приложении (он есть в каждом драйвере): соединения переиспользуются, а не открываются на каждый запрос.

Когда инстансов много, добавляют PgBouncer — внешний пул между приложениями и базой. Сотни клиентских подключений он обслуживает десятком реальных, в режиме transaction pooling соединение выдаётся только на время транзакции.

Таймауты — обязательны

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

-- на уровне роли приложения
ALTER ROLE app SET statement_timeout = '5s';
ALTER ROLE app SET idle_in_transaction_session_timeout = '30s';

Второй параметр закрывает отдельную беду: транзакция открыта, приложение ушло думать — а база всё это время держит блокировки.

Миграции, которые кладут прод

DDL в Postgres берёт блокировки. Три частых случая:

Миграция Что происходит
ADD COLUMN ... DEFAULT ... В современных версиях — мгновенно; в старых переписывала всю таблицу
CREATE INDEX Блокирует запись на всё время построения
ALTER TABLE ... SET NOT NULL Полное сканирование под блокировкой

Правила выживания:

CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);