Перевод: Database Connections & Connection Pooling — Sagar Shiroya, 18 апреля 2026.
Что такое соединение (connection)?
Когда приложение хочет общаться с базой данных, оно не может просто отправить запрос «в воздух». Нужно установить соединение — выделенный канал связи между приложением и сервером БД. После установки соединения можно отправлять SQL-запросы и получать результаты.
Что происходит внутри PostgreSQL при открытии соединения
PostgreSQL использует модель process-per-connection. При каждом подключении клиента PostgreSQL форкает новый процесс ОС (postmaster spawning backend process).
Каждый backend-процесс:
- Выделяет собственную память (обычно 5-10 МБ на соединение)
- Поддерживает сессию аутентификации
- Имеет собственное состояние транзакций и буферный кеш
Именно поэтому соединения дорогие. Это не просто сокет — это процесс ОС с реальной памятью.
PostgreSQL имеет жёсткий лимит одновременных соединений — параметр max_connections (по умолчанию 100).
Жизненный цикл соединения
Открытие (20-100 мс в зависимости от сети):
- TCP 3-way handshake
- SSL/TLS negotiation (если включён)
- Аутентификация (логин + пароль)
- Форк backend-процесса
Выполнение SQL: отправка запросов, получение результатов.
Закрытие: backend-процесс убивается, память освобождается, сокет закрывается.
Если приложение открывает и закрывает соединение на каждый запрос, шаги 1 и 3 легко могут превысить время самого запроса. Запрос на 2 мс обходится значительно дороже из-за накладных расходов.
Сколько занимает открытие соединения?
| Расположение | Задержка |
|---|---|
| Один дата-центр, одна стойка | 1-5 мс |
| Один дата-центр, разные стойки | 5-20 мс |
| Разные дата-центры | 50-200 мс |
| Разные регионы | 800-1500 мс |
Решение: Connection Pooling
Connection pool — кеш заранее установленных соединений с БД, которые переиспользуются вместо открытия/закрытия на каждый запрос.
Идея проста: вместо создания нового соединения приложение берёт соединение из пула, использует его, и возвращает обратно. Пул управляет набором долгоживущих соединений.
- Active connections — те, на которых прямо сейчас выполняются запросы.
- Idle connections — ожидают входящих запросов.
- Idle-соединения не бесплатны: каждое блокирует 10-30 МБ памяти.
Как работает пул
- Инициализация: при старте приложения пул открывает минимальное число соединений (например, 3 или 5) и держит их живыми.
- Borrow: приходит запрос, пул выдаёт свободное соединение. Если все заняты — запрос ждёт в очереди.
- Use: запрос выполняет SQL на полученном соединении.
- Return: после завершения соединение возвращается в пул. Оно не закрывается — сбрасывается и становится доступным.
- Health Checks: пул периодически проверяет, что idle-соединения живы, и заменяет протухшие.
const { Pool } = require("pg");
const pool = new Pool({
host: "localhost",
database: "myapp",
min: 2, // всегда держать 2 соединения
max: 10, // не более 10 одновременно
idleTimeoutMillis: 30000, // закрывать idle после 30с
connectionTimeoutMillis: 2000, // ошибка если пул исчерпан
});
async function getUser(id) {
// берёт соединение, выполняет запрос, возвращает автоматически
const res = await pool.query("SELECT * FROM users WHERE id = $1", [id]);
return res.rows[0];
}Application-side pool
- Живёт внутри процесса приложения.
- Гибкая настройка под конкретные нужды приложения.
- Примеры:
pg.Pool(Node.js), HikariCP (Java).
Database-side pool
- Отдельный прокси-процесс между приложением и PostgreSQL.
- Контролируется на стороне БД, меньше кастомизации.
- Примеры: PgBouncer, Odyssey.
Что происходит, когда все соединения заняты?
Если все соединения пула используются, новый запрос ставится в очередь ожидания. Он будет обслужен, как только соединение освободится. Если ожидание превышает connectionTimeoutMillis, пул выбрасывает ошибку — это правильное поведение, предотвращающее неконтролируемый рост очереди и исчерпание памяти.
Пул, постоянно заполненный до максимума с растущей очередью — сигнал, что пул слишком мал или запросы слишком медленные. Не увеличивайте пул бездумно — это может ухудшить ситуацию.
Закон Литтла — математика за пулингом
Connection pooling подкреплён принципом из теории очередей — законом Литтла:
L = λ × W- L — среднее число запросов в системе (обслуживаемые + ожидающие)
- λ — средняя частота поступления запросов (req/s)
- W — среднее время запроса в системе
Пример:
- 50 запросов/с, каждый держит соединение 20 мс (0.02 с):
L = 50 × 0.02 = 1 соединение- Но если медленный запрос увеличивает время до 200 мс:
L = 50 × 0.2 = 10 соединенийВывод: латентность запросов напрямую определяет потребность в соединениях. Медленные запросы — не только проблема UX, но и проблема масштабирования инфраструктуры.
Максимальная пропускная способность пула:
max_throughput = pool_size / avg_service_timeПул из 10 соединений при среднем запросе 10 мс обрабатывает 1000 req/s. Сверх этого — очередь растёт.
Формула Кингмана — почему 100% утилизация опасна
Даже если пул достаточен в среднем, при высокой утилизации могут возникать большие очереди. Формула Кингмана показывает: длина очереди растёт нелинейно (экспоненциально) при приближении утилизации к 100%.
Правило: никогда не эксплуатируйте пул выше 70-80% утилизации.
После этого порога даже небольшие всплески трафика вызывают непропорционально длинные очереди. Это справедливо для любых разделяемых ресурсов: CPU, сетевая полоса, соединения с БД.
Первое действие при исчерпании пула — не увеличивать его размер, а уменьшить латентность запросов.
Размер пула: соотношение процессов и ядер
БД может выполнять параллельно столько запросов, сколько у неё CPU-ядер. Больше соединений, чем ядер, не ускоряет БД — она замедляется из-за context switch между процессами.
Формула:
pool_size = (core_count × 2) + effective_spindle_countДля SSD или managed облачных БД (RDS, Supabase) — effective_spindle_count = 1.
Пример: сервер с 4 CPU и SSD:
pool_size = (4 × 2) + 1 = 9 соединенийКажется мало? Да, 9 соединений для 500 concurrent users. Потому что эти 9 соединений обслуживают запросы параллельно на скорости CPU, а пользователи в основном ждут I/O, а не CPU.
Урок Uber/Postgres
Uber задокументировали, что уменьшение числа соединений (через PgBouncer) и оптимизация запросов улучшили пропускную способность больше, чем увеличение пула. Маленький загруженный пул работает лучше большого с context switching.
Учитывайте количество инстансов приложения
total_connections = pool_size × num_app_instancesЕсли 10 инстансов приложения с пулом по 20 — это 200 соединений. При max_connections = 100 в PostgreSQL часть будет отклонена.
Ключевые выводы
Соединение с БД — реальный процесс ОС. Открытие стоит 20-100 мс и 5-10 МБ RAM.
Всегда используйте connection pool в продакшне. Сырые соединения не масштабируются.
Размер пула — не «чем больше, тем лучше». Правильный размер:
(cores × 2) + spindles. Больше соединений, чем ядер — overhead от context switch.Закон Литтла связывает скорость запросов с потребностью в соединениях. Медленные запросы потребляют соединения дольше. Оптимизируйте запросы в первую очередь.
Не превышайте 70-80% утилизации пула. Формула Кингмана: латентность взрывается нелинейно выше этого порога.
Учитывайте все инстансы приложения.
total_connections = pool_size × num_instancesдолжно быть меньшеmax_connections.Для serverless — используйте connection proxy. Функции с cold start не должны открывать прямые соединения. PgBouncer, RDS Proxy, Supabase Pooler — обязательны.