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

Базы данных

Процессная модель Postgres и почему увеличение max_connections снижает производительность

Суть Каждое Postgres-соединение форкает backend через postmaster; пропускная способность достигает пика при (ядра × 2) активных процессах и нелинейно падает после этого — увеличение max_connections без пулера почти всегда неправильное решение.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 12 min

Команда сталкивается с исчерпанием пула. Они поднимают max_connections со 100 до 1000. P99 ухудшается. Поднимают до 2000. База данных начинает падать с OOM. Повышение max_connections — это инстинкт; понимание того, почему это не работает, — это навык.

Процессная модель Postgres подробно

При подключении клиента postmaster (процесс-супервизор) форкает новый backend-процесс. Каждый backend имеет:

  • Приватная память: ~5–10 МБ резидентной памяти (рабочая память для запросов, кэш планов, буферы libpq). На 16-ядерной машине с 1 000 backend’ов это ~5–10 ГБ только для приватной памяти backend’ов.
  • Слот в общей памяти: запись proc array (снятие снапшота сканирует её), запись в таблице блокировок, метаданные predicate-lock. Эти структуры предварительно выделяются при запуске, размером под max_connections. Удвоение max_connections удваивает фиксированный объём общей памяти.
  • Ядерный процесс с собственным стеком, таблицей файловых дескрипторов и записью в планировщике.

Запуск соединения стоит ~5 мс на стороне Postgres (fork + инициализация) плюс TLS и аутентификация на сетевой стороне.

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

Почему увеличение max_connections снижает пропускную способность

Наивные команды поднимают max_connections со 100 до 1 000 при исчерпании пула. Четыре вещи идут не так:

  1. Масштабирование общей памяти: max_connections = 1000 предварительно выделяет ~500 МБ+ фиксированной общей памяти только для proc array, хэша блокировок и структур predicate-lock — ощутимо на небольших инстанциях.

  2. Накладные расходы переключения контекста: при 1 000 активных backend’ов под нагрузкой время планирования ядра становится измеримой долей CPU. Каждый OS-процесс конкурирует за планировщик.

  3. Конкуренция на менеджере блокировок: итерация proc array при снятии снапшота O(active_backends). При 1 000 активных backend’ов каждый BEGIN сканирует 1 000 записей. Сканы таблицы блокировок и конкуренция за MultiXact буфер масштабируются аналогично.

  4. Очередь форка postmaster: создание соединения требует форка нового процесса через postmaster; при высокой частоте соединений это сериализуется. ~2 КБ PgBouncer на клиентское соединение против ~10 МБ Postgres на backend — ключевая асимметрия.

Кривая пропускной способности нелинейна: 16-ядерный Postgres достигает пика примерно при 32–48 активных backend’ах и деградирует после него. Добавление backend’ов после пика означает, что каждый backend получает меньшую долю CPU, при этом платя за бо́льшие накладные расходы конкуренции.

Активные backend’ыПоведение (16-ядерный Postgres)
1–32Линейный рост пропускной способности; растёт использование CPU
32–50Плато пропускной способности; начинают проявляться накладные расходы переключения контекста и блокировок
50–200Пропускная способность стабильна или снижается; видна конкуренция за общую память
200–1000Пропускная способность падает; удельная пропускная способность на backend может вдвое снизиться
1000+Вероятен OOM или исчерпание таблицы процессов ядра

Практический лимит max_connections

Руководство сообщества Postgres (неизменное 25+ лет): ограничьте max_connections несколькими сотнями. Практический диапазон для production OLTP — 200–500: достаточно для административных соединений, реплик и некоторого запаса выше pool_size пулера.

Распределение бюджета max_connections:

  • PgBouncer pool_size (primary) + reserve_pool_size — реальные OLTP backend’ы
  • Соединения реплик (потоковая репликация)
  • superuser_reserved_connections (по умолчанию 3) — для аварийного доступа
  • Административные и мониторинговые соединения
  • Пулы PgBouncer для read-реплик, если есть

Пример: 8-ядерный primary, pool_size = 24, reserve = 6, 2 реплики по pool_size = 20, superuser_reserved = 3, admin/monitoring = 10. Итого: 24 + 6 + 40 + 3 + 10 = 83. max_connections = 100 — нормально; max_connections = 1000 — расточительно и вредно.

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

Что происходит, если max_connections слишком мал? Postgres возвращает “FATAL: remaining connection slots are reserved for non-replication superuser connections”, когда заполняются последние слоты superuser_reserved_connections — воркеры приложения начинают получать отказы до жёсткого лимита. Всегда оставляйте запас выше pool_size + reserve_pool_size для административного доступа во время инцидентов.

Production числа процессной модели Postgres
Приватная память backend
~5–10 МБ
Дефолт max_connections
100
Практический лимит max_connections
200–500
Оптимальные активные backend'ы (16 ядер, NVMe)
32–48
Память PgBouncer на клиентское соединение
~2 КБ
Стоимость запуска backend
~5 мс (fork + init)
Общая память: proc array при 1000 backend'ах
~500 МБ+
Викторина

Инстанция Postgres с max_connections=200 начинает отклонять соединения при ~150 активных backend'ах. Почему это может происходить ниже лимита?

Викторина

Почему Postgres использует модель процесс-на-соединение, а не потоки?

Вспомните перед уходом
  1. 01
    Какие структуры общей памяти масштабируются с max_connections и почему его увеличение снижает пропускную способность?
  2. 02
    Как должен senior-инженер распределять бюджет max_connections на primary Postgres-инстанции?
  3. 03
    Почему деградация пропускной способности после (ядра × 2) нелинейная, а не линейная?
Итог

Каждое Postgres-соединение форкает backend через postmaster: ~10 МБ приватной памяти, слот proc array, запись в таблице блокировок. Запуск соединения стоит ~5 мс. Структуры общей памяти предварительно выделяются под max_connections при запуске — удвоение удваивает фиксированные накладные расходы. После (ядра × 2) активных backend’ов накладные расходы переключения контекста и сканирования proc array вызывают нелинейную деградацию пропускной способности. Руководство сообщества Postgres на протяжении 25 лет: ограничивайте max_connections до 200–500; мультиплексируйте выше с помощью пулера. Повышение max_connections до 1 000 без пулера — почти всегда неверный ответ на исчерпание пула: это смещает симптом, ухудшая базовый профиль пропускной способности.

Связанные уроки
встречается в258
Продолжить восхождение ↑Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказов
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.