awesome-everything EN
↑ Обратно к восхождению

Базы данных

Connection pool: зачем амортизировать стоимость backend Postgres

Суть Каждое соединение с Postgres форкает тяжёлый OS-процесс; пул кэширует небольшой набор открытых соединений, чтобы запросы никогда не платили стоимость установки заново.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 8 min

Отто пишет new Client(); client.connect(); await client.query(...); client.end() на каждый запрос. P99 = 80 мс — почти всё это connect. Свен переходит на пул. P99 падает до 4 мс: та же база, те же запросы.

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

Открытие соединения с Postgres — не дёшево: TCP handshake, TLS handshake, Postgres protocol startup, аутентификация и форк backend OS-процесса. В сумме: 5–50 мс на новое соединение и ~10 МБ памяти на backend, которую Postgres должен выделить.

Делать это на каждый веб-запрос превращает setup соединения в доминирующую стоимость — до того как выполнится хоть один байт SQL.

Пул амортизирует стоимость: открываем соединения один раз при старте, переиспользуем для тысяч запросов. Накладные расходы пула на запрос — поиск в списке за долю миллисекунды, а не полный OS round-trip.

Метафора: стоянка такси

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

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

Без пулаС пулом
Новые TCP + TLS + auth на запрос: 5–50 мс накладных расходовБерём idle-соединение: <1 мс
Каждый backend: ~10 МБ памяти PostgresПул держит 5–20 соединений; память фиксирована
40 worker × 20 соединений = 800 — превышает max_connections=100PgBouncer мультиплексирует 800 клиентов на 50 backend

Два слоя: client-side и server-side

Современные стеки используют два слоя пулов:

  1. Client-side pool — живёт внутри каждого процесса приложения (node-postgres Pool, HikariCP для Java, asyncpg для Python). Кэширует 5–20 TCP-соединений на worker. Убирает стоимость connect на каждый запрос.

  2. Server-side pooler (PgBouncer, Supavisor, Odyssey) — отдельный процесс перед Postgres. Принимает тысячи клиентских соединений от всех worker и маршрутизирует их на маленький набор реальных backend Postgres. Убирает проблему лимита max_connections.

Комбинация: каждый worker имеет ~10 дешёвых client-pool соединений к PgBouncer; PgBouncer держит 20–50 реальных backend. 10 000 клиентов приложения становятся устойчивыми на Postgres с max_connections = 100.

Почему это работает

Почему Postgres использует один OS-процесс на соединение, а не потоки? История: Postgres создавался до появления надёжных pthreads на большинстве Unix-систем. Модель процессов изолирует сбои (OOM одного backend не портит других), упрощает управление памятью — у каждого backend собственная куча. Цена — неустранимые накладные расходы на соединение, которые пулы и призваны поглощать.

Сбой при масштабировании без server-side pooler

Команда запускает 4 worker, client pool max: 20, Postgres max_connections = 100. Масштабируют до 40 worker. Итого соединений: 40 × 20 = 800. Postgres жёстко ограничивает 100. Результат: FATAL: sorry, too many clients already на 700 попыток. Приложение падает.

Фикс: добавляем PgBouncer. Worker подключаются к PgBouncer (8000 client-соединений — дёшево, ~2 КБ каждое). PgBouncer держит 50 реальных backend. Итого backend Postgres: 50. Проблема исчезает.

Викторина

Почему открытие нового соединения с Postgres на каждый HTTP-запрос — плохая идея?

Викторина

У worker client pool max 20. Масштабируете до 40 worker, Postgres имеет max_connections=100. Что произойдёт?

Расставь шаги по порядку

Расположи шаги, происходящие при заимствовании соединения из пула:

  1. 1 Код приложения вызывает pool.query(sql, params)
  2. 2 Пул ищет idle-соединение в кэше
  3. 3 Если есть — заимствует; если нет и под max — открывает новое; если на max — ждёт или отвергает
  4. 4 Пул отправляет запрос Postgres через заимствованное соединение
  5. 5 Postgres выполняет запрос и возвращает строки
  6. 6 Пул возвращает соединение в idle-список — НЕ закрывает его
  7. 7 Код приложения получает строки и продолжает
Закончи аналогию

Заполни пропуск: connection pool относится к соединениям Postgres так же, как пул потоков относится к OS-___ — небольшой кэш дорогих переиспользуемых ресурсов, амортизирующий стоимость setup на множество коротких задач.

Вспомните перед уходом
  1. 01
    Двумя предложениями: что такое connection pool и почему он всегда нужен в продакшн Postgres?
  2. 02
    Что решает каждый из двух слоёв пула?
  3. 03
    Почему Postgres использует один OS-процесс на соединение и каков практический вывод для пулинга?
Итог

Postgres создаёт один OS-процесс на соединение — каждый backend потребляет ~10 МБ и занимает слот в max_connections (по умолчанию 100). Открытие соединения стоит 5–50 мс TCP + TLS + auth overhead. Connection pool решает оба вопроса: client-side pool кэширует соединения на worker, чтобы запросы никогда не платили setup; server-side pooler, например PgBouncer, мультиплексирует тысячи клиентских соединений на несколько десятков реальных backend, удерживая количество backend Postgres ниже max_connections. Без обоих слоёв масштабирование с 4 до 40 worker по 20 соединений каждый создаёт 800 попыток соединения против 100-слотового лимита — гарантированные ошибки.

Связанные уроки
встречается в258
Продолжить восхождение ↑Режимы PgBouncer: session, transaction и statement
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.