Почему не стоит ставить max_connections = 1000 просто "на всякий случай"
Взято с основного технического канала Postgres DBA (возможны правки в исходной статье).
Предисловие
При настройке PostgreSQL многие администраторы ориентируются на простое правило: «чем выше лимит соединений, тем больше соединений я могу обслужить». Казалось бы, установка max_connections = 1000 — это дальновидный запас на будущее. ⚠️Однако за этим решением скрывается неочевидная плата, которую база данных взимает ежесекундно, даже если большую часть времени вы используете лишь 20 подключений.
Вопреки распространенному мнению, проблема не сводится к «резервированию лишней памяти». В этом материале мы разберем, как статический размер структур ядра (массивы PGPROC, хеш-таблицы блокировок) влияет на алгоритмы планировщика, приводит к деградации кэша процессора и создает фоновую конкуренцию за спинлоки, поэтому работа с расчетом на «толпу» из 1000 процессов замедляет даже простые транзакции и это связано с устройством shared memory.
Теоретическая часть
⚠️При установке `max_connections = 1000` PostgreSQL не выделяет всю память сразу под все 1000 соединений при старте, ‼️но ‼️резервирует системные структуры и ограничения, которые приводят к косвенному увеличению потребления памяти и ресурсов.
ℹ️Фиксированные структуры ядра (выделяются при старте)
При запуске сервера PostgreSQL выделяет:
🔴max_connections — количество слотов для подключений (внутренние структуры `Backend` и `ProcArray`).
🔴max_prepared_transactions— если включены подготовленные транзакции.
🔴max_locks_per_transaction— количество блокировок на транзакцию (умножается на `max_connections` для расчёта общего лимита блокировок).
ℹ️Эти структуры выделяются в shared memory и не освобождаются в течение работы сервера. Их размер зависит от `max_connections` и других параметров.
✅ Итог: Что означает "резервирует память под 1000 соединений"?
ℹ️Не резервируется— 1000 × `work_mem` в оперативной памяти.
⚠️Резервируется— ‼️фиксированные структуры в shared memory‼️ (блокировки, слоты процессов, массивы транзакций).
⚠️Резервируется— системные лимиты(дескрипторы, память под метаданные).
Подробности о фиксированных структурах в shared memory.
ℹ️Резервирование памяти под неиспользуемые структуры — это только верхушка айсберга.
⚠️Главная проблема заключается в том, что даже в простое (при 20 активных соединениях из 1000 возможных) алгоритмы внутри PostgreSQL вынуждены работать с расчетом на "толпу" из 1000 конкурирующих процессов. Это создает постоянную фоновую нагрузку на механизмы синхронизации и планировщик.
Ниже приведено подробное описание того, как каждая структура влияет на производительность.
1. Массив состояний процессов (PGPROC array)
ℹ️Этот массив — один из главных источников косвенных издержек (overhead). Он представляет собой список структур, где каждый потенциальный backend (процесс обслуживания соединения) имеет свой слот.
🔴Влияние на планировщик и очереди ожидания: Ядро PostgreSQL и операционная система используют эти структуры для управления видимостью процессов и очередями. Когда активны только 20 процессов, но массив рассчитан на 1000, многие внутренние алгоритмы (например, поиск следующего процесса для получения блокировки или пробуждения) могут просматривать или учитывать "пустые" слоты. Хотя алгоритмы оптимизированы, сам размер управляющих структур влияет на скорость работы хеш-функций и поиска.
🔴Кэш-промахи (CPU Cache Misses): Это наиболее осязаемое влияние. Процессорный кэш (L1/L2/L3) — это очень быстрая, но маленькая память.
‼️Когда структура PGPROC массива раздута до 1000 элементов, она занимает значительно больше места. Даже если активны только 20 процессов, их рабочие структуры данных "размазаны" по большому участку памяти.
Это снижает вероятность того, что данные текущего активного процесса находятся в быстром кэше процессора (Cache Locality), увеличивая количество обращений к медленной оперативной памяти.
2. Массив блокировок (LOCK и PROCLOCK hash tables)
⚠️Это наиболее критичная часть с точки зрения алгоритмической сложности. Эти таблицы представляют собой хеш-таблицы в разделяемой памяти.
🔴Деградация хеш-таблиц (Hash Table Degradation):
ℹ️ Размер этих таблиц изначально рассчитывается исходя из max_connections * max_locks_per_transaction . Это значение используется как max_size при создании хеш-таблицы.
⚠️Проблема: Если фактическое количество блокировок (занятых записей в таблице) значительно меньше расчетного max_size, это не страшно. Но если вы выставили max_size под 1000 соединений, а используете 20, внутренняя структура таблицы (количество "корзин" — buckets) рассчитывается под это большое значение .
ℹ️Механизм замедления: При добавлении новой блокировки (а это происходит постоянно при работе даже 20 соединений) хеш-функция вычисляет индекс "корзины". Из-за того, что корзин много, они могут быть почти пустыми, но сам процесс вычисления хеша и перехода по ссылкам происходит в контексте большой таблицы. В сценариях, где таблица блокировок почти пуста (как в нашем случае с 20 соединениями), замедление минимально, но оно есть. ⚠️Главная опасность возникает, когда количество блокировок начинает расти — производительность такой таблицы падает нелинейно.
ℹ️Комментарий в исходном коде: Разработчики PostgreSQL прямо комментируют это в функции ShmemInitHash: эффективность доступа (access efficiency) будет деградировать, если размер таблицы существенно превышен, так как это влияет на размер директории и переполнение корзин (buckets get overfull) .
🔴Конкуренция за спинлоки (Spinlock Contention): Даже если блокировок немного, сам механизм доступа к глобальным структурам данных (разделяемым хеш-таблицам) требует захвата легковесных блокировок (LW Locks) или спинлоков. Когда эти таблицы имеют большой размер, управление ими требует более сложной синхронизации, что увеличивает шансы на то, что два процесса одновременно попытаются обратиться к разным частям одной большой структуры, вызывая микро-задержки .
3. Таблица фиксированных идентификаторов
ℹ️Сюда входят различные слоты: идентификаторы транзакций (XID), идентификаторы команд (Command ID) и другие глобальные счетчики.
🔴Влияние на инфраструктуру ожидания (LWLock и семафоры):
Процессы часто ждут сигналов о завершении действий других процессов (например, записи в WAL). Инфраструктура для этих ожиданий (LWLock Wait and Wake) также завязана на количество потенциальных участников .
ℹ️Пример с WAL (журнал предзаписи): Когда 20 процессов пытаются одновременно записать изменения, они конкурируют за WALInsertLock. Механизмы, управляющие этим (например, NUM_XLOGINSERT_LOCKS), эвристически подбираются под MaxConnections . Если MaxConnections огромен (1000), механизмы синхронизации WAL могут использовать более "тяжелые" алгоритмы, ожидая высокой конкуренции, что добавляет лишние циклы CPU даже при низкой конкуренции.
🔴Пробуждение процессов: Система должна отслеживать, кого разбудить при снятии блокировки или завершении записи. ⚠️Чем больше max_connections, тем сложнее структуры данных, которые необходимо обойти для принятия решения о пробуждении (даже если спит всего 1 процесс).
Послесловие
Разбирая внутренности PostgreSQL, мы столкнулись с удивительным парадоксом: иногда «запас» работает против вас. Массив PGPROC, рассчитанный на тысячу процессов, превращает быстрый процессорный кэш в захламленный склад, а хеш-таблицы блокировок, настроенные на толпу, даже в моменты простоя вынуждают ядро использовать более тяжелые алгоритмы синхронизации.
ℹ️Это тот случай, когда невидимый глазу оверхед накапливается снежным комом. Система тратит такты не на обработку ваших данных, а на управление потенциальной, но несуществующей нагрузкой. Поэтому, настраивая PostgreSQL, стоит помнить: щедрость в отношении лимитов соединений оборачивается скупостью производительности. Оптимальная стратегия — держать max_connections на уровне разумного минимума, достаточного для работы пулера соединений и выполнения фоновых задач, чтобы ядро базы данных не «отвлекалось» на обслуживание фантомных процессов.

Postgres DBA
210 постов27 подписчиков
Правила сообщества
Пока действуют стандартные правила Пикабу.