PHP и узкое место «коннекты к базе»: пулеры, прокси и практика
Во многих стеках запросы уже не «убийцы», а прод всё равно падает в too many connections, remaining connection slots are reserved или подвисания сразу после деплоя. Частая причина — не медленный SQL, а арифметика соединений: модель запроса в PHP даёт всплески connect + auth + TLS, а у базы есть жёсткий потолок параллельных backend’ов. Пулеры и управляемые прокси как раз держат небольшой стабильный пул серверных сессий за большим числом короткоживущих клиентов в PHP.
Связанные материалы: БД под нагрузкой: запросы и масштабирование · Sail: базы и Docker-сервисы
Содержание
- Почему PHP усугубляет проблему
- Чем вы реально ограничены
- Пулеры и прокси «посередине»
- PostgreSQL: PgBouncer на практике
- MySQL и MariaDB: ProxySQL и аналоги
- Облачные прокси (RDS Proxy и др.)
- Другие пулеры: PgCat, Odyssey, pgpool-II
- Заметки про Laravel
- Что пулеры не лечат
- Чеклист
Почему PHP усугубляет проблему
Классический PHP-FPM обрабатывает запрос, ходит во внешние сервисы, отдаёт ответ и освобождает ресурсы запроса. Без persistent connections (и их компромиссов) каждый запрос к БД обычно открывает или забирает TCP-сессию, аутентифицируется, при необходимости TLS, затем выполняет запросы.
Под нагрузкой:
pm.max_childrenзадаёт, сколько процессов PHP могут работать одновременно на машине. Если большинство запросов бьёт в БД, теоретически нужно до стольки же одновременных сессий на воркер-ноду.- Воркеры очередей (
queue:work, Horizon) — долгоживущие процессы; каждый параллельный воркер часто держит одно или несколько открытых соединений на время джоба. - Горизонтальное масштабирование умножает всё: три ноды по восемьдесят детей — уже 240 потенциальных сессий, без cron, разовых CLI и прочего.
База видит шторм коннектов при деплое и скачках трафика. Даже если max_connections формально хватает, упираются память на backend (особенно Postgres) и CPU на аутентификацию.
Чем вы реально ограничены
max_connectionsу Postgres/MySQL — глобальный потолок; служебные и репликационные резервы уменьшают доступное приложению.- Память — у каждого backend есть буферы и состояние; «просто поднять лимит» может привести к OOM.
- Задержка установки соединения — TLS, пароль, иногда LDAP добавляют миллисекунды и десятки миллисекунд на запрос при connect-per-request.
- Thundering herd — после рестарта все PHP-процессы могут подключиться разом и забить accept/auth.
Практическое правило: считайте все программы, которые говорят с SQL (веб, воркеры, cron, админки, BI), а не только HTTP.
Пулеры и прокси «посередине»
Пулер стоит между PHP и СУБД. PHP открывает относительно дёшевое соединение к пулеру; пулер держит меньший пул настоящих соединений к Postgres/MySQL и переиспользует их между множеством клиентов.
Плюсы:
- Меньше серверных backend’ов и RAM на инстансе БД.
- Мультиплексирование: многие простаивающие PHP-клиенты не закрепляют за собой idle-сессию на сервере.
- Более ровное поведение при скачках нагрузки.
Минусы:
- Ещё один хоп (латентность, зона отказа, безопасность и мониторинг).
- Семантика сессии меняется в зависимости от режима пулинга (см. PgBouncer).
- Сам пулер нужно размерить, иначе он станет новым узким местом (CPU, FD, голод пула).
PostgreSQL: PgBouncer на практике
PgBouncer — фактический стандарт пулинга для Postgres в PHP-стеках.
Режимы:
| Режим | Поведение | PHP / Laravel |
|---|---|---|
| Session | Один серверный коннект на всю клиентскую сессию до disconnect | Максимальная совместимость: SET, LISTEN, advisory locks, временные таблицы, prepared statements как при прямом подключении. Меньший выигрыш при долгих клиентах (воркеры) или connect-per-request. |
| Transaction | Серверное соединение возвращается в пул после транзакции (COMMIT/ROLLBACK) | Сильное мультиплексирование для коротких веб-запросов. Ломает сессионные вещи между транзакциями, LISTEN, долгоживущие temp tables; именованные prepared statements часто требуют настройки драйвера. |
| Statement | Возврат после каждого оператора | Для ORM почти не подходит; ломает многооператорные транзакции. |
Prepared statements и transaction pooling: драйверы часто готовят запросы по имени в сессии. Если физическое соединение к серверу между запросами меняется, именованные prepare ломаются. В проде используют безымянные prepare, simple query или эмуляцию prepare на клиенте (PDO::ATTR_EMULATE_PREPARES и аналоги — по документации драйвера).
Имеет смысл передавать application_name в параметрах подключения — проще разбирать pg_stat_activity.
MySQL и MariaDB: ProxySQL и аналоги
ProxySQL — популярный прокси по протоколу MySQL: маршрутизация, правила к запросам, разделение read/write и пул соединений с настраиваемым мультиплексированием.
Типичные задачи:
- Ограничить backend-коннекты, пока к ProxySQL подключаются сотни детей FPM.
- Направлять чтение на реплики по правилам (всё равно следите за лагом репликации).
- Аккуратно переписывать или фильтровать классы запросов — помните, что логика в прокси — это операционная сложность.
MySQL Router и часть облачных балансировщиков ведут себя иначе — не всё, что слушает 3306, даёт тот же мультиплексинг, что ProxySQL; читайте документацию.
MariaDB MaxScale — маршрутизация и управление соединениями в зависимости от редакции и модулей; проверяйте лицензирование.
Облачные прокси (RDS Proxy и др.)
У облаков есть управляемые прокси перед RDS, Aurora, Cloud SQL и т.д. Обычно они дают пулинг, интеграцию с IAM/токенами и более мягкое поведение при failover.
Семантика СУБД не отменяется: при агрессивном мультиплексировании те же ограничения, что у self-hosted PgBouncer, в том числе по prepared statements и состоянию сессии — смотрите матрицу совместимости для вашего движка и драйвера.
Другие пулеры: PgCat, Odyssey, pgpool-II
- PgCat и Odyssey — пулеры для Postgres с растущим использованием; сравните режимы пула, метрики и особенности драйверов с PgBouncer перед сменой инструмента.
- pgpool-II — часто ставят ради репликации и маршрутизации не меньше, чем ради пулинга; тяжелее в эксплуатации, если нужен только мультиплексинг.
Заметки про Laravel
config/database.php—connections.*.optionsи флаги драйвера: подогнать PDO под пулер (например эмуляция prepare).- Read/write — Laravel может слать
SELECTнаread; вместе с пулером проверьтеstickyи ожидания по лагy реплик. - Octane / Swoole / FrankenPHP — долгоживущие воркеры: persistent connections могут быть удачны, но нельзя протекать состоянием транзакции между запросами; следите за idle timeout на сервере и пулере.
- Horizon /
queue:work—concurrency × workersдаёт постоянную нагрузку на пул; либо пул на воркер, либо transaction mode с совместимыми настройками. - Telescope, debug-бары в проде держат транзакции дольше, чем кажется — оставьте их вне прода.
Пример: в .env хост указывает на пулер, не на прямой VIP Postgres:
DB_HOST=pgbouncer.internal
DB_PORT=6432
DB_DATABASE=app
DB_USERNAME=app_rw
default_pool_size, reserve_pool (PgBouncer) или лимиты ProxySQL нужно вязать с реальной параллельностью запросов, а не с числом PHP-процессов.
Что пулеры не лечат
- N+1 и отсутствие индексов по-прежнему жгут CPU и I/O — пулер только ограничивает число сессий, через которые это выражается.
- Длинные транзакции удерживают backend из пула — при transaction pooling выигрыш исчезает, если код держит транзакцию открытой на время внешнего HTTP.
- Миграции и DDL — прогон
migrateчерез перегруженный пулер может конфликтовать с блокировками; часто выделяют прямой админский путь к БД.
Чеклист
- Инвентаризация всех типов процессов с SQL (FPM
max_children× ноды, воркеры Horizon, cron, CLI). - Сравнить с
max_connectionsи памятью на соединение — решить: пулер или сначала дисциплина в приложении. - Выбрать режим пула (Postgres) или правила мультиплексирования (MySQL) под ORM и драйвер.
- Прогнать нагрузочный тест с prepared statements и нужными сессионными фичами (
SET, temp tables, advisory locks). - Мониторить очередь на пулер и активные коннекты на СУБД — если очередь растёт, узкое место всё ещё база или запросы.
Пулер «посередине» — это инфраструктура, которую вы эксплуатируете (или покупаете). При грамотной настройке он превращает «PHP открыл восемьсот соединений» в «Postgres видит шестьдесят занятых backend’ов» — ту форму OLTP, с которой СУБД умеет работать предсказуемо.