
PostgreSQL создаёт отдельный серверный процесс под каждое клиентское соединение. Каждый такой процесс потребляет память (обычно от 5 до 15 МБ), CPU на переключение контекста и участвует в планировании задач. Когда у вас десятки сервисов, фоновые воркеры, задачи и веб‑процессы, суммарное число соединений легко уходит за сотни. В итоге:
Пул соединений решает сразу три задачи:
На практике грамотный пул уменьшает число подключений в 5–10 раз и снижает CPU на 30–40% на узлах базы. Это прямые деньги и меньше инцидентов.
Есть два уровня пула:
Пул в приложении/ORM. Например, SQLAlchemy имеет встроенный пул, Django переиспользует соединения в рамках процесса. Это помогает, но не защищает от всплесков при большом числе процессов/воркеров и не даёт кросс‑процессного пула.
Внешний прокси‑пул — PgBouncer. Он держит ограниченное число серверных соединений к PostgreSQL и распределяет их между большим числом клиентских подключений от приложений.
PgBouncer поддерживает три режима:
Для веб‑API и фоновых задач чаще всего подходит transaction, он даёт лучший баланс между совместимостью и экономией ресурсов.
Ниже — минимальная, но рабочая сборка для локального стенда и тестовой среды.
yaml
version: "3.9"
services:
postgres:
image: postgres:16
container_name: pg
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_pass
POSTGRES_DB: app
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app"]
interval: 5s
timeout: 3s
retries: 10
volumes:
- pg_data:/var/lib/postgresql/data
pgbouncer:
image: edoburu/pgbouncer:1.23
container_name: pgb
depends_on:
postgres:
condition: service_healthy
ports:
- "6432:6432"
volumes:
- ./pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
- ./pgbouncer/userlist.txt:/etc/pgbouncer/userlist.txt:ro
volumes:
pg_data:
Конфигурация PgBouncer:
ini
; ./pgbouncer/pgbouncer.ini
[databases]
app = host=pg port=5432 dbname=app user=app_user password=app_pass
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
;
; Аутентификация. Для локального/тестового стенда оставим plain.
; В проде используйте md5/scram и TLS.
auth_type = plain
auth_file = /etc/pgbouncer/userlist.txt
; Режим пула
pool_mode = transaction
; Ограничения и размеры пулов
max_client_conn = 1000
default_pool_size = 50
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 5
; Таймауты и гигиена соединений
server_idle_timeout = 600
server_lifetime = 3600
query_timeout = 600
idle_transaction_timeout = 60
; Сброс состояния после транзакции
server_reset_query = DISCARD ALL
; Админ-доступ для SHOW команд
admin_users = app_user
log_connections = 1
log_disconnections = 1
stats_period = 60
Файл пользователей:
ini
; ./pgbouncer/userlist.txt
"app_user" "app_pass"
Подключаемся:
bash
# Прямо к PgBouncer (порт 6432)
psql "host=127.0.0.1 port=6432 dbname=app user=app_user password=app_pass"
# Консоль PgBouncer (служебная БД "pgbouncer")
psql "host=127.0.0.1 port=6432 dbname=pgbouncer user=app_user password=app_pass" -c "SHOW POOLS;"
Django не содержит полноценного пула, но умеет переиспользовать соединение в процессе. С PgBouncer в режиме transaction используйте такие настройки:
python
# settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "app",
"USER": "app_user",
"PASSWORD": "app_pass",
"HOST": "127.0.0.1",
"PORT": "6432",
# Закрывать соединение после запроса — PgBouncer сам управляет пулом
"CONN_MAX_AGE": 0,
"OPTIONS": {
"connect_timeout": 5,
# Лимит на длительность запроса (в мс)
"options": "-c statement_timeout=5000",
},
}
}
# Если используете потоковую выборку .iterator(), отключите серверные курсоры
DATABASES["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
Совет:
python
from sqlalchemy import create_engine, text
engine = create_engine(
"postgresql+psycopg2://app_user:app_pass@127.0.0.1:6432/app",
pool_size=5, # на процесс
max_overflow=0, # не раздувать пул
pool_pre_ping=True, # проверка соединений перед выдачей
pool_recycle=1800, # пересоздавать раз в 30 минут
connect_args={
"connect_timeout": 5,
"options": "-c statement_timeout=5000",
},
)
with engine.begin() as conn:
conn.execute(text("CREATE TABLE IF NOT EXISTS t(id serial PRIMARY KEY, v text)"))
conn.execute(text("INSERT INTO t(v) VALUES (:v)"), {"v": "hello"})
rows = conn.execute(text("SELECT * FROM t"))
print(rows.all())
В режиме transaction у PgBouncer нежелательны подготовленные запросы (prepared statements), а в asyncpg они включены по умолчанию. Их стоит отключить:
python
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
async def main():
engine = create_async_engine(
"postgresql+asyncpg://app_user:app_pass@127.0.0.1:6432/app",
pool_size=5,
max_overflow=0,
pool_pre_ping=True,
connect_args={
# отключаем кэш подготовленных запросов
"statement_cache_size": 0,
"timeout": 5,
},
)
async with engine.begin() as conn:
await conn.execute(text("SELECT 1"))
await engine.dispose()
asyncio.run(main())
Посчитайте конкурентность на уровне процессов приложения. Пример: 4 сервиса, каждый по 10 воркеров, каждый воркер делает до 5 параллельных обращений к БД → максимум 200 одновременных запросов.
Задайте default_pool_size в PgBouncer с запасом 20–30% от этого числа. Например, 260–280. Если вы делите по БД/пользователю, настраивайте по секции [databases] точечно.
max_client_conn должен быть больше потенциальных внешних клиентов (все процессы приложений) — например, 1000.
На стороне PostgreSQL установите разумный max_connections, например 200–300. Помните: каждый бэкенд — это память. Слишком большое значение съест RAM и ухудшит планирование задач.
Оставьте резерв под служебные подключения (админ, бэкапы): reserve_pool_size и пара десятков соединений в max_connections.
Пример:
sql
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
ALTER SYSTEM SET statement_timeout = '60s';
SELECT pg_reload_conf();
В PgBouncer:
На клиенте (ORM/драйвер):
Все три уровня нужны, чтобы гарантировать ограничение “плохих” случаев и быстро освобождать ресурсы.
Проверка пулов в PgBouncer:
sql
-- Подключение к БД pgbouncer
SHOW POOLS;
SHOW STATS;
SHOW CLIENTS;
SHOW SERVERS;
Полезные метрики:
На стороне PostgreSQL:
sql
SELECT state, count(*) FROM pg_stat_activity GROUP BY 1;
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
GROUP BY 1, 2
ORDER BY count DESC;
Также стоит собирать:
Экспорт метрик: pgbouncer‑exporter для Prometheus, готовые дашборды Grafana.
LISTEN/NOTIFY. В режиме transaction сессия не фиксирована, поэтому такие сценарии ненадёжны. Используйте session режим для отдельного соединения слушателя или подключайтесь к PostgreSQL в обход PgBouncer для канала уведомлений.
Временные таблицы и переменные сессии. Они “живут” в рамках сессии, а не транзакции. Избегайте или переходите на session режим для конкретного кода.
Подготовленные запросы (prepared statements). В transaction режиме они не работают, потому что следующая транзакция может уйти на другой серверный бэкенд. Отключайте кэш подготовленных запросов в драйвере (см. параметр statement_cache_size=0 для asyncpg; для JDBC — prepareThreshold=0; для некоторых ORM — соответствующие флаги).
Сессионные advisory‑lock’и. В transaction режиме используйте транзакционные варианты (pg_advisory_xact_lock) вместо сессионных.
Длинные транзакции. Они блокируют автоочистку и удерживают версии строк. Дробите батчи, коммитьте чаще, не делайте сетевые вызовы внутри транзакции.
Отмена запросов. В некоторых версиях и конфигурациях отмена (cancel) из клиента может не попасть в нужный бэкенд. Проверяйте, что параметры сервиса и PgBouncer согласованы, и тестируйте.
Компания с 5 микросервисами и пиком 1500 RPS держала ~800 соединений к PostgreSQL. Всплески приводили к “Too many connections”, API временами “замерзало”. После внедрения PgBouncer (transaction), пересчёта пулов в приложениях и настройки таймаутов:
Инфраструктурные расходы сократились за счёт отказа от ещё одного реплики и уменьшения размера инстанса БД.
Итог: пул соединений и PgBouncer — это недорогой и быстро внедряемый способ стабилизировать работу БД, ускорить API и снизить затраты. Главное — осознанно выбрать режим пула, выставить лимиты и таймауты и не забыть про наблюдаемость.