Пул соединений и PgBouncer

О лекции

Каждое клиентское соединение в PostgreSQL — отдельный серверный процесс. Лимит max_connections невелик. Пул в приложении и PgBouncer решают разные этажи одной задачи.

PostgreSQL postgres собеседование

Каждое клиентское соединение в PostgreSQL — отдельный серверный процесс ОС, не легковесная горутина. На соединение выделяются память, слот в proc array, накладные расходы. max_connections часто держат в диапазоне 100–400. Выше — больше RAM и хуже конкуренция за внутренние структуры. Это сознательный потолок, не «поставьте 10 000 как в Node».

Пример. 50 подов по 20 соединений — уже тысяча. Сервер отвечает too many connections. Клиенты ловят таймауты. Деплой «масштабирует» ситуацию.

Пул в приложении ограничивает процесс. Несколько реплик Deployment суммируются. Cron, миграции и админские сессии тоже едят слоты. PgBouncer стоит перед Postgres и мультиплексирует много клиентских соединений в малое число серверных. Это другой этаж: приложение всё ещё должно иметь свой потолок, иначе вы просто перенесёте давку на вход баунсера.

Пул в приложении

В Go это pgxpool или database/sql с SetMaxOpenConns. Считают вслух: replicas × MaxOpenConns + cron + админы < max_connections, плюс запас на SUPERUSER — иначе при аварии вы не зайдёте починить. Слишком маленький пул даёт очередь в приложении. Слишком большой — очередь и OOM на сервере БД.

Утечка соединений выглядит как рост активных бэкендов без роста QPS: забытый sql.Rows без Close, транзакция без Commit/Rollback, healthcheck каждую секунду из каждого пода напрямую в Postgres, минуя пул.

Режимы PgBouncer

  • session pooling — серверный коннект на всю сессию клиента. Простой мультиплексор, экономия небольшая.
  • transaction pooling — серверный коннект только на время транзакции. Экономит процессы. Ломает session-состояние: подготовленные statements в сессии, advisory locks, temp tables, SET без RESET, LISTEN/NOTIFY.
  • statement pooling — ещё жёстче, с ORM почти не используют.

Transaction mode — рабочая лошадка. Цена: приложение не имеет права рассчитывать, что «это тот же бэкенд, что в прошлом запросе». Prepared statements нужно либо unnamed/на транзакцию, либо включать поддержку на стороне баунсера и драйвера (max_prepared_statements и подобное).

pool, err := pgxpool.New(ctx, "postgres://app@pgbouncer:6432/db?pool_max_conns=10")
if err != nil {
	return err
}
defer pool.Close()
// 20 реплик × 10 = 200 клиентских; баунсер держит, например, 50 серверных
// за PgBouncer: transaction mode, без temp tables и session SET

Пул приложения ограничивает процесс. Сумма по репликам деплоя должна оставаться заметно ниже max_connections. За PgBouncer — transaction mode без session state.

Пример. Advisory lock «на время запроса» в transaction mode опасен: лок взяли на одном серверном коннекте, следующий запрос ушёл на другой — лок не снимется или окажется чужим.

Типичная ошибка

Пул «на всякий 100» в каждом поде и надежда, что Postgres сам разберётся. Не разберётся. Вторая ловушка — включить transaction pooling и оставить session-level prepared statements или advisory lock «на время запроса»: лок окажется на чужом бэкенде или не снимется.

На собеседовании

Посчитайте вслух: реплики × MaxOpenConns плюс cron и админы против max_connections, запас на SUPERUSER. Назовите, зачем transaction pooling и что он ломает: prepared statements, temp tables, LISTEN, SET, advisory locks. Пул приложения и PgBouncer — не взаимоисключающие кнопки.