PostgreSQLПроизводительностьDevOps

Connection pooling в PostgreSQL: зачем нужен PgBouncer

5 мин чтения

Под нагрузкой приложение вдруг начинает падать с ошибкой FATAL: sorry, too many clients already. База жива, запросы лёгкие, а соединения кончились. Это упор в число подключений PostgreSQL — типичная стена, в которую проект въезжает при росте трафика или числа инстансов. Разберём, почему соединения в Postgres дорогие и как помогает пул.

Почему соединения дорогие

PostgreSQL на каждое клиентское соединение запускает отдельный процесс операционной системы (не поток). Каждый такой процесс занимает память, а их общее число ограничено параметром max_connections — по умолчанию около 100. Установка соединения тоже не бесплатна: форк процесса, аутентификация, инициализация.

Пока соединений десятки — это незаметно. Проблема появляется, когда их начинают открывать много и одновременно.

Откуда берётся «too many clients»

Число реальных соединений складывается перемножением. Допустим, у приложения свой пул на 20 соединений. Запущено 4 веб-воркера gunicorn — уже 80. Добавили второй инстанс приложения — 160. Плюс фоновые воркеры со своими подключениями. И всё это упирается в max_connections = 100.

Особенно остро это в serverless и при автоскейлинге: каждый новый экземпляр открывает свои соединения, и их число растёт неконтролируемо. Просто поднять max_connections — плохое решение: каждое соединение ест память, и сотни процессов сами по себе замедляют сервер.

Что делает пул соединений

Пул соединений — посредник между приложением и базой. Он держит небольшой постоянный набор реальных соединений к PostgreSQL и переиспользует их, раздавая приложению по запросу. Приложение «думает», что у него много соединений, а к базе их идёт мало и они не пересоздаются на каждый запрос.

Самый распространённый внешний пул для PostgreSQL — PgBouncer. Он лёгкий, ставится перед базой, и приложение подключается к нему вместо неё.

Режимы пулинга PgBouncer

У PgBouncer три режима, и разница между ними принципиальна:

  • session — соединение к базе закрепляется за клиентом на всё время его сессии. Безопасно, совместимо со всем, но экономия минимальна.
  • transaction — соединение выдаётся на время одной транзакции и возвращается в пул сразу после неё. Самый эффективный режим: десятки клиентов делят единицы соединений.
  • statement — соединение возвращается после каждого запроса. Самый агрессивный, с жёсткими ограничениями.

На практике берут transaction pooling — но у него есть цена.

Подводный камень transaction pooling

В режиме transaction соединение между транзакциями переходит к другому клиенту, поэтому всё, что живёт на соединении, ломается:

  • prepared statements — драйверы вроде asyncpg и psycopg готовят запросы на соединении; в transaction-режиме это даёт ошибки. Обычно лечится отключением кэша prepared statements в драйвере.
  • SET на сессию, advisory locks, LISTEN/NOTIFY, временные таблицы — всё это привязано к соединению и в transaction-режиме работать не будет.

Поэтому переход на transaction pooling — не «просто включить», а проверить, что приложение не полагается на состояние соединения.

Пул в приложении — это не то же самое

У ORM и драйверов есть свой пул (например, в SQLAlchemy). Он живёт внутри одного процесса приложения и переиспользует соединения только этого процесса. Когда инстансов и воркеров много, их пулы суммируются и упираются в max_connections — здесь и нужен общий внешний пул вроде PgBouncer перед базой. Один не отменяет другой: внутренний пул экономит на пересоздании соединений в процессе, внешний — ограничивает их общее число к базе.

Как подключить на практике

PgBouncer описывается коротким конфигом: какие базы проксировать, режим пула и его размеры.

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

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

Приложение может открыть до max_client_conn соединений к самому PgBouncer, а тот держит к базе не больше default_pool_size на каждую пару «база — пользователь». В приложении при этом меняется только строка подключения: вместо порта PostgreSQL (5432) указывается порт PgBouncer (по умолчанию 6432). Код трогать не нужно.

Частые грабли

  • Подняли max_connections вместо пула. Сотни процессов съедают память и замедляют сервер — проблему отложили, а не решили.
  • transaction pooling без проверки. Prepared statements и SET-настройки начинают давать ошибки.
  • Долгие транзакции. Транзакция, висящая в ожидании, держит соединение из пула и не отдаёт его — пул быстро исчерпывается.
  • Слишком большой пул. Если размер пула близок к max_connections, смысл пулинга теряется.

Когда это нужно

  • Несколько инстансов или много воркеров приложения суммарно открывают больше соединений, чем тянет база.
  • Появляются ошибки too many clients под нагрузкой.
  • Serverless или автоскейлинг с непредсказуемым числом экземпляров.

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


Грамотная работа с соединениями — часть эксплуатации базы наравне с бэкапами и переносом без простоя. На Hostim managed Postgres поднимается с разумными настройками по умолчанию, а пул соединений подключается перед базой без отдельной установки и сопровождения PgBouncer — приложение получает стабильное число соединений независимо от того, сколько у него инстансов и воркеров.

Читать дальше