---
title: 'PHP приложенията и тясното място на пула от връзки към БД | DevSense'
description: 'Защо PHP-FPM и фоновите воркери умножават сесиите към базата данни, как прокситата от средно ниво споделят реални връзки и какво трябва да знаят Laravel екипите за PgBouncer режимите, ProxySQL и prepared statements.'
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 handshakes и автентификации). За разлика от него, Node.js и Go използват асинхронни среди с един процес, които поддържат общ дълготраен пул от връзки, споделен между всички заявки." }
    - { question: 'Решава ли пулингът на връзките проблема с бавните заявки към базата данни?', answer: 'Не. Пулингът решава единствено проблема с режийните разходи за установяване на връзка и предотвратява превишаването на лимитите `max_connections` на сървъра. Той не ускорява бавния SQL код, не замества липсващи индекси и не намалява натоварването върху CPU/диска на БД, причинено от неоптимизирани заявки.' }
published: '2026-05-31'
---
# PHP приложенията и тясното място на базата данни: пулери, проксита и реалност

При много технологични стекове базата данни е достатъчно бърза и заявките са оптимални — но в продакшън все пак се появяват грешки като **`too many connections`**, **`remaining connection slots are reserved`** или **странни забивания** веднага след внедряване. Причината често не е в бавен SQL код, а в **аритметиката на връзките**: моделът на заявките в PHP създава **вълни от свързване + автентификация + TLS**, а базата данни има **твърд таван** за паралелни процеси. Прокситата от средно ниво (**poolers**) и **управляваните проксита** съществуват точно за да поставят **малък, стабилен набор от сървърни сесии** пред **огромно ято от краткотрайни 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** изпълнява заявка, комуникира с услугите, връща отговор и изчиства ресурсите на ниво заявка. Освен ако не използвате **персистентни (постоянни) връзки** (и приемете техните компромиси), всяка заявка, която докосва базата данни, обикновено **отваря** TCP сесия, **автентифицира се**, по избор договаря **TLS** и след това изпълнява SQL заявките.

Под натоварване:

* **`pm.max_children`** в конфигурацията на FPM дефинира колко PHP процеса могат да работят **едновременно** на дадена машина. Ако повечето заявки изискват достъп до БД, ще ви трябват **до толкова** паралелни сесии към базата **за всяка уеб машина**.
* **Воркерите за опашки** (`queue:work`, Horizon) са **дълготрайни процеси** — всеки воркер често държи **една или повече** отворени връзки, докато изпълнява задачи.
* **Хоризонталното скалиране** умножава всичко: три сървъра с по 80 процеса дават **240** потенциални сесии още преди да преброите воркерите, планировчиците и CLI задачите.

Базата данни вижда **буря от връзки (connection storms)** при деплой и пикове в трафика: стотици ръкостискания в секунда. Дори когато `max_connections` е достатъчно висок, **паметта за всеки процес** (особено при Postgres) и **натоварването на CPU за автентификация** стават истинският лимит.

---

<a id="limits"></a>
## Какви са реалните ви ограничения

* **`max_connections` (Postgres)** / **`max_connections` (MySQL)** — глобален таван. Резервираните слотове за суперпотребители и репликация могат да намалят това, което получават приложенията.
* **Памет** — всеки сървърен процес носи буфери и състояние; простото вдигане на лимита може да доведе до **OOM (Out of Memory)** срив на сървъра.
* **Латентност при свързване** — TLS + проверка на парола + опционален LDAP добавят **от няколко до десетки милисекунди** на заявка, ако се свързвате всеки път.
* **Ефект на тълпата (Thundering herd)** — след рестарт всеки PHP процес може да се опита да се свърже **едновременно**, претоварвайки опашката или пътя за автентификация.

> [!NOTE]
> **Аритметика на общото натоварване**
> Основно правило: бройте **всички** програми, които говорят с базата данни (уеб, воркери, cron задачи, административни инструменти, BI системи), а не само HTTP трафика.

---

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

**Пулерът** стои **между** PHP и базата данни. PHP отваря евтина връзка **към пулера**; пулерът поддържа **по-малък пул** от реални връзки към Postgres/MySQL и ги **преизползва** за много клиенти.

### Ползи
* По-малко **активни процеси** и по-малко **RAM** на хоста на базата данни.
* **Мултиплексиране**: много празни PHP клиенти не блокират празни сървърни сесии.
* По-плавно поведение при **пиков** трафик.

### Разходи и предупреждения
* Още един **мрежов хоп** (латентност, потенциална точка на отказ, сложност при конфигуриране и мониторинг).
* **Семантиката на сесиите** се променя в зависимост от **режима** на пулинг — вижте PgBouncer по-долу.
* Все още трябва да оразмерите пулера правилно, за да не се превърне той в **новото** тясно място.

---

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

**PgBouncer** е стандартът де факто за пулинг на Postgres в PHP среди.

Чести **режими на пулинг (pool modes)**:

| Режим | Поведение | Подходящ за PHP / Laravel |
|------|----------|-------------------|
| **Session** | Една сървърна връзка се запазва за цялата клиентска сесия до прекъсване | Най-сигурна съвместимост: `SET`, `LISTEN`, временни таблици, подготвени изрази работят. **Най-малка полза от мултиплексиране**, ако клиентите остават свързани дълго. |
| **Transaction** | Сървърната връзка се връща в пула **след всяка трансакция** (COMMIT/ROLLBACK) | **Силно мултиплексиране** за кратки уеб заявки. Нарушава функции на ниво **сесия**: `SET LOCAL` между трансакции, `LISTEN`, временни таблици, подготвени изрази. |
| **Statement** | Сървърната връзка се освобождава след **всяка заявка** | Рядко за ORM; нарушава трансакциите от няколко стъпки. Не е типична цел за Laravel. |

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

Много драйвери подготвят изрази **по име** в рамките на сесията. Когато физическата сървърна връзка се промени под вас, **именуваните подготвени изрази** могат да се счупят. Решения в продакшън среди:

* Предпочитайте **неименувани** подготвени изрази / протокола за прости заявки (simple query), или
* **Деактивирайте** подготвянето на изрази от страна на сървъра за връзката към пулера (често чрез `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 протокола**: маршрутизиране, правила за заявки, разделяне на четене/запис и **пулинг на връзки** с правила за мултиплексиране, настроени за конкретен потребител/схема.

Екипите го използват за:
* Ограничаване на **връзките към базата данни**, докато стотици PHP-FPM процеси се свързват към ProxySQL.
* Маршрутизиране на **четенето** към реплики (все пак следете **забавянето на репликацията**).
* Филтриране или пренаписване на определени заявки директно в проксито (с повишено внимание — това добавя сложност).

**MySQL Router** (InnoDB Cluster) и някои **облачни балансьори** предлагат функции, подобни на пулинг, но **проверете документацията**: не всеки слой мултиплексира по същия начин като ProxySQL.

**MariaDB MaxScale** може да работи като маршрутизатор с функции за управление на връзките в зависимост от версията и модулите — проверете **лицензирането** преди внедряване.

---

<a id="managed"></a>
## Управлявани проксита (RDS Proxy и др.)

Облачните доставчици предлагат **управлявани проксита за връзки** пред RDS, Aurora, Cloud SQL и др. Те обикновено управляват:
* **Пулинг** и интеграция с **IAM или токен автентификация**.
* **Лесно превключване при отказ (failover)** (превключване на базата без необходимост от прекъсване на всички PHP процеси едновременно).

Те все още спазват **семантиката на базата данни**: ако продуктът мултиплексира агресивно, ще се сблъскате със същите ограничения за **подготвените изрази** и **състоянието на сесията**, както при собствено хостван PgBouncer.

---

<a id="other-tools"></a>
## Други пулери: PgCat, Odyssey, pgpool-II

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

---

<a id="laravel"></a>
## Бележки, специфични за Laravel

* **`config/database.php`** — `connections.*.options` и флаговете на драйвера са местата, където напасвате поведението на **PDO** с вашия пулер (напр. емулиране на подготвени изрази).
* **Разделяне на четене/запис** — Laravel може да изпраща заявките за извличане към `read` хостове; когато използвате пулер, се уверете, че **sticky** семантиката съвпада с очакванията ви (забавяне на репликата спрямо `sticky` конфигурацията).
* **Octane / Swoole / FrankenPHP** — **дълготрайните** воркери променят изчисленията: персистентните връзки могат да **работят добре**, но трябва да избягвате **изтичане** на състоянието на връзката между заявките и да следите **idle timeout** на сървъра и пулера.
* **Horizon / `queue:work`** — паралелността на воркерите добавя **постоянни** връзки; използвайте пул **за всеки воркер** или **трансакционен режим** със съвместими настройки.

Примерна конфигурация на средата с използване на пулер:
```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 и I/O** на сървъра — пулингът само ограничава **колко сесии** могат да изразят това натоварване едновременно.
* **Дългите трансакции** блокират сървърните връзки — вашите ползи от **трансакционния режим** изчезват, ако кодът държи трансакции отворени по време на външни HTTP повиквания.
* **Глобалните заключвания и миграциите** — изпълнението на `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 max children × брой сървъри, Horizon воркери, cron, CLI).
2. Сравнете общия брой с **`max_connections`** и **RAM на връзка** в БД — първо решете дали имате нужда от **пулер или от повече дисциплина в приложението**.
3. Изберете **режим на пулинг** (Postgres) или **правила за мултиплексиране** (MySQL), които съответстват на възможностите на **ORM + драйвера**.
4. Валидирайте **подготвените изрази** и **сесийните функции** (`SET`, временни таблици, заключвания) под натоварване.
5. Наблюдавайте **времето за изчакване в пулера** и **активните сървърни връзки** — ако опашката на пулера расте, базата данни или качеството на заявките все още са лимитът.

---

## Извод

Прокситата от средно ниво са **инфраструктура, която управлявате** (или купувате). Използвани правилно, те превръщат хаотичните „800 връзки от PHP“ в стабилни „60 активни сесии към Postgres“ — което е точно режимът, за който са проектирани повечето релационни бази данни.

---

<a id="self-test-quiz"></a>
## Тест за самопроверка

### Въпрос 1: Какво се случва, ако се опитате да използвате PostgreSQL препоръчителни заключвания (advisory locks) през PgBouncer в режим на трансакционен пулинг?
- А) Заключванията работят правилно, тъй като PgBouncer ги прихваща.
- Б) Заключванията могат да заключат грешна сесия или да бъдат загубени при смяна на физическата връзка между заявките.
- В) PgBouncer веднага ще върне SQL грешка.

<details>
<summary>Кликнете, за да видите отговора</summary>

**Отговор: Б**
Препоръчителните заключвания са обвързани с физическата сесия на базата данни. В режим на трансакции следващата ви заявка може да бъде изпратена по друга физическа връзка, оставяйки заключването върху предишния процес.
</details>

### Въпрос 2: Защо PHP-FPM създава натоварване при свързване в сравнение с асинхронни среди като Go или Node.js?
- А) PHP-FPM процесите не поддържат TCP връзки.
- Б) PHP-FPM изчиства състоянието в края на всяка заявка, затваряйки и отваряйки връзките към базата данни многократно.
- В) Node.js и Go използват вградени бази данни.

<details>
<summary>Кликнете, за да видите отговора</summary>

**Отговор: Б**
Тъй като контекстът на изпълнение в PHP е ограничен до една заявка, връзките се договарят и затварят при всяка заявка, освен ако не са конфигурирани сложни персистентни механизми.
</details>