Сколько соединений к PostgreSQL до деградации: находим свою точку перелома
Рано или поздно любой, кто настраивает PostgreSQL под нагрузку, ищет одно и то же число: сколько соединений сервер выдержит, прежде чем всё начнёт тормозить. И почти всегда натыкается на чужие цифры из форумов и статей — «мы держим 500», «у нас упало на 1200». Проблема в том, что это число не переносится с сервера на сервер: у PostgreSQL нет жёсткого порога, после которого он рушится, — есть постепенная деградация, которая начинается в разных точках в зависимости от вашей памяти, CPU, дисков и характера запросов. Разберём, из чего складывается цена одного соединения, почему увеличение max_connections часто делает только хуже, и как найти свою точку перелома, а не полагаться на чужую.
Содержание
- Из чего складывается цена одного соединения
- Как max_connections привязан к памяти и CPU сервера
- Почему «просто накрутить max_connections» часто делает хуже
- PgBouncer: как разорвать прямую связь между клиентами и backend-процессами
- Как построить свой нагрузочный тест и найти точку перелома
- Что делать с результатами: настройка под найденный предел
Из чего складывается цена одного соединения
PostgreSQL использует модель «процесс на соединение»: на каждое новое подключение сервер форкает отдельный backend-процесс (в отличие от MySQL и многих других СУБД, где чаще используются потоки). Это даёт надёжность — падение одного backend не роняет весь сервер, — но цена платится памятью и переключениями контекста.
У каждого backend-процесса есть базовый overhead: код процесса, локальные структуры, копии части разделяемой памяти через copy-on-write после fork. Точная цифра зависит от версии PostgreSQL, ОС, загруженных расширений и от того, сколько объектов схемы кэшируется в relcache/syscache — не берите чужие цифры, измерьте свою:
# базовая память до открытия дополнительных соединений
ps -o pid,rss,vsz,cmd -C postgres --sort=-rss | head -5
# открываем N holостых подключений (например, через pgbench -C или пул) и сравниваем RSS
ps -o rss= -C postgres | awk '{sum+=$1} END {print sum/1024" MB суммарно"}'
Разница между «до» и «после» N подключений, делённая на N, — это реальный базовый overhead на соединение на вашем сервере с вашей схемой.
Дальше — главный переменный компонент, work_mem. Это не память на соединение, а память на *операцию сортировки или хеширования* внутри одного запроса. Ключевой нюанс: сложный запрос с несколькими join, сортировками и агрегациями открывает несколько таких операций сразу, и каждая может выделить свой work_mem. План с четырьмя узлами sort/hash способен потребить примерно четыре work_mem одновременно, в рамках одного запроса на одном соединении. Проверить число таких узлов можно так:
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
и посмотреть, сколько операций Sort/Hash/HashAggregate встречается в плане — это множитель, на который умножается work_mem в худшем случае для одного запроса.
Третий компонент — maintenance_work_mem, который используется при VACUUM, CREATE INDEX, ALTER TABLE ... ADD FOREIGN KEY и в процессах автовакуума. Он не привязан к обычным клиентским соединениям напрямую, но конкурирует за ту же память, если автовакуум активен параллельно с пиковой нагрузкой.
Как max_connections привязан к памяти и CPU сервера
Формула теоретического пикового потребления памяти выглядит примерно так:
RAM_worst_case ≈ shared_buffers
+ max_connections × (overhead_backend + work_mem × N_операций_в_плане)
+ maintenance_work_mem × число_параллельных_vacuum/autovacuum_workers
Это *теоретический потолок*, а не то, что реально используется в среднем — большинство соединений простаивают (idle) или выполняют лёгкие запросы без сортировок. Но если пиковая нагрузка одновременно поднимает много тяжёлых аналитических запросов на большом max_connections, разрыв между «обычно» и «в худшем случае» может стать причиной внезапного OOM, когда всё вроде бы было настроено штатно. Разбор такого сценария — в статье PostgreSQL: Out of Memory — причины и решение.
С CPU связь другая. Каждое активное соединение — отдельный процесс, за которым следит планировщик ОС. При росте числа одновременно выполняющих запросы backend-процессов (именно *активных*, а не просто открытых idle-соединений) растёт число переключений контекста, конкуренция за CPU-кэш и, что часто недооценивают, за внутренние блокировки PostgreSQL — например, ProcArrayLock, который берётся при построении снапшота видимости транзакций и при коммитах. Чем больше активных backend'ов, тем дороже эта служебная работа, причём нелинейно: до определённой точки рост числа активных соединений почти не влияет на латентность запроса, а после — каждое новое соединение отбирает больше, чем даёт.
Понять, где вы находитесь относительно этой кривой, помогают штатные представления:
-- сколько соединений открыто и сколько из них реально что-то делают
SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY count(*) DESC;
-- на что реально ждут активные backend'ы прямо сейчас
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE state = 'active'
GROUP BY 1, 2
ORDER BY 3 DESC;
Если в wait_event_type регулярно всплывает Lock или LWLock с ростом конкурентности — вы уже видите конкуренцию за внутренние ресурсы, а не просто «сервер устал». Это отличается от ситуации, когда узкое место — диск (wait_event_type = IO) или сеть, и лечится разными способами.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему «просто накрутить max_connections» часто делает хуже
max_connections — это не регулятор производительности, а верхняя граница допустимого. Поднять её «на всякий случай» с расчётом, что PostgreSQL сам разберётся, — частая ошибка, и вот почему она обычно не помогает, а иногда и вредит:
- Память резервируется под теоретический пик, а не под факт. Даже если реально активных соединений мало, ОС и мониторинг должны быть готовы, что
max_connectionsможет быть достигнут одновременно активными backend'ами — иначе вы просто откладываетеOOMна момент пиковой нагрузки. - Больше соединений — больше конкуренции за одни и те же строки. Если узкое место — горячая таблица с частыми
UPDATE, дополнительные соединения не ускорят обработку очереди, а лишь увеличат её. - Растёт стоимость служебной работы. С ростом числа активных backend-процессов растёт стоимость операций, учитывающих состояние всех соединений — например, построение снапшота для
MVCC. - Проблема часто не в лимите, а в его симптоме.
FATAL: too many connectionsобычно означает, что где-то не закрываются соединения (утечка пула в приложении) или что нет пулинга вообще, а не что серверу физически не хватаетmax_connections. Разбор этой ошибки — в статье PostgreSQL не принимает подключения: причины и решение.
Деградация в PostgreSQL почти никогда не выглядит как обрыв по вертикальной линии — это постепенно растущий p95/p99, который в какой-то момент начинает расти быстрее нагрузки. Это ускорение и есть точка перелома, а не момент, когда сервер падает целиком.
PgBouncer: как разорвать прямую связь между клиентами и backend-процессами
Раз каждое соединение к PostgreSQL — отдельный процесс с ощутимым overhead, естественное решение — не открывать backend-процесс на каждого клиента, а держать небольшой пул реальных соединений и раздавать их по очереди. Это делает PgBouncer — лёгкий connection pooler между приложением и PostgreSQL.
У PgBouncer три режима пулинга, и разница между ними принципиальна:
| Режим | Когда соединение возвращается в пул | Плюсы | Ограничения |
|---|---|---|---|
session | После явного отключения клиента | Полная совместимость: работают prepared statements, LISTEN/NOTIFY, temp-таблицы, advisory locks | Экономии соединений почти нет — фактически 1:1 с клиентами |
transaction | После завершения каждой транзакции | Максимальная экономия backend-соединений, подходит для веб-приложений с короткими транзакциями | Ломается всё, что привязано к сессии: SET на уровне сессии, PREPARE, temp-таблицы между транзакциями |
statement | После каждого отдельного запроса | Самая агрессивная экономия | Не поддерживает многошаговые транзакции — годится только для автокоммитных запросов |
На практике для веб- и API-нагрузок чаще используют transaction-режим — он даёт основную экономию соединений. Но именно в нём чаще всего ловят неожиданные ошибки, если ORM полагается на server-side prepared statements или на состояние сессии между запросами. Разбор такой ситуации — в статье PgBouncer в режиме transaction сломал prepared-запросы.
Базовый конфиг pgbouncer.ini для старта:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
Ключевая идея: max_client_conn — сколько клиентов приложения подключается к PgBouncer (может быть большим, эти соединения дешёвые), а default_pool_size — сколько реальных backend-соединений к PostgreSQL держится на базу (должно укладываться в реальный max_connections с запасом на служебные подключения — мониторинг, репликацию). Именно это число вы подбираете нагрузочным тестом, а не переписываете из чужого конфига. Установка описана в статье Как установить и настроить PgBouncer на VPS.
Честно: PgBouncer не бесплатен — он сам потребляет CPU на проксирование, добавляет сетевой хоп и накладывает ограничения режима transaction. Для нагрузок с короткими транзакциями выигрыш обычно перевешивает издержки, но это тоже стоит проверить, а не принимать на веру.
Как построить свой нагрузочный тест и найти точку перелома
Единственный надёжный способ узнать свою точку деградации — прогнать нагрузку, похожую на реальную, постепенно увеличивая конкурентность, и наблюдать не за фактом падения сервера, а за формой кривой латентности.
Базовый инструмент, который идёт в комплекте с PostgreSQL, — pgbench. Он не заменит тест с реальными запросами приложения, но хорошо подходит для первой прикидки и для проверки эффекта от изменения max_connections или включения пулинга:
# инициализация тестовой базы (масштаб влияет на размер данных)
pgbench -i -s 50 mydb
# сам тест: -c число клиентских соединений, -j воркеры pgbench, -T время в секундах
pgbench -c 20 -j 4 -T 60 -P 5 mydb
pgbench -c 50 -j 4 -T 60 -P 5 mydb
pgbench -c 100 -j 4 -T 60 -P 5 mydb
pgbench -c 200 -j 4 -T 60 -P 5 mydb
Ключевая методика — не один тест с фиксированной конкурентностью, а серия с растущим -c и график двух величин от числа соединений: транзакций в секунду (tps) и латентности, лучше не средней, а перцентилей (флаг --latency-limit). Для рабочей нагрузки вместо встроенного сценария используйте -f script.sql с реальными запросами приложения — синтетический SELECT-бенчмарк не отразит вашу реальную нагрузку на диск и блокировки.
Форма точки перелома обычно такая: tps растёт почти линейно с ростом -c, затем рост замедляется, tps выходит на плато или падает, а латентность в этот момент начинает расти уже не линейно, а резко. Момент, где tps перестаёт расти линейно, а латентность ускоряется — и есть точка деградации, а не момент отказа в соединении.
Снимайте показания не только с PostgreSQL, но и с ОС — деградация может упираться в память, CPU или диск, и это разные диагнозы:
# переключения контекста и очередь на CPU
vmstat 1
# утилизация дисков и очередь на I/O
iostat -x 1
# память: сколько реально свободно и сколько уходит в page cache
free -m
Если с ростом -c растёт wa (iowait) в vmstat — узкое место диск, и увеличение max_connections не поможет вообще. Если растёт cs (context switches) без пропорционального роста tps — вы упёрлись в CPU-конкуренцию между backend-процессами. Если free показывает исчерпание памяти и рост swap — вы в шаге от OOM, и это тот сценарий, который лучше поймать на тесте, а не на проде.
Тестируйте на окружении, близком к продакшену: тот же порядок объёма данных, та же версия PostgreSQL, те же индексы и autovacuum-настройки. Тест на пустой базе с игрушечным датасетом покажет совсем другую точку перелома, чем реальная база с фрагментированными индексами и активным автовакуумом на фоне.
Что делать с результатами: настройка под найденный предел
Когда точка перелома найдена, дальше — настройка конкретных параметров под неё, а не под общие советы из интернета.
Практический порядок действий:
- Зафиксируйте безопасный
max_connections. Возьмите число активных соединений в момент, предшествующий точке перелома, добавьте разумный запас на непокрытые тестом пики и на служебные соединения —superuser_reserved_connections, мониторинг, репликацию. - Пересчитайте
work_memпод этотmax_connectionsи вашу RAM. Ориентир, не формула на все случаи: доступная память делится не наmax_connections, а на реалистичное число одновременно активных сложных запросов — иначе либо занизитеwork_memи получите уход операций на диск (temp files, видно вpg_stat_database.temp_files), либо завысите и вернётесь к рискуOOM. - Разверните пулинг между приложением и базой, если ещё не сделали — это отдельный рычаг, снижающий реальное число backend-процессов независимо от числа клиентских соединений.
- Перепроверьте настройку тем же тестом. После изменений прогоните ту же серию — точка перелома должна сдвинуться дальше по оси конкурентности, и это единственное объективное подтверждение, что изменения помогли.
- Мониторьте
pg_stat_activityиwait_eventна проде постоянно, а не только во время теста — точка перелома, найденная сегодня, через полгода роста базы и трафика может сдвинуться.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Есть ли универсальное число max_connections, которое подходит большинству серверов?
Нет. Значение по умолчанию (100) — консервативный компромисс разработчиков PostgreSQL, а не рекомендация под вашу нагрузку. Правильное число зависит от RAM, характера запросов и того, используете ли вы пулинг.
Можно ли ориентироваться на формулы вида «(число ядер × 2) + число дисков» для размера пула?
Такая эвристика периодически встречается в обсуждениях пулинга и может быть отправной точкой для первого теста, но это не закон — она не учитывает характер запросов и профиль ввода-вывода. Используйте её только как стартовую точку, которую затем проверяете тестом.
PgBouncer в режиме transaction сломал часть запросов приложения — что делать?
Проверьте, использует ли ORM server-side prepared statements, SET на уровне сессии или temp-таблицы между запросами одной операции — это несовместимо с transaction-режимом. Решается настройками драйвера (отключение prepared statements) или переводом части трафика на session-режим через отдельный порт PgBouncer.
Если тест не показывает явного излома, а латентность растёт плавно — что считать точкой перелома?
Ориентируйтесь на бизнес-требование: SLA по латентности (например, p95 не выше определённого значения). Точка, где p95 пересекает этот порог, и есть практическая точка деградации.
Нужно ли повторять нагрузочный тест после каждого релиза?
Периодически — да, особенно после изменений схемы запросов, новых индексов или заметного роста объёма данных. Точка перелома не статична — она меняется вместе с нагрузкой.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →