Перевод: Database Connections & Connection Pooling — Sagar Shiroya, 18 апреля 2026.

Что такое соединение (connection)?

Когда приложение хочет общаться с базой данных, оно не может просто отправить запрос «в воздух». Нужно установить соединение — выделенный канал связи между приложением и сервером БД. После установки соединения можно отправлять SQL-запросы и получать результаты.

Что происходит внутри PostgreSQL при открытии соединения

PostgreSQL использует модель process-per-connection. При каждом подключении клиента PostgreSQL форкает новый процесс ОС (postmaster spawning backend process).

Каждый backend-процесс:

Именно поэтому соединения дорогие. Это не просто сокет — это процесс ОС с реальной памятью.

PostgreSQL имеет жёсткий лимит одновременных соединений — параметр max_connections (по умолчанию 100).

Жизненный цикл соединения

  1. Открытие (20-100 мс в зависимости от сети):

    • TCP 3-way handshake
    • SSL/TLS negotiation (если включён)
    • Аутентификация (логин + пароль)
    • Форк backend-процесса
  2. Выполнение SQL: отправка запросов, получение результатов.

  3. Закрытие: backend-процесс убивается, память освобождается, сокет закрывается.

Если приложение открывает и закрывает соединение на каждый запрос, шаги 1 и 3 легко могут превысить время самого запроса. Запрос на 2 мс обходится значительно дороже из-за накладных расходов.

Сколько занимает открытие соединения?

РасположениеЗадержка
Один дата-центр, одна стойка1-5 мс
Один дата-центр, разные стойки5-20 мс
Разные дата-центры50-200 мс
Разные регионы800-1500 мс

Решение: Connection Pooling

Connection pool — кеш заранее установленных соединений с БД, которые переиспользуются вместо открытия/закрытия на каждый запрос.

Идея проста: вместо создания нового соединения приложение берёт соединение из пула, использует его, и возвращает обратно. Пул управляет набором долгоживущих соединений.

Как работает пул

  1. Инициализация: при старте приложения пул открывает минимальное число соединений (например, 3 или 5) и держит их живыми.
  2. Borrow: приходит запрос, пул выдаёт свободное соединение. Если все заняты — запрос ждёт в очереди.
  3. Use: запрос выполняет SQL на полученном соединении.
  4. Return: после завершения соединение возвращается в пул. Оно не закрывается — сбрасывается и становится доступным.
  5. Health Checks: пул периодически проверяет, что idle-соединения живы, и заменяет протухшие.
JAVASCRIPT
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

Database-side pool

Что происходит, когда все соединения заняты?

Если все соединения пула используются, новый запрос ставится в очередь ожидания. Он будет обслужен, как только соединение освободится. Если ожидание превышает connectionTimeoutMillis, пул выбрасывает ошибку — это правильное поведение, предотвращающее неконтролируемый рост очереди и исчерпание памяти.

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

Закон Литтла — математика за пулингом

Connection pooling подкреплён принципом из теории очередей — законом Литтла:

PLAINTEXT
L = λ × W
Нажмите, чтобы развернуть и увидеть больше

Пример:

PLAINTEXT
L = 50 × 0.02 = 1 соединение
Нажмите, чтобы развернуть и увидеть больше
PLAINTEXT
L = 50 × 0.2 = 10 соединений
Нажмите, чтобы развернуть и увидеть больше

Вывод: латентность запросов напрямую определяет потребность в соединениях. Медленные запросы — не только проблема UX, но и проблема масштабирования инфраструктуры.

Максимальная пропускная способность пула:

PLAINTEXT
max_throughput = pool_size / avg_service_time
Нажмите, чтобы развернуть и увидеть больше

Пул из 10 соединений при среднем запросе 10 мс обрабатывает 1000 req/s. Сверх этого — очередь растёт.

Формула Кингмана — почему 100% утилизация опасна

Даже если пул достаточен в среднем, при высокой утилизации могут возникать большие очереди. Формула Кингмана показывает: длина очереди растёт нелинейно (экспоненциально) при приближении утилизации к 100%.

Правило: никогда не эксплуатируйте пул выше 70-80% утилизации.

После этого порога даже небольшие всплески трафика вызывают непропорционально длинные очереди. Это справедливо для любых разделяемых ресурсов: CPU, сетевая полоса, соединения с БД.

Первое действие при исчерпании пула — не увеличивать его размер, а уменьшить латентность запросов.

Размер пула: соотношение процессов и ядер

БД может выполнять параллельно столько запросов, сколько у неё CPU-ядер. Больше соединений, чем ядер, не ускоряет БД — она замедляется из-за context switch между процессами.

Формула:

PLAINTEXT
pool_size = (core_count × 2) + effective_spindle_count
Нажмите, чтобы развернуть и увидеть больше

Для SSD или managed облачных БД (RDS, Supabase) — effective_spindle_count = 1.

Пример: сервер с 4 CPU и SSD:

PLAINTEXT
pool_size = (4 × 2) + 1 = 9 соединений
Нажмите, чтобы развернуть и увидеть больше

Кажется мало? Да, 9 соединений для 500 concurrent users. Потому что эти 9 соединений обслуживают запросы параллельно на скорости CPU, а пользователи в основном ждут I/O, а не CPU.

Урок Uber/Postgres

Uber задокументировали, что уменьшение числа соединений (через PgBouncer) и оптимизация запросов улучшили пропускную способность больше, чем увеличение пула. Маленький загруженный пул работает лучше большого с context switching.

Учитывайте количество инстансов приложения

PLAINTEXT
total_connections = pool_size × num_app_instances
Нажмите, чтобы развернуть и увидеть больше

Если 10 инстансов приложения с пулом по 20 — это 200 соединений. При max_connections = 100 в PostgreSQL часть будет отклонена.

Ключевые выводы

  1. Соединение с БД — реальный процесс ОС. Открытие стоит 20-100 мс и 5-10 МБ RAM.

  2. Всегда используйте connection pool в продакшне. Сырые соединения не масштабируются.

  3. Размер пула — не «чем больше, тем лучше». Правильный размер: (cores × 2) + spindles. Больше соединений, чем ядер — overhead от context switch.

  4. Закон Литтла связывает скорость запросов с потребностью в соединениях. Медленные запросы потребляют соединения дольше. Оптимизируйте запросы в первую очередь.

  5. Не превышайте 70-80% утилизации пула. Формула Кингмана: латентность взрывается нелинейно выше этого порога.

  6. Учитывайте все инстансы приложения. total_connections = pool_size × num_instances должно быть меньше max_connections.

  7. Для serverless — используйте connection proxy. Функции с cold start не должны открывать прямые соединения. PgBouncer, RDS Proxy, Supabase Pooler — обязательны.

Авторские права

Автор: Vasiliy Fakunin

Ссылка: https://notes.melancholic.tech/posts/%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F-%D0%BA-%D0%B1%D0%B0%D0%B7%D0%B5-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85-%D0%B8-connection-pooling/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша