Пул соединений с БД: как работает и лучшие практики
Зачем нужен пул соединений
Каждый запрос к базе данных требует соединения. Создание нового соединения включает TCP-handshake, аутентификацию, выделение памяти на сервере БД и инициализацию сессии. Для MySQL это занимает 5–50 мс. Для PostgreSQL — 50–100 мс из-за форка процесса. Умножьте это на тысячи запросов в секунду, и накладные расходы на соединения становятся серьёзным узким местом.
Пул соединений решает эту проблему, поддерживая набор заранее установленных соединений, которые разделяются между запросами. Вместо создания и уничтожения соединений приложение берёт соединение из пула, использует его и возвращает обратно. Результат: значительно меньшая задержка, более высокая пропускная способность и сниженная нагрузка на сервер БД.
Как работает пул соединений
Пул соединений — это кэш подключений к базе данных. Жизненный цикл:
- Инициализация — пул создаёт минимальное количество соединений при запуске (minimum pool size)
- Заимствование — когда коду нужно соединение, он запрашивает его у пула. Если есть свободное — возвращается немедленно. Если нет — создаётся новое (до максимального размера пула)
- Использование — приложение выполняет запросы через заимствованное соединение
- Возврат — после использования соединение возвращается в пул
- Вытеснение — простаивающие соединения, превысившие idle timeout, закрываются для освобождения ресурсов
- Валидация — перед выдачей пул может проверить, что соединение живо (Ping или тестовый запрос)
Настройка размера пула
Самая критичная настройка. Слишком мал — запросы встают в очередь за соединениями, увеличивая задержку. Слишком велик — база данных перегружена параллельными соединениями, каждое из которых потребляет память и CPU.
| Параметр | Описание | Типичное значение |
|---|---|---|
| min_pool_size | Соединения, создаваемые при старте | 5–10 |
| max_pool_size | Максимальное число одновременных соединений | 20–50 |
| idle_timeout | Закрытие простаивающих соединений после | 300 с |
| connection_timeout | Максимальное ожидание соединения из пула | 5–30 с |
| max_lifetime | Закрытие соединения после общего времени жизни | 1800 с |
Формула оптимального размера пула
Широко цитируемая формула из вики PostgreSQL:
pool_size = (количество_ядер * 2) + количество_дисков
Пример для 4-ядерного сервера с SSD:
pool_size = (4 * 2) + 1 = 9
Это отправная точка, а не окончательный ответ. Правильный размер зависит от длительности запросов, паттернов конкурентности и типа нагрузки (CPU-bound или I/O-bound). Профилируйте под реалистичной нагрузкой.
Распространённая ошибка — слишком большой пул. Производительность PostgreSQL значительно деградирует при более чем ~100 соединениях из-за конкуренции за блокировки и переключения контекста. MySQL выдерживает больше, но тоже страдает при 200–300+.
Пул соединений в PHP
Жизненный цикл PHP-запроса создаёт уникальную проблему: каждый запрос обычно создаёт новое соединение с БД и уничтожает его в конце. Нативный PHP не имеет встроенного пулирования. Решения:
- Persistent connections —
PDO::ATTR_PERSISTENT = trueповторно использует соединения между запросами одного worker-процесса. Просто, но ограничено: нет контроля размера пула, возможны утечки - ProxySQL — middleware-прокси, пулирующий соединения между PHP и MySQL. Гибко настраивается, поддерживает мультиплексирование
- MySQL Router — официальный middleware от MySQL для управления соединениями и балансировки
- Swoole/RoadRunner — PHP application серверы с поддержкой пулов соединений между запросами
// PHP PDO persistent connection
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp',
'user',
'password',
[PDO::ATTR_PERSISTENT => true]
);
// С ProxySQL — подключение через прокси вместо MySQL напрямую
$pdo = new PDO(
'mysql:host=127.0.0.1;port=6033;dbname=myapp',
'user',
'password'
);
Пул соединений с PgBouncer
PgBouncer — самый популярный пулер для PostgreSQL. Он располагается между приложением и PostgreSQL, управляя пулом серверных соединений:
# /etc/pgbouncer/pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
PgBouncer поддерживает три режима пулирования:
- Session mode — одно серверное соединение на клиентскую сессию. Самый безопасный, но наименее эффективный
- Transaction mode — соединение назначается на транзакцию. Лучший для большинства веб-приложений. В 10 раз эффективнее использование соединений
- Statement mode — соединение назначается на оператор. Самый агрессивный, но ломает многооператорные транзакции
Мониторинг пулов соединений
Метрики для отслеживания:
- Активные соединения — используемые в данный момент. Если постоянно равны max_pool_size — пул насыщен
- Простаивающие соединения — ожидающие работы. Слишком много — трата памяти БД
- Длина очереди ожидания — запросы, ждущие соединения. Ненулевое значение = пул слишком мал или запросы слишком медленные
- Частота создания соединений — частое создание = отток, что нивелирует смысл пулирования
- Среднее время ожидания — время ожидания соединения. Должно быть близко к нулю
-- MySQL: проверка активных соединений
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW STATUS LIKE 'Max_used_connections';
-- PostgreSQL: проверка соединений
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
Типичные ошибки
- Утечки соединений — заимствование без возврата. В итоге пул исчерпывается, и все запросы зависают. Всегда используйте try/finally
- Пул на каждый экземпляр микросервиса — 20 экземпляров с пулом 50 = 1000 соединений к БД. Координируйте размеры пулов между сервисами
- Отсутствие валидации — протухшие соединения (после перезапуска БД, сетевого сбоя) вызывают ошибки. Включите проверку test-запросом
- Длинные транзакции — транзакция, удерживающая соединение секунды, блокирует другие запросы, фактически уменьшая размер пула
Лучшие практики
- Начинайте с небольшого пула (10–20) и увеличивайте на основе мониторинг сайтов
- Устанавливайте connection timeout для предотвращения бесконечного ожидания (5–10 секунд)
- Используйте transaction-mode для веб-нагрузок (PgBouncer) или ProxySQL для MySQL
- Активно мониторьте насыщение пула и метрики очереди ожидания
- Убедитесь, что все пути кода возвращают соединения (обработка ошибок и исключений)
- Координируйте размеры пулов при запуске нескольких экземпляров приложения
- Тестируйте отказоустойчивость: что происходит при перезапуске БД?
- Держите транзакции короткими для максимальной доступности соединений
Заключение
Пулирование соединений — одна из самых эффективных и простых в реализации оптимизаций для приложений, работающих с базами данных. Оно снижает задержку, увеличивает пропускную способность и защищает БД от «штормов» соединений. Выберите правильную стратегию для вашего стека, тщательно настройте размеры пулов, мониторьте насыщение и избегайте утечек. Результат будет немедленным и измеримым.
Проверьте ваш сайт прямо сейчас
Проверить →