---
title: 'PHP-приложения и узкое горлышко пула соединений с БД | DevSense'
description: 'Почему PHP-FPM и фоновые воркеры умножают число сессий СУБД, как промежуточные пулеры и прокси распределяют физические соединения и что нужно знать о режимах PgBouncer, ProxySQL и подготовленных выражениях.'
faq:
    - { question: 'В чем разница между сессионным (Session) и транзакционным (Transaction) режимами пулa в 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>