Persistent connections в PHP через PDO — инструмент полезный, но коварный. Он позволяет переиспользовать открытые соединения к СУБД между запросами и заметно снижает накладные расходы на connect, TLS-рукопожатия и аутентификацию. Но на практике чаще всего проблемы возникают не от их отсутствия, а от неконтролируемого роста «спящих» коннектов, таймаутов и исчерпания max_connections на сервере базы данных. В этой статье я, Вася из Fastfox, собрал проверенные подходы для MySQL и PostgreSQL: как включать persistent, какие таймауты ставить, как считать лимиты для php-fpm и чем мониторить ситуацию.
Как работает persistent в PDO
В PHP persistent-соединение — это не «вечный TCP», а возможность драйвера вернуть из локального пула уже установленный коннект, закреплённый за процессом интерпретатора. Для веба это обычно процессы php-fpm. Ключевые особенности:
- Пул соединений — пер-процесс: каждый рабочий процесс php-fpm держит свои коннекты. Если у вас 24
pm.max_children, пиково в СУБД может появиться до 24 соединений только от одного пула и только для одной БД/пары кредов/настроек. - Совпадение параметров: соединение переиспользуется, если совпадает DSN, логин, набор опций (например,
PDO::ATTR_EMULATE_PREPARES) и другие атрибуты. Любые отличия — новый слот в пуле. - Сброс состояния: современные драйверы стараются сбрасывать состояние сессии (транзакции, переменные), но полагаться на это на 100% нельзя. Безопаснее явно задавать нужную «гигиену сессии» сразу после получения PDO.
- Не равно TCP keepalive: persistent — про переиспользование объекта соединения в PHP. Сетевые keepalive-настройки живут в ОС и СУБД и влияют на то, как долго «живёт» простой на уровне TCP.
Золотое правило: persistent-соединения уменьшают цену connect, но увеличивают время жизни «хвостов». Их нужно планировать, лимитировать и мониторить.
Когда включать persistent
- Высокая частота коротких запросов, а коннект стоит дорого: TLS к БД, удалённый хост, агрессивный
max_connections. См. также материалы по настройке TLS: TLS между приложением и MySQL/PostgreSQL. - У вас контролируемое число рабочих процессов (php-fpm) и понятная модель нагрузки.
- Микросервисы/очереди с частыми коннектами по CLI: для долгоживущих воркеров persistent уменьшает флаттер соединений.
Когда лучше не включать:
- Очень много php-fpm процессов без строгих лимитов — есть риск упереться в
max_connections. - Вы используете PostgreSQL без пула (PgBouncer/аналог) при десятках и сотнях приложений — дешевле и стабильнее вынести пул на отдельный уровень.
- Приложение активно меняет сессионные переменные и не приводит их к канону на каждом получении PDO.
Включаем persistent в коде: PDO для MySQL
Минимальный пример с «гигиеной сессии», безопасными настройками и таймаутом подключения:
<?php
$dsn = 'mysql:host=db1;port=3306;dbname=app;charset=utf8mb4';
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => 2, // секунды, таймаут подключения
// Для старых окружений можно продублировать инициализацию:
// PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION sql_mode='STRICT_ALL_TABLES', time_zone='+00:00'"
];
$pdo = new PDO($dsn, 'app', 'secret', $options);
// Гигиена сессии на всякий случай (важно при persistent)
$pdo->exec("SET SESSION sql_mode='STRICT_ALL_TABLES', time_zone='+00:00'");
Пояснения:
charset=utf8mb4в DSN корректно выставит кодировку через mysqlnd.PDO::ATTR_TIMEOUTвлияет на таймаут подключения. Для таймаута выполнения запросов используйте логику на уровне приложения или server-side лимиты.PDO::ATTR_EMULATE_PREPARES = falseактивирует серверные подготовленные выражения там, где это выгодно.
PDO для PostgreSQL: persistent и параметры подключения
Пример подключения с connect_timeout и полезным application_name для диагностики:
<?php
$dsn = 'pgsql:host=pg1;port=5432;dbname=app;connect_timeout=2;application_name=php-fpm';
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
$pdo = new PDO($dsn, 'app', 'secret', $options);
$pdo->exec("SET SESSION TIME ZONE 'UTC'");
Для PostgreSQL persistent увеличивают число одновременно открытых серверных сессий. Если у вас много приложений/пулов php-fpm, почти всегда выгоднее использовать отдельный пулер (например, в режиме transaction) — об этом ниже.
Таймауты и keepalive: где настраивать
PHP
PDO::ATTR_TIMEOUT— таймаут подключения в секундах (MySQL/pgsql поддерживают).default_socket_timeoutв php.ini влияет на сетевые потоки, но не заменяет таймауты выполнения SQL.
MySQL/MariaDB
wait_timeout— сколько держать неинтерактивные idle-сессии. При persistent уменьшайте разумно, но помните: закрытое сервером соединение означает переподключение при следующем запросе.interactive_timeout— то же для интерактивных клиентов.max_connections— общий лимит соединений. Заложите запас под админские сессии и репликацию.- TCP keepalive — на уровне ОС (
net.ipv4.tcp_keepalive_time) и mysqld (наследует системные). Помогает чистить «мертвые» соединения при разрывах сети.
Пример фрагмента my.cnf для веб-нагрузки:
[mysqld]
max_connections = 200
wait_timeout = 120
interactive_timeout = 1800
skip_name_resolve = 1
PostgreSQL
max_connectionsиsuperuser_reserved_connections— лимиты серверных сессий.idle_in_transaction_session_timeout— авто-убийство спящих сессий, застрявших в транзакции.tcp_keepalives_idle,tcp_keepalives_interval,tcp_keepalives_count— keepalive-параметры подключения.statement_timeout— лимит времени выполнения запросов на серверной стороне.
Пример фрагмента postgresql.conf:
max_connections = 200
superuser_reserved_connections = 3
idle_in_transaction_session_timeout = 60000 # 60s
statement_timeout = 0 # или заданное значение
tcp_keepalives_idle = 300
tcp_keepalives_interval = 30
tcp_keepalives_count = 4

Расчёт лимитов: php-fpm, persistent и max_connections
Базовая оценка максимального количества соединений от одного пула php-fpm:
- Верхняя граница:
pm.max_children× (число уникальных DSN × опций × пользователей) в текущем процессе. - Добавьте сюда другие пулы PHP, воркеры очередей и фоновые задания.
- Для PostgreSQL без пула множитель может стать критичным уже при десятках приложений.
Пример пула php-fpm:
[app]
user = app
pm = dynamic
pm.max_children = 24
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.max_requests = 500
Если приложение обращается к двум БД с разными кредами и набором опций — умножайте. Именно поэтому важно унифицировать DSN и опции, чтобы не плодить «лишние» слоты в пуле PDO. Для оптимизации общей памяти и снижения накладных в стеке PHP/БД посмотрите также заметку о памяти и HugeTLB/THP: HugePages/THP для PHP и БД.
PostgreSQL: PgBouncer против persistent
PostgreSQL плохо масштабируется по числу серверных сессий: каждая — отдельный backend-процесс с памятью. Поэтому в нагруженных веб-проектах выгодно использовать пулер соединений (часто PgBouncer): он держит ограниченное число серверных коннектов и тысячами обслуживает клиентские.
Минимальный контур PgBouncer:
[databases]
app = host=pg1 port=5432 dbname=app
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
server_idle_timeout = 300
ignore_startup_parameters = extra_float_digits
А в приложении:
<?php
$dsn = 'pgsql:host=pgbouncer.local;port=6432;dbname=app;connect_timeout=2;application_name=php-fpm';
$options = [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo = new PDO($dsn, 'app', 'secret', $options);
В режиме transaction PgBouncer раздаёт серверные соединения на время транзакции, что отлично сочетается с веб-нагрузкой и ограничивает рост «спящих» backend-процессов PostgreSQL.

Диагностика и наблюдаемость
MySQL/MariaDB
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Aborted_connects';
SHOW PROCESSLIST;
SELECT user, host, command, time FROM information_schema.PROCESSLIST ORDER BY time DESC LIMIT 20;
Обратите внимание на число Sleep и длительность idle. Если «спящих» сотни — проверьте таймауты, количество процессов php-fpm и унификацию DSN.
PostgreSQL
SELECT datname, usename, application_name, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
ORDER BY state, usename, datname;
SELECT state, COUNT(*) FROM pg_stat_activity GROUP BY state;
SELECT name, setting FROM pg_settings WHERE name IN ('max_connections','superuser_reserved_connections');
Большое число idle — это нормально, вопрос в масштабах. Если видите много idle in transaction — добавьте idle_in_transaction_session_timeout и проверьте логику приложения.
Типичные ловушки и анти‑паттерны
- Неограниченный php-fpm. Случайно завышенные
pm.max_childrenмгновенно упирают вас вmax_connectionsБД. - Раздробленные DSN. Разные хостнеймы (alias), наборы опций и креды делают пулы несовместимыми и множат соединения.
- Грязная сессия. Сессионные переменные не приводятся к канону; следующий запрос получает «чужое» состояние. Всегда выполняйте инициализацию сессии.
- Долгие транзакции. Persistent плюс открытая транзакция удерживают ресурсы и блокировки дольше, чем вы думаете.
- Слишком малый wait_timeout в MySQL без ретраев. Сервер разорвал idle-коннект — первая же операция получает «server has gone away».
Надёжный ретрай при обрыве (MySQL)
При умеренных wait_timeout и persistent возможны разрывы idle-коннектов. Добавьте один безопасный ретрай на «server has gone away»:
<?php
function queryWithReconnect(PDO $pdo, string $sql, array $params = []) {
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
$msg = $e->getMessage();
if (strpos($msg, 'server has gone away') !== false || strpos($msg, 'Lost connection') !== false) {
// Одно переподключение
$refl = new ReflectionClass($pdo);
$ctor = $refl->getConstructor();
// В реальном коде храните DSN/логин/пароль/опции отдельно и создавайте новый PDO
throw $e; // Заглушка: реализуйте фабрику PDO и повтор вызова
}
throw $e;
}
}
Идея проста: при сетевом обрыве создаём новый PDO из фабрики и повторяем операцию один раз. Бесконечные ретраи запрещены.
План безопасного внедрения persistent
- Аудит: замерьте текущий RPS, среднее и пиковое число php-fpm процессов, активных коннектов к БД.
- Единый канон DSN и опций: одинаковые хосты, порты, кодировки, опции PDO во всех местах.
- Добавьте инициализацию сессии:
SET SESSIONдля критичных переменных (таймзона, режимы). - Включите persistent на части пула или в staging. Установите консервативные
wait_timeout/idle_in_transaction_session_timeout. - Проверьте
max_connections: рассчитайте верхнюю границу, оставьте запас 10–20% под админские/репликационные сессии. - Наблюдаемость: графики числа соединений, разбивка по состояниям, ошибки подключения, длительность транзакций.
- При необходимости — пулер: для PostgreSQL почти всегда выгоден PgBouncer в режиме
transaction; для MySQL уместны прокси/пулы при большом числе клиентов.
FAQ
Это то же самое, что TCP keepalive?
Нет. Persistent — про переиспользование логического соединения в PHP. TCP keepalive — сетевой механизм обнаружения «мертвых» пиров.
Что делает PDO::ATTR_TIMEOUT?
Это таймаут подключения. Таймаут выполнения запросов задаётся на стороне БД (например, statement_timeout в PostgreSQL) или логикой приложения.
Можно ли persistent в CLI-скриптах и очередях?
Да, особенно для долгоживущих воркеров. Но следите за тем, чтобы воркеры корректно завершались и освобождали ресурсы.
Как понять, что я упираюсь в max_connections?
Ошибки «Too many connections» в MySQL или «sorry, too many clients already» в PgBouncer/«remaining connection slots are reserved» в PostgreSQL. Лечится уменьшением числа клиентов, пулами, повышением лимитов и оптимизацией.
Итоги
Persistent connections в PHP через PDO — мощный инструмент, если управлять ими как ресурсом: учитывать архитектуру php-fpm, унифицировать DSN и опции, задавать таймауты и лимиты, включить мониторинг. Для MySQL это чаще всего вопрос дисциплины и ретраев при разрывах. Для PostgreSQL в нагруженных инсталляциях почти всегда выигрывает связка «PHP + PgBouncer (transaction) + строгие таймауты». На VDS вы контролируете весь стек — от php-fpm и драйверов до конфигурации СУБД и системных keepalive. Для небольших проектов на виртуальном хостинге persistent тоже может дать прирост, если соблюдены лимиты и мониторинг.


