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

Базы данных

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

Суть Пропускная способность Postgres достигает пика при (ядра × 2) + шпиндели активных backend''''ов; клиентский пул на воркер определяется как concurrent_requests × db_calls × avg_ms / budget_ms; многоуровневая математика удерживает оба параметра в границах.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Команда добавляет 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:

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 с для быстрого отказа).

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

Почему формула использует ядра × 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 без его перегрузки.

Связанные уроки
встречается в258
Продолжить восхождение ↑Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночи
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.