Прод заливает ошибками SQLSTATE[HY000] [1040] Too many connections на пике нагрузки
На пике PHP начинает бросать SQLSTATE[HY000] [1040] Too many connections, и сайт отдаёт 500. Ничего не деплоили, а трафик всего на ~20% выше прошлонедельного пика. За балансировщиком стоят четыре сервера PHP-FPM; MySQL — один primary.
Ограничения: перезапускать MySQL нельзя, и число надо объяснить, а не просто поднять.
$ mysql -e "SHOW VARIABLES LIKE 'max_connections'"
max_connections 151
$ mysql -e "SHOW STATUS LIKE 'Threads_connected'"
Threads_connected 151
$ mysql -e "SHOW PROCESSLIST" | awk '{print $5}' | sort | uniq -c
148 Sleep
3 Query
# 4 сервера приложения, на каждом pm.max_children = 50
# DSN для PDO собран с PDO::ATTR_PERSISTENT => true
Определите причину.
Посчитайте потолок: четыре сервера на pm.max_children 50 — это 200 возможных воркеров, тогда как max_connections равен 151, и PHP способен запросить больше соединений, чем MySQL выдаст. ATTR_PERSISTENT усугубляет: каждый воркер держит свой сокет открытым между запросами, поэтому 148 из 151 в состоянии Sleep и лишь 3 выполняют запрос. Ограничьте пулы FPM или поставьте пулер вроде pgbouncer/ProxySQL — а не просто поднимайте max_connections.
- ✗Поднимать
max_connections, не сверив с ним произведение серверов наpm.max_children - ✗Читать потоки
Sleepкак свободный запас, а не как воркеров с открытыми persistent-сокетами - ✗Называть
ATTR_PERSISTENTпулом соединений — он привязывает один сокет к одному воркеру
- →Какая арифметика связывает
pm.max_childrenсmax_connectionsчерез несколько серверов? - →Что даёт пулер в transaction-режиме такого, чего не может
ATTR_PERSISTENT?
Считать надо не соединения, а потолок, который PHP может потребовать.
4 сервера × pm.max_children 50 = 200 воркеров
max_connections = 151
200 > 151 — при достаточной нагрузке PHP гарантированно упрётся. Ничего не деплоили: просто впервые одновременно проснулось больше 151 воркера.
ATTR_PERSISTENT превращает это из «пикового» в «постоянное»: сокет остаётся привязан к воркеру и между запросами, поэтому воркер, обслуживший один запрос час назад, всё ещё держит соединение. Отсюда 148 Sleep при 3 Query.
<?php
// ❌ каждый воркер навсегда занимает слот в max_connections
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => true,
]);
// ✅ соединение живёт ровно один запрос
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
Порядок действий:
max_connections с запасом на репликацию, миграции и админские сессии.
и только они меняют спрос.
много: он мультиплексирует сотни коротких клиентских соединений на десяток серверных. Это единственный способ иметь много воркеров и мало соединений.
памяти база потратит на каждый дополнительный поток.
- Свести арифметику.
серверы × pm.max_childrenдолжно быть меньше - Снять
ATTR_PERSISTENTили понизитьpm.max_children— это два рычага, - Поставить пулер (
pgbouncer, ProxySQL), если воркеров действительно нужно - Поднимать
max_connections— последний шаг, и только когда посчитано, сколько