open atlas
↑ К треку
Базы данных DB · 05 · 03

Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стек

Пропускная способность Postgres достигает пика при (ядра × 2) + шпиндели активных backend''''ов; клиентский пул на воркер определяется как concurrent_requests × db_calls × avg_ms / budget_ms; многоуровневая математика удерживает оба параметра в границах.

DB Middle ◷ 12 min
Уровень
ОсновыJuniorMiddleSenior

Команда добавляет PgBouncer и устанавливает pool_size = 200 — по числу клиентов. P99 не падает, а растёт. Добавление backend’ов к Postgres сделало всё медленнее. Формула размера пула — не «одно соединение на клиента».

Почему больше Postgres backend’ов не означает больше пропускной способности

Каждый Postgres backend — это отдельный OS-процесс. После нескольких кратного числа ядер CPU дополнительные активные backend’ы перестают давать прирост пропускной способности и начинают её снижать: больше переключений контекста, больше конкуренции за общие структуры памяти (proc array при снятии снапшота, таблица блокировок), больше накладных расходов ядра на планирование.

Формула, популяризированная HikariCP, для оптимального числа активных backend’ов на стороне Postgres:

pool_size = (ядра CPU × 2) + effective_spindle_count
  • ядра × 2 покрывает CPU-параллелизм и часть эффекта hyper-threading
  • effective_spindle_count добавляет запас для I/O-параллелизма, когда хранилище может обслуживать несколько параллельных чтений

На современных NVMe при хорошо прогретом кэше число шпинделей фактически равно 0 — формула сводится к ядра × 2. На 16-ядерном Postgres-сервере с данными в RAM: pool_size ≈ 32, а не 200.

Это pool_size (или default_pool_size) в PgBouncer, а не число воркеров приложения.

СценарийРезультат формулыПримечания
8 ядер, NVMe, данные в RAM(8 × 2) + 0 = 16+50% запас → pool_size = 24
16 ядер, NVMe, данные в RAM(16 × 2) + 0 = 32+50% запас → pool_size = 48
8 ядер, HDD RAID, холодные данные(8 × 2) + 6 = 22+50% → pool_size = 33
Наивный подход «по числу клиентов»200–1000Пропускная способность Postgres деградирует; не делайте так

Размер клиентского пула на воркер

Клиентский пул (node-postgres, HikariCP, asyncpg) размеряется иначе — по внутреннему параллелизму воркера, а не по числу ядер Postgres. Когда видишь медленный API и замечаешь, что каждый запрос ждёт checkout из пула, формула ниже даёт точный ответ, сколько соединений нужно этому воркеру:

worker_pool_size ≈ concurrent_requests × db_calls_per_request × avg_query_ms / request_budget_ms

Пример: 100 параллельных запросов на воркер, 2 вызова БД по 5 мс, бюджет запроса 50 мс:

= 100 × 2 × 5 / 50 = 20 соединений

При 10 воркерах: 200 клиентских соединений к PgBouncer — дёшево (~2 КБ на соединение). PgBouncer мультиплексирует их на пул из ~24 backend’ов.

Конкретный пример расчёта пула

Расчёт пула для Node.js API за PgBouncer в transaction mode

1/3

Ключевые параметры PgBouncer

pool_size (целевой размер backend-пула на БД, по умолчанию 20) — устанавливайте в (ядра × 2) + шпиндели + ~50% запас. Это потолок реальных Postgres backend’ов, которые поддерживает PgBouncer.

reserve_pool_size — дополнительные backend’ы, которые PgBouncer может поднять под пиковую нагрузку (по умолчанию 0; устанавливайте ~25% от pool_size).

max_client_conn — максимальное число входящих клиентских соединений PgBouncer (по умолчанию 100; устанавливайте в тысячи — клиентские соединения дёшевы).

server_reset_query = DISCARD ALL — SQL, выполняемый против backend’а перед возвратом в пул; очищает утёкшее состояние сессии.

query_wait_timeout — сколько клиент может ждать backend’а перед отклонением (по умолчанию 120 с; в production: 10–30 с для быстрого отказа).

Вместе эти пять параметров формируют защитный контур: pool_size управляет нагрузкой на Postgres, reserve_pool_size обрабатывает всплески, max_client_conn защищает сам пулер от перегрузки, server_reset_query предотвращает утечку состояния между клиентами, а query_wait_timeout превращает исчерпание пула в быстрый явный сбой вместо тихой очереди. Пропусти любой — под нагрузкой проявится соответствующий режим отказа.

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

Почему формула использует ядра × 2, а не ядра × 1? Одно ядро CPU может обрабатывать немного больше одного активного backend’а, потому что Postgres backend’ы часть времени ждут I/O или блокировок, а не непрерывно потребляют CPU. Множитель ×2 — практическое среднее для смешанных CPU + I/O нагрузок; для исключительно CPU-bound запросов (сложные агрегации, JIT) множитель должен быть ближе к 1.

Референсные числа для размера пула
Формула: оптимальное число backend'ов
(ядра × 2) + шпиндели
Типичный pool_size в production (8 ядер)
24 (16 + 50% запас)
Формула клиентского пула на воркер
concurrent × calls × avg_ms / budget_ms
Память PgBouncer на клиентское соединение
~2 КБ
Память Postgres backend
~5–10 МБ
Рекомендация reserve_pool_size
~25% от pool_size
Типичный max_client_conn
1 000–10 000
Викторина

Postgres-сервер имеет 16 ядер, все данные помещаются в RAM (NVMe). Какой правильный стартовый pool_size для PgBouncer?

Викторина

Node.js воркер обрабатывает 100 параллельных запросов, каждый делает 2 вызова БД по 5 мс в среднем, бюджет запроса 50 мс. Каков правильный max клиентского пула?

Вспомните перед уходом
  1. 01
    Почему `(ядра × 2) + шпиндели` — формула для активных backend'ов на стороне Postgres, а не для числа клиентов?
  2. 02
    Какие параметры PgBouncer управляют backend-пулом и клиентским потолком, и как каждый из них следует устанавливать?
  3. 03
    Когда формулу (ядра × 2) нужно корректировать вверх, а когда вниз?
Итог

Пропускная способность Postgres достигает пика примерно при (ядра × 2) + шпиндели активных backend’ов. После этого переключения контекста и конкуренция за общую память снижают пропускную способность на backend. Устанавливайте PgBouncer pool_size к этому числу плюс ~50% запас на пики — на типичном 8-ядерном NVMe-сервере это 24 backend’а, не 200. Клиентский пул каждого воркера рассчитывается отдельно по формуле параллелизма: concurrent_requests × db_calls × avg_ms / budget_ms. Двухуровневый стек (клиентский пул на воркер → пул PgBouncer в transaction mode → Postgres) мультиплексирует тысячи клиентов на небольшое число backend’ов, реально насыщающих CPU без его перегрузки. Теперь, когда видишь исчерпание пула под нагрузкой, формула даёт число для сверки: если pool_size уже выше (ядра × 2) + 50%, ответ — не «поднять pool_size», а «найти, что удерживает соединения открытыми».

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки
встречается в287

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.