PHP і вузьке місце «коннекти до бази»: пулери, проксі та практика

У багатьох стеках запити вже не «вбивці», а прод усе одно впирається в too many connections, обмеження слотів або підвисання після деплою. Часта причина — не повільний SQL, а арифметика з’єднань: модель запиту в PHP дає сплески connect + auth + TLS, а в бази є жорстка стеля паралельних backend’ів. Пулери та керовані проксі тримають невеликий стабільний пул серверних сесій за великою кількістю короткоживучих клієнтів PHP.

Пов’язані матеріали: БД під навантаженням · Sail: бази та Docker

Зміст


Чому PHP посилює проблему

Класичний PHP-FPM обробляє запит, звертається до сервісів, відповідає й звільняє ресурси запиту. Без persistent connections кожен запит до БД зазвичай відкриває TCP, автентифікується, за потреби TLS, потім виконує запити.

Під навантаженням:

  • pm.max_children — скільки процесів PHP одночасно на ноді; якщо більшість запитів б’є в БД, може знадобитися стільки ж одночасних сесій на воркер-машину.
  • Воркери черг (queue:work, Horizon) — довгоживучі; кожен паралельний воркер часто тримає відкрите з’єднання під час джоба.
  • Горизонтальне масштабування множить усе: кілька нод із великим max_children дають сотні потенційних сесій до БД.

Під час деплою та стрибків трафіку база бачить шторм коннектів. Навіть якщо max_connections формально достатньо, вдаряють пам’ять на backend (особливо Postgres) і вартість auth.


У чому реальні обмеження

  • Глобальний max_connections; зарезервовані слоти зменшують доступне застосунку.
  • RAM на кожне з’єднання — нескінченно піднімати ліміт небезпечно (OOM).
  • Затримка handshake — TLS і перевірка пароля на кожен запит дорого в сумі.
  • Thundering herd після рестарту — усі процеси PHP підключаються одночасно.

Рахуйте усі програми з SQL: веб, воркери, cron, CLI, адмінки.


Пулери та проксі між шарами

Пулер стоїть між PHP і СУБД. PHP підключається до пулера; пулер тримає менший пул справжніх з’єднань до Postgres/MySQL і перевикористовує їх між клієнтами.

Плюси: менше backend’ів на сервері БД, мультиплексування, рівніша поведінка при сплесках.

Мінуси: додатковий хоп, зміна семантики сесії залежно від режиму, ризик, що сам пулер стане новим вузьким місцем, якщо його не розмірити.


PostgreSQL: PgBouncer

Режим Ідея Застосунок з Laravel
Session Один серверний коннект на всю клієнтську сесію Максимум сумісності: SET, LISTEN, temp tables, prepared statements як при прямому коннекті. Менший виграш за мультиплексування, якщо клієнт довгоживучий.
Transaction Повернення серверного коннекту в пул після COMMIT/ROLLBACK Сильне мультиплексування для коротких HTTP-запитів. Ламає сесію між транзакціями; іменовані prepared statements часто треба вимикати або емулювати на клієнті.
Statement Після кожного оператора Для ORM зазвичай не підходить.

Prepared statements: при зміні фізичного коннекту іменовані prepare ламаються — типові обхідні шляхи: безіменні prepare, simple protocol, PDO::ATTR_EMULATE_PREPARES (за рекомендацією драйвера).

Передайте application_name для трасування в pg_stat_activity.


MySQL / MariaDB: ProxySQL

ProxySQL — проксі протоколу MySQL: маршрутизація, правила, read/write split, пул і налаштовуване мультиплексування. Допомагає обмежити backend-коннекти, поки до проксі підключаються сотні FPM-процесів.

MySQL Router, балансувальники в хмарі можуть не давати той самий рівень мультиплексування — перевіряйте документацію. MariaDB MaxScale — залежить від редакції та ліцензії.


Керовані проксі в хмарі

RDS Proxy, аналоги перед Aurora / Cloud SQL зазвичай дають пулинг і зручніший failover. Обмеження по сесії та prepared statements залишаються — читайте матрицю сумісності для вашого драйвера.

Інші пулери: PgCat, Odyssey, pgpool-II

  • PgCat та Odyssey — пулери для Postgres; порівняйте режими пула, метрики та кварки драйверів із PgBouncer.
  • pgpool-II — часто для реплікації та маршрутизації; важчий в експлуатації, якщо потрібен лише мультиплексинг.

Laravel

  • config/database.php — опції PDO під пулер (емуляція prepare тощо).
  • Read/write + sticky — узгодьте з лагом реплік.
  • Octane / Swoole — довгоживучі воркери: обережно з стікістю з’єднання та транзакціями між запитами.
  • Horizonconcurrency × workers додає стійке навантаження на пул.
  • У проді не залишайте інструменти, що розтягують транзакції.

У .env DB_HOST часто вказує на хост пулера:

DB_HOST=pgbouncer.internal
DB_PORT=6432

Розмір пулу на пулері зв’язуйте з реальною паралельністю запитів, а не лише з pm.max_children.


Що пулер не вилікує

  • N+1 і відсутність індексів.
  • Довгі транзакції (зовнішні HTTP всередині транзакції).
  • Складні DDL / міграції — інколи потрібен прямий коннект до БД.

Чекліст

  1. Перелічити всі типи процесів із SQL.
  2. Порівняти з max_connections і пам’яттю на з’єднання.
  3. Обрати режим пула / правила під ORM і драйвер.
  4. Навантажувальний тест із prepared statements і потрібними сесійними фічами.
  5. Моніторити чергу на пулер і активні коннекти на СУБД.

Пулер «посередині» перетворює сотні коротких клієнтських сесій PHP на десятки реальних backend’ів на базі — саме ту форму, з якою OLTP зазвичай працює стабільніше.