Выберите продукт

PHP PDO persistent connections для MySQL/PostgreSQL: настройка, таймауты и контроль max_connections

Persistent-соединения в PHP через PDO уменьшают цену установления коннекта, но могут быстро съесть max_connections и вызвать таймауты. Разбираем механику для MySQL и PostgreSQL, безопасные таймауты и keepalive, расчёт лимитов под php-fpm, мониторинг и план внедрения на VDS и виртуальном хостинге.
PHP PDO persistent connections для MySQL/PostgreSQL: настройка, таймауты и контроль max_connections

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

Иллюстрация таймаутов и TCP keepalive для PHP, MySQL и PostgreSQL

Расчёт лимитов: 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 и БД.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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.

Архитектура PgBouncer в режиме transaction между PHP и 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 из фабрики и повторяем операцию один раз. Бесконечные ретраи запрещены.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

План безопасного внедрения persistent

  1. Аудит: замерьте текущий RPS, среднее и пиковое число php-fpm процессов, активных коннектов к БД.
  2. Единый канон DSN и опций: одинаковые хосты, порты, кодировки, опции PDO во всех местах.
  3. Добавьте инициализацию сессии: SET SESSION для критичных переменных (таймзона, режимы).
  4. Включите persistent на части пула или в staging. Установите консервативные wait_timeout/idle_in_transaction_session_timeout.
  5. Проверьте max_connections: рассчитайте верхнюю границу, оставьте запас 10–20% под админские/репликационные сессии.
  6. Наблюдаемость: графики числа соединений, разбивка по состояниям, ошибки подключения, длительность транзакций.
  7. При необходимости — пулер: для 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 тоже может дать прирост, если соблюдены лимиты и мониторинг.

Поделиться статьей

Вам будет интересно

Linux sysctl и systemd-sysctl: ip_forward, rp_filter, tcp_tw_reuse и порядок override OpenAI Статья написана AI (GPT 5)

Linux sysctl и systemd-sysctl: ip_forward, rp_filter, tcp_tw_reuse и порядок override

Разбираем sysctl и systemd-sysctl на практике: где хранить параметры ядра, что важнее — /etc/sysctl.conf или sysctl.d, как работае ...
ClickHouse on VDS 2026: MergeTree parts, merges и память без сюрпризов OpenAI Статья написана AI (GPT 5)

ClickHouse on VDS 2026: MergeTree parts, merges и память без сюрпризов

Разбираем, почему ClickHouse на VDS внезапно «роняет» SELECT: рост MergeTree parts, фоновые merges, влияние INSERT и конкуренция з ...
Linux: Port 22: Connection timed out — диагностика SSH по шагам (firewall, fail2ban, ss, tcpdump) OpenAI Статья написана AI (GPT 5)

Linux: Port 22: Connection timed out — диагностика SSH по шагам (firewall, fail2ban, ss, tcpdump)

Если SSH «висит» с Connection timed out на 22 порту, чаще виноваты сеть и фильтрация, а не пароль. Разбираем по шагам: проверка кл ...