---
title: 'PHP-застосунки та вузьке місце пулу з’єднань з БД | DevSense'
description: 'Чому PHP-FPM та фонові воркери множать число сесій СУБД, як проміжні пулери та проксі розподіляють фізичні з’єднання і що потрібно знати про режими PgBouncer, ProxySQL та підготовлені вирази.'
faq:
    - { question: 'В чому різниця між сесійним (Session) та транзакційним (Transaction) режимами пулу в PgBouncer?', answer: "Сесійний пул закріплює фізичне з'єднання за клієнтом на весь час його сесії та звільняє тільки при його відключенні. Транзакційний пул повертає фізичне з'єднання назад у пул відразу після закінчення транзакції (`COMMIT` або `ROLLBACK`). Транзакційний режим дозволяє обслуговувати набагато більше клієнтів, але ламає фічі на рівні сесій (тимчасові таблиці, рекомендаційні блокування, налаштування `SET`)." }
    - { question: 'Чому підготовлені вирази (prepared statements) ламаються в транзакційному режимі пулу?', answer: 'У транзакційному режимі послідовні запити одного клієнта можуть виконуватися на різних фізичних серверах БД. Якщо клієнтський запит А підготовлює вираз на сервері 1, а запит Б намагається його виконати на сервері 2, то виконання завершиться з помилкою, оскільки сервер 2 нічого не знає про цей вираз.' }
    - { question: "Як процесна модель PHP-FPM впливає на з'єднання з базою даних порівняно з Node.js або Go?", answer: "PHP-FPM використовує модель «процес-на-запит», де кожен дочірній процес обробляє один запит в один момент часу і закриває ресурси по його закінченні. Під навантаженням це створює «шторм з'єднань» через часті TCP-хендшейки та автентифікації. Node.js і Go працюють у єдиному асинхронному рантаймі, зберігаючи спільний довгоживучий пул з'єднань, що розділяється між запитами." }
    - { question: "Чи вирішує пулер з'єднань проблему повільних SQL-запитів?", answer: "Ні. Пулер з'єднань тільки прибирає накладні витрати на встановлення зв'язку з СУБД і рятує від перевищення лімітів `max_connections`. Він не прискорює виконання повільного SQL, не замінює відсутні індекси і не знижує навантаження на CPU/диск СУБД." }
published: '2026-05-31'
---
# PHP-застосунки та вузьке місце бази даних: пулери, проксі та реальність

У багатьох проектах база даних досить продуктивна, а запити оптимізовані — але в продакшені все одно періодично виникають помилки **`too many connections`**, **`remaining connection slots are reserved`** або виникають **непоясненні зависання** відразу після релізу. Причина часто криється не в повільному SQL, а в **арифметиці з'єднань**: модель роботи PHP генерує **всплески підключень, автентифікацій та TLS-узгоджень**, а у баз даних є **жорсткий ліміт** на кількість одночасних процесів. Проміжні **пулери** та **керовані проксі** створені якраз для того, щоб поставити **невеликий стабільний набір серверних сесій** перед **величезною зграєю короткочасних PHP-клієнтів**.

**Пов’язані матеріали:** [БД під навантаженням: запити та масштабування](database-performance-and-scaling) · [Спостережуваність і моніторинг](observability-monitoring-laravel)

## Зміст

* [Чому PHP посилює проблему](#why-php)
* [Реальні обмеження баз даних](#limits)
* [Проміжні пулери та проксі](#middle-tier)
* [PostgreSQL: PgBouncer на практиці](#pgbouncer)
* [MySQL та MariaDB: ProxySQL та аналоги](#proxysql)
* [Керовані хмарні проксі (RDS Proxy та ін.)](#managed)
* [Альтернативні пулери: PgCat, Odyssey, pgpool-II](#other-tools)
* [Особливості роботи в Laravel](#laravel)
* [Що пулери *не* виправляють](#not-a-cure)
* [Часті помилки](#common-mistakes)
* [Чеклист](#checklist)
* [Квіз для самоперевірки](#self-test-quiz)

---

<a id="why-php"></a>
## Чому PHP посилює проблему

Класичний **PHP-FPM** обробляє запит, опитує сервіси, віддає відповідь і повністю очищає ресурси. Якщо ви не використовуєте **постійні з'єднання** (persistent connections, які мають свої недоліки), кожен запит, що працює з БД, **відкриває** нову TCP-сесію, **проходить автентифікацію**, узгоджує **TLS** і тільки потім виконує запити.

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

* **`pm.max_children`** у конфігурації FPM визначає ліміт процесів PHP, що працюють **одночасно** на сервері. Якщо кожен запит йде в БД, вам знадобиться **до цієї кількості** паралельних сесій БД **на кожен веб-сервер**.
* **Фонові воркери** (`queue:work`, Horizon) — це **довгоживучі процеси**. Кожен активний воркер тримає **одне або більше** відкритих з'єднань під час роботи.
* **Горизонтальне масштабування** множить ці цифри: три сервери по 80 воркерів у кожному дають **240** потенційних з'єднань ще до врахування черг, планувальників та консольних команд.

База даних страждає від **шторму з'єднань (connection storms)** на деплоях та піках трафіку: сотні рукостискань за секунду. Навіть якщо `max_connections` дозволяє це зробити, лімітуючим фактором стають **витрата пам'яті на кожен бекенд** (особливо в Postgres) та **навантаження на CPU при автентифікації**.

---

<a id="limits"></a>
## Реальні обмеження баз даних

* **`max_connections` (Postgres / MySQL)** — глобальний ліміт. Зарезервовані слоти під суперкористувачів та реплікацію знижують доступний застосунку пул.
* **Оперативна пам'ять** — кожне з'єднання СУБД вимагає буферів та пам'яті під стан сесії; просте збільшення ліміту може відправити сервер в **OOM (Out of Memory)**.
* **Затримка з'єднання** — TLS + перевірка пароля + опціональний LDAP додають **від одиниць до десятків мілісекунд** на кожен запит, якщо з'єднання встановлюється заново.
* **Ефект греблі, що прорвалася (Thundering herd)** — після перезапуску кожен процес PHP намагається підключитися **одночасно**, забиваючи чергу очікування.

> [!NOTE]
> **Арифметика сумарного навантаження**
> Емпіричне правило: враховуйте **всі** сервіси, які звертаються до SQL (веб-запити, черги, крон-задачі, адмінки, аналітичні BI-системи), а не тільки HTTP-трафік.

---

<a id="middle-tier"></a>
## Проміжні пулери та проксі

**Пулер** стає **між** PHP та базою даних. PHP відкриває дешеве локальне з'єднання **до пулера**, а пулер тримає **невеликий пул** постійних з'єднань з реальною СУБД і **розподіляє** їх між клієнтами.

### Переваги
* Менше **активних процесів** та витрати **RAM** на сервері БД.
* **Мультиплексування**: прості PHP-клієнти не тримають порожні сесії на СУБД.
* Більш стабільна поведінка при **вибуховому** зростанні трафіку.

### Недоліки та застереження
* Додатковий **мережевий крок** (мережевий хоп, потенційна точка відмови, складність моніторингу).
* Зміна **семантики сесій** залежно від **режиму** пулера — див. розділ про PgBouncer.
* Пулер також потрібно масштабувати за CPU, лімітами файлів (file descriptors) та стежити за чергами.

---

<a id="pgbouncer"></a>
## PostgreSQL: PgBouncer на практиці

**PgBouncer** — стандарт де-факто для пулінгу Postgres в PHP-стеку.

Режими пулу (**pool modes**):

| Режим | Поведінка | Сумісність з PHP / Laravel |
|------|----------|-------------------|
| **Session** (Сесійний) | Одне фізичне з'єднання закріплюється за клієнтом до його відключення | Повна сумісність: працюють `SET`, `LISTEN`, рекомендаційні блокування, тимчасові таблиці, підготовлені вирази. **Мінімальний виграш** по мультиплексуванню, якщо клієнти не відключаються. |
| **Transaction** (Транзакційний) | Фізичне з'єднання повертається в пул **після кожної транзакції** | **Відмінний рівень мультиплексування** для коротких веб-запитів. Ламає **сесійні** фічі: `SET LOCAL` між транзакціями, `LISTEN`, тимчасові таблиці, підготовлені вирази. |
| **Statement** (Операторний) | З'єднання віддається після **кожного SQL-виразу** | Рідко використовується з ORM; повністю ламає багатокрокові транзакції. |

### Підготовлені вирази та транзакційний пул

Драйвери часто готують вирази **за ім'ям** в межах сесії. При зміні фізичного сервера БД у транзакційному режимі іменовані підготовлені запити ламаються. Способи вирішення в продакшені:

* Використовувати **безіменні** підготовлені вирази (simple query protocol).
* **Вимкнути** підготовку на стороні сервера для пулера (наприклад, через `PDO::ATTR_EMULATE_PREPARES`).

```php
// config/database.php
'connections' => [
    'pgsql' => [
        'driver' => 'pgsql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '5432'),
        // ...
        'options' => [
            PDO::ATTR_EMULATE_PREPARES => true, // Емуляція підготовлених запитів на стороні PHP
        ],
    ],
],
```

---

<a id="proxysql"></a>
## MySQL та MariaDB: ProxySQL та аналоги

**ProxySQL** — популярне рішення для протоколу **MySQL**: роутинг, правила для запитів, розділення читання/запису (read/write split) та **пулінг з'єднань** з тонким налаштуванням під схеми та користувачів.

Використовується для:
* Обмеження **з'єднань до БД** від сотень процесів PHP-FPM.
* Маршрутизації **читання** на репліки (з урахуванням відставання реплікації).
* Фільтрації або переписування важких запитів «на льоту» (вимагає обережності, щоб не переносити бізнес-логіку в проксі).

**MySQL Router** (у складі InnoDB Cluster) та деякі **хмарні балансувальники** також вміють керувати з'єднаннями, але **вивчайте документацію**: не кожен інструмент мультиплексує запити так само ефективно, как ProxySQL.

**MariaDB MaxScale** може виступати в ролі розумного маршрутизатора та пулера залежно від ліцензії та використовуваних модулів.

---

<a id="managed"></a>
## Керовані хмарні проксі (RDS Proxy та ін.)

Хмарні провайдери пропонують готові рішення перед базами даних (RDS Proxy для AWS, Cloud SQL Auth Proxy та ін.). Вони беруть на себе:
* **Пулінг** та інтеграцію з системами автентифікації (IAM, токени).
* **Пом'якшення наслідків failover** (перемикання репліки на майстер без падіння сотень PHP-процесів).

Для них діють ті ж **семантичні обмеження**: якщо проксі працює в режимі агресивного мультиплексування, ви зіткнетеся з тими ж нюансами підготовлених виразів та стану сесій, що й на звичайному PgBouncer.

---

<a id="other-tools"></a>
## Альтернативні пулери: PgCat, Odyssey, pgpool-II

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

---

<a id="laravel"></a>
## Особливості роботи в Laravel

* **`config/database.php`** — у секції `options` налаштовуються параметри PDO для узгодження роботи з пулером (наприклад, емуляція prepared statements).
* **Розділення читання/запису** — Laravel підтримує розподіл запитів. При спільному використанні з пулером стежте за параметром `sticky`, щоб уникнути читання старих даних з реплік відразу після запису.
* **Octane / Swoole / FrankenPHP** — довгоживучі воркери змінюють правила гри. Постійні з'єднання **працюють чудово**, але стежте за **витоками стану** між запитами та таймаутами простою (`idle timeout`) на пулері.
* **Horizon / воркери** — паралельні завдання генерують постійний потік з'єднань. Використовуйте пул **на кожного воркера** або транзакційний режим з сумісними налаштуваннями.

Приклад налаштування оточення при використанні пулера:
```env
# .env
# PHP підключається до PgBouncer на порт 6432; PgBouncer йде в Postgres на 5432
DB_HOST=pgbouncer.internal
DB_PORT=6432
DB_DATABASE=app
DB_USERNAME=app_rw
```

---

<a id="not-a-cure"></a>
## Что пулеры *не* виправляють

* **Проблему N+1 запитів** та відсутність індексів — неоптимальний код все так само буде вантажити **CPU та диск** сервера БД. Пулер тільки обмежує число паралельних сесій, що виконують ці запити.
* **Довгі транзакції** — якщо код тримає транзакцію відкритою під час зовнішніх HTTP-викликів, фізичне з'єднання СУБД блокується і не повертається в пул, зводячи нанівець переваги транзакційного режиму.
* **Глобальні блокування та міграції** — запуск `artisan migrate` через перевантажений пулер може призвести до конфлікту блокувань. Для DDL-операцій краще використовувати **пряме підключення** до бази даних.

---

<a id="common-mistakes"></a>
## Часті помилки

1. **Використання змінних сесії в транзакційному пулі**: Застосування тимчасових таблиць або виконання команд типу `SET TIMEZONE` в транзакційному режимі PgBouncer, через що налаштування хаотично перетікають до інших клієнтів.
2. **Забута емуляція підготовлених запитів**: Відсутність прапорця `PDO::ATTR_EMULATE_PREPARES => true` при роботі з PgBouncer у режимі транзакцій, що призводить до помилок "prepared statement already exists".
3. **Неузгоджені ліміти пулера та СУБД**: Налаштування максимального розміру бекенд-пулу в PgBouncer більшим, ніж фізичний ліміт `max_connections` на сервері PostgreSQL.
4. **Постійні PDO-з'єднання в FPM без контролю**: Ввімкнення `PDO::ATTR_PERSISTENT` у веб-застосунку без обмеження життєвого циклу процесів FPM, через що з'єднання забивають базу.

---

<a id="checklist"></a>
## Чеклист

1. **Проведено аудит** усіх типів процесів, що відкривають з'єднання з SQL (максимальне число процесів FPM × ноди, воркери черг, крон-задачі).
2. Сумарне число з'єднань зіставлено з **`max_connections`** та обсягом **RAM** на сервері БД.
3. Вибрано **режим роботи пулу** (Postgres) або **правила мультиплексування** (MySQL), сумісні з ORM та драйвером.
4. Навантажувальними тестами перевірена робота **підготовлених виразів** та **сесійних функцій** (`SET`, тимчасові таблиці, блокування).
5. Налаштовано метрики **часу очікування в пулі** та **активних фізичних сесій** СУБД.

---

## Підсумок

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

---

<a id="self-test-quiz"></a>
## Квіз для самоперевірки

### Запитання 1: Що станеться при спробі використати рекомендаційні блокування (advisory locks) в Postgres через PgBouncer в режимі транзакційного пулу (transaction pooling)?
- А) Блокування будуть працювати коректно, оскільки PgBouncer сам відстежує їхній стан.
- Б) Блокування може бути втрачене або заблокувати сесію іншого клієнта при зміні фізичного з'єднання між запитами.
- В) PgBouncer відразу поверне синтаксичну помилку SQL.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Рекомендаційні блокування прив'язані до фізичної сесії СУБД. У режимі транзакцій ваш наступний запит може піти на інше фізичне з'єднання, залишивши блокування висіти на попередньому бекенді.
</details>

### Запитання 2: В чому основна відмінність моделі роботи з базою даних PHP-FPM від асинхронних рантаймів (Go, Node.js)?
- А) PHP-FPM не підтримує постійні TCP-сесії на транспортному рівні.
- Б) PHP-FPM завершує контекст виконання в кінці кожного запиту, закриваючи та відкриваючи дескриптори заново, тоді як Go/Node.js тримають постійний пул у пам'яті процесу.
- В) Node.js та Go використовують вбудовані СУБД прямо всередині рантайму.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Оскільки стек виконання PHP ізольований у межах одного запиту, повторне відкриття з'єднань відбувається на кожному циклі, якщо не налаштовані складні механізми постійного кешування дескрипторів.
</details>