PostgreSQL для 500+ подключений: pgBouncer, архитектура и требования к железу
2026-05-28 12:52
Приложение упёрлось в max_connections=200, увеличили до 1000, а сервер стал тормозить ещё сильнее. Это базовая особенность архитектуры. PostgreSQL 500 подключений и больше держит только при правильной инфраструктуре: пулер соединений, реплики для чтения, разнесённые тома под WAL и адекватное железо под OLTP. Настройка max_connections в этом сценарии перестаёт работать после первой пары сотен коннектов.
Ниже разберём, почему PostgreSQL чувствителен к числу подключений, какую роль играет pgBouncer и его режимы пула, какая архитектура работает на 500+ коннектов и какое железо нужно под нагрузку.
Почему PostgreSQL упирается в число подключений
В архитектуре PostgreSQL на каждое клиентское подключение создаётся отдельный backend-процесс с областью памяти 5–10 МБ оверхеда плюс work_mem. 1000 подключений × 10 МБ - это 10 ГБ RAM только на оверхед процессов, плюс work_mem на сортировки. На сервере с 64 ГБ RAM половина уходит просто на «держать коннекты».
Дополнительная нагрузка ложится на CPU: планировщик ядра Linux тратит время на переключение контекста между сотнями процессов. Реальный потолок «голого» PostgreSQL без пулера - 100–200 активных подключений. Это ориентир от практики высоконагруженных инсталляций, который подтверждается на типовом железе: на 2× Xeon Gold с 256 ГБ RAM выше двухсот активных подключений начинается деградация по памяти и контекстным переключениям, скорость падает нелинейно. Если приложение или несколько приложений суммарно хотят 200+ одновременных подключений, это сигнал ставить pgBouncer до того, как стало плохо.
pgBouncer: что это и как работает
Режимы пула: session, transaction, statement
pgBouncer для PostgreSQL - это прозрачный пулер соединений, который держит несколько десятков «реальных» подключений к БД и обслуживает с них сотни клиентских. Работает в трёх режимах.
Session pooling. Backend выдаётся на всё время сессии и освобождается при разрыве со стороны клиента. Подходит для долгих стабильных приложений, но эффект от пулинга минимальный.
Transaction pooling. Backend выдаётся на время транзакции, после COMMIT/ROLLBACK возвращается в пул. Один коннект к БД последовательно обслуживает десятки клиентов. Основной режим в боевых конфигурациях.
Statement pooling. Backend выдаётся на один SQL-запрос - максимальная плотность пула. Запрещены транзакции из нескольких операций, prepared statements и временные таблицы. Узкие сценарии с короткими автокоммитами.
Ограничения transaction pooling: не работают SET LOCAL, advisory locks вне транзакции, prepared statements (без спецнастроек). Совместимость приложения проверяется до перехода. Типичный пул: на 500 клиентских коннектов закладывают 20–50 backend-соединений в transaction-режиме, соотношение 10:1 или 25:1 - норма.
Где ставить pgBouncer в архитектуре
Три типичных места размещения. На том же хосте, что и приложение - низкая задержка, локальный сокет, минус - общий пул не работает, у каждого сервера приложений свой pgBouncer. На том же хосте, что и PostgreSQL - простая архитектура, один пул на всех клиентов, минус - pgBouncer и PostgreSQL конкурируют за ресурсы. Если выбран этот вариант, журналы pgBouncer обязательно выносят на отдельный физический том от PGDATA и WAL: на одном диске логи пулера будут конкурировать с WAL за IOPS, и именно эту ошибку чаще всего видят при разборе деградации производительности под нагрузкой. На отдельном сервере в HA-паре с keepalived - промышленная схема с отказоустойчивостью пула, минус - больше точек эксплуатации.
Выбор: малые инсталляции - pgBouncer рядом с PostgreSQL, средние на серверах приложений, большие с HA - выделенная HA-пара пулеров перед мастером БД.
Архитектура высоконагруженного PostgreSQL-сервера
Базовая HA-схема: один primary (master) плюс несколько standby с асинхронной или синхронной репликацией через WAL. На случай отказа master - автоматическое переключение через Patroni, repmgr или Pacemaker. Patroni - основной выбор для cloud-native и контейнерных сред, использует etcd/Consul/ZooKeeper как distributed configuration store и хорошо живёт в Kubernetes. repmgr - наследник классического стека Postgres, проще в bare-metal и для команд, привыкших к утилитам PostgreSQL без внешних координаторов. Pacemaker - универсальный кластерный менеджер от Red Hat, ставится там, где уже работают другие сервисы под Pacemaker и нужна единая модель эксплуатации.
Под новый проект DBA-сообщество чаще рекомендует Patroni, у него самая активная разработка и широкая совместимость с современным окружением. Все клиенты подключаются к pgBouncer, а не напрямую к PostgreSQL: это и пул соединений, и точка переключения при failover. Пулер переподключается на новый master, клиенты не замечают.
Read replicas распределяют нагрузку на чтение: SELECT-запросы можно отправлять на standby, что критично при перевесе чтения (BI-отчёты, аналитика, поиск). Распределение реализуется на уровне приложения или через прокси (Pgpool-II, HAProxy с тегами).
Синхронная репликация даёт zero data loss, но запись ждёт подтверждения от standby и замедляет master. Асинхронная быстрее, но есть риск потери последних транзакций. Гибрид: синхронная на одну реплику в том же ЦОД, асинхронная на DR-реплику в другом. Один pgBouncer - единственная точка отказа, поэтому промышленный стандарт - два пулера в HA-паре с keepalived. Мониторинг - pg_stat_activity, pg_stat_replication и pgBouncer через SHOW POOLS и SHOW STATS, метрики в Prometheus или Zabbix.
Требования к железу под нагрузку 500+ подключений
CPU, память, диски, сеть под высоконагруженный PostgreSQL
Сервер PostgreSQL под нагрузку в 500+ подключений собирается под четыре компонента, и на каждом есть характерные узкие места.
CPU. Важнее однопоточная производительность: Intel Xeon Gold/Platinum от 3 ГГц или AMD EPYC высокочастотных серий. Ядер - от 16, для тяжёлой OLTP - от 32.
Память. shared_buffers около 25% от RAM, остальное ОС использует под page cache. Под 500+ подключений и средние базы - от 128 ГБ RAM, для нагруженных систем 256–512 ГБ.
Диски под PGDATA только NVMe SSD enterprise-класса в RAID 10. SATA-SSD и HDD не подходят, IOPS критичны. Том под WAL - на физически отдельных дисках от PGDATA: WAL пишется последовательно, его не должно тормозить случайное чтение.
Сеть. Между приложением и PostgreSQL минимум 10 Гбит/с, между master и standby - отдельный сегмент 10 Гбит/с+ под синхронную репликацию. ECC-память обязательна, RAID-контроллер с BBU/FBWC под write-back, дублированные БП, ИБП. На сервер БД не ставится ничего кроме PostgreSQL и pgBouncer. Когда нагрузка прогнозируется в районе 500+ подключений с NVMe-массивами и отдельной сетью репликации, удобнее отталкиваться от типовых конфигураций - серверы баз данных уже сбалансированы под OLTP-нагрузки PostgreSQL и MS SQL.
Тюнинг postgresql.conf под высокую нагрузку
Настройка postgresql.conf под пулер и высокую нагрузку сводится к нескольким ключевым параметрам.
max_connections - установить с запасом под пулер: 200–300, не 500+. pgBouncer держит реальные коннекты, в БД их кратно меньше.
shared_buffers - около 25% от RAM сервера. Выше 40% отдача от ОС-кэша становится незначительной.
effective_cache_size - около 60–70% от RAM. Это подсказка планировщику, сколько данных лежит в кэше ОС. Чем выше, тем чаще выбирается index scan вместо seq scan.
work_mem - память на сортировку или хеш. Типовой диапазон 4–32 МБ. Опасно завышать: значение умножается на число одновременных операций.
wal_buffers - обычно хватает 16 МБ; для очень нагруженных систем до 64 МБ.
checkpoint_timeout и max_wal_size - растягивают checkpoint во времени, снижают пиковую нагрузку на диск. Типовые: 15 минут и 4–8 ГБ.
Логирование. log_min_duration_statement 1000–5000 мс для медленных запросов, log_checkpoints on, log_connections off (иначе на 500 коннектов лог разорвёт диск).
Когда pgBouncer не помогает: альтернативы
Pgpool-II. Кроме пулинга умеет балансировать read-нагрузку между master и standby, переадресовывать запросы по типу SQL. Сложнее в эксплуатации, но даёт больше.
Citus. Расширение PostgreSQL для горизонтального шардирования. Когда данные не помещаются на один сервер и нужно распределить по нескольким узлам - это путь.
Read replicas с балансировкой на уровне приложения. Многие ORM (Django, Rails) отправляют SELECT на реплику, UPDATE - на master. Простая схема без Pgpool.
Managed-решения - Postgres Pro Enterprise или облачные PostgreSQL-сервисы. Когда инфраструктура и эксплуатация дороже подписки.
Когда пора уходить от PostgreSQL. Если нагрузка превышает 10 000 клиентских подключений к pgBouncer (а не к самому PostgreSQL) при активной записи, и горизонтальный шардинг через Citus не подходит по структуре данных, стоит рассматривать специализированные хранилища под конкретные подсистемы: ClickHouse под аналитику и логи, Redis или KeyDB под сессии и кэш, Cassandra или ScyllaDB под широкие колоночные данные с высокой пропускной способностью записи. PostgreSQL при этом остаётся для транзакционных операций, где консистентность и ACID критичны.
Чего не делать
Не ставить max_connections в 1000+ без пулера. На реальной нагрузке производительность будет хуже, чем при 200. Типовая ошибка миграции с MySQL.
Не использовать pool_mode=statement без понимания. Ломаются транзакции, prepared statements, временные таблицы.
Не игнорировать мониторинг pgBouncer. Состояние пула, число активных и waiting клиентов, утилизация backend-коннектов - must-have метрики.
Не ставить логи pgBouncer на тот же физический том, что и WAL PostgreSQL. Это правило применяется в обеих архитектурах - и когда pgBouncer стоит на отдельном сервере, и когда он живёт на одном хосте с PostgreSQL: журналы пулера и WAL соревнуются за IOPS, и под нагрузкой это бьёт по latency транзакций. Разносить по разным физическим дискам обязательно.
Заключение
PostgreSQL под 500+ подключений - это не «увеличить max_connections», а целая архитектура: pgBouncer перед мастером, реплики для чтения, отдельные тома под WAL, тюнинг postgresql.conf и адекватное железо с NVMe. Когда задача выходит за рамки настройки одного сервера и затрагивает кластер из master, standby, пары pgBouncer и мониторинга, проще закрыть проект как интеграцию серверного оборудования с фиксированным регламентом по failover и SLA - это дешевле потерь от ошибок проектирования.