MAATRIX / Блог / Сколько подключений к MySQL до свопа: связь max_connections и памяти в цифрах

Сколько подключений к MySQL до свопа: связь max_connections и памяти в цифрах

MAATRIX

У MySQL и MariaDB нет процесса на соединение, как у PostgreSQL, — есть поток, но плата за каждое подключение всё равно реальная: стек потока, сессионные буферы, кэши сортировки. Пока соединений мало, это незаметно. Но стоит поднять max_connections «с запасом» и не пересчитать, сколько памяти сервер обязан выделить, если все они одновременно окажутся заняты, — и в момент пиковой нагрузки сервер вместо ускорения уходит в своп. Разберём, из чего складывается цена одного соединения, как считать пиковое потребление и где вместо роста лимита стоит поставить пул.

Из чего состоит память на одно соединение MySQL

MySQL и MariaDB используют модель «поток на соединение» (thread-per-connection): под каждое подключение выделяется отдельный поток ОС, а не процесс, как у PostgreSQL. Это дешевле, чем fork процесса, но каждый поток всё равно тянет за собой набор сессионных буферов, которые выделяются либо сразу при подключении, либо по мере необходимости в рамках сессии.

Основные компоненты памяти одного соединения:

  • Thread stack — управляется параметром thread_stack, по умолчанию обычно в районе 256 КБ–1 МБ в зависимости от версии и сборки. Это память под стек выполнения потока, выделяется на каждое соединение.
  • Сессионные буферы, которые выделяются per-connection и не входят в общий буферный пул InnoDB:
  • sort_buffer_size — под сортировку в рамках запроса;
  • join_buffer_size — под join без индекса;
  • read_buffer_size и read_rnd_buffer_size — под последовательное и случайное чтение таблиц;
  • net_buffer_length / max_allowed_packet-связанные буферы сети — под приём и отправку пакетов протокола.
  • Кэши подготовленных запросов и курсоров, если приложение активно использует prepared statements — их состояние тоже держится в сессии, а не в общем пуле.

Важный нюанс: часть этих буферов (sort_buffer_size, join_buffer_size, буферы чтения) выделяется не при открытии соединения, а по требованию — когда запрос реально делает сортировку, join без индекса или сканирование таблицы. У простого соединения без активных запросов эти буферы могут быть не выделены вовсе. Значит, реальный пик — это не «холостое» соединение, а соединение, выполняющее тяжёлый запрос. Проверить текущее потребление памяти по потокам можно через performance_schema, если он включён:

SELECT thread_id, SUBSTRING_INDEX(user, '@', 1) AS user,
       current_memory / 1024 / 1024 AS current_mb
FROM performance_schema.memory_summary_by_thread_by_event_name
JOIN performance_schema.threads USING (thread_id)
WHERE current_memory > 0
ORDER BY current_memory DESC
LIMIT 20;

Это даёт фактическую картину на вашей схеме и версии сервера — не берите чужие цифры «на соединение» из статей, overhead зависит от версии MySQL/MariaDB, плагинов и профиля запросов конкретного приложения.

Буферный пул InnoDB — отдельная и куда более крупная статья памяти

Буферы соединений — не главный потребитель памяти на типичном сервере MySQL. Основную долю обычно забирает innodb_buffer_pool_size — общий кэш страниц данных и индексов InnoDB, один на весь инстанс, а не на соединение. По распространённой рекомендации (не жёсткому правилу — проверяйте под свою нагрузку) на выделенном под БД сервере под буферный пул отводят порядка 50–70% доступной RAM, оставляя остальное под ОС, page cache, соединения и служебные процессы. Почему буферный пул вообще так важен для производительности — отдельная тема, разобранная в статье Буферный пул базы данных: почему он важнее скорости вашего диска.

Ключевая ошибка при расчёте памяти — рассматривать буферный пул и лимит соединений изолированно друг от друга, как будто это две независимые настройки. На деле это одна и та же RAM, поделённая между постоянным крупным потребителем (буферный пул) и переменным, зависящим от нагрузки (соединения). Если увеличить innodb_buffer_pool_size до 80% RAM и одновременно поднять max_connections до нескольких тысяч без пересчёта, суммарный теоретический пик легко превысит физическую память сервера — даже если при обычной нагрузке всё работает штатно.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Формула пикового потребления памяти

Теоретический пик памяти MySQL/MariaDB можно грубо оценить так:

RAM_worst_case ≈ innodb_buffer_pool_size
               + key_buffer_size (если есть MyISAM-таблицы)
               + max_connections × (thread_stack + сессионные буферы на активное соединение)
               + overhead ОС, page cache, служебные процессы

Здесь принципиален смысл max_connections × буферы_на_соединение — это не среднее, а худший случай: все допустимые соединения одновременно активны и выполняют запросы с максимальными сессионными буферами. На практике так бывает редко, но именно ради этого редкого случая — пиковой нагрузки, флеш-моба трафика, массового переподключения клиентов после сбоя — и существует лимит max_connections: он должен быть безопасным в худшем случае, а не только в среднем.

Прикидка на конкретном примере. Сервер с 8 ГБ RAM, innodb_buffer_pool_size = 5 ГБ, thread_stack по умолчанию, sort_buffer_size = 2 МБ, join_buffer_size = 2 МБ. Условно принимаем overhead активного соединения (стек + типичный набор буферов при не самом простом запросе) около 3–5 МБ — это ориентир для прикидки, не измеренная константа, у вас реальная цифра может отличаться в разы. При max_connections = 2000 пиковая добавка от соединений — уже 6–10 ГБ сверх буферного пула, что само по себе больше, чем RAM на сервере. Такая конфигурация не упадёт сразу — большинство соединений в реальности простаивают или выполняют лёгкие запросы, — но у неё нет запаса на случай, когда активность резко вырастет, и именно тогда сервер начинает вытеснять память в своп.

Почему своп при пиковой нагрузке — это тихая деградация, а не падение

Своп не роняет MySQL сразу — сервер продолжает отвечать, просто с задержками, растущими нелинейно. Как только ядро Linux начинает вытеснять страницы памяти сервера (в худшем случае — и страницы буферного пула InnoDB) на диск, каждое обращение к вытесненным данным превращается из операции с RAM в операцию с диском — на порядки медленнее, даже на NVMe. Запросы, которые раньше выполнялись за миллисекунды, занимают секунды, соединения не успевают освобождаться, накапливается очередь новых подключений — и «сервер немного притормозил» превращается в каскадную деградацию именно под пиковой нагрузкой, когда сервер должен был выдержать больше всего.

Проверить, движется ли сервер в эту сторону, можно по нескольким сигналам одновременно:

# активность свопа: si/so — страницы, уходящие в своп и обратно
vmstat 1

# сколько памяти реально свободно и сколько занято под кэш/буферы
free -m

# число текущих подключений и их состояние
mysql -e "SHOW STATUS LIKE 'Threads_connected';"
mysql -e "SHOW STATUS LIKE 'Threads_running';"
mysql -e "SHOW STATUS LIKE 'Max_used_connections';"

Threads_connected показывает, сколько соединений открыто прямо сейчас, Threads_running — сколько из них реально выполняют запрос, а не простаивают, Max_used_connections — исторический пик с момента последнего рестарта. Если Max_used_connections регулярно приближается к max_connections, а vmstat в те же моменты показывает ненулевые si/so — это прямая связь между ростом соединений и уходом в своп, а не совпадение. Стоит помнить и о внешних ограничителях: даже правильно посчитанный max_connections не спасёт, если сам процесс mysqld ограничен по памяти на уровне systemd-юнита или cgroup меньше, чем реально нужно под пик, — тогда сервер уйдёт в своп или упадёт по OOM ещё до исчерпания физической RAM сервера.

Как правильно выставить max_connections под доступную память

Правильный порядок — не «подобрать число побольше», а посчитать от доступной памяти в обратную сторону:

  1. Отведите память под буферный пул и ОС. Зафиксируйте innodb_buffer_pool_size (обычно 50–70% RAM на выделенном под БД сервере) и оставьте резерв под page cache, служебные процессы, мониторинг, бэкапы — на практике это ещё 15–25% RAM, которые не должны обещаться соединениям.
  2. Посчитайте, сколько осталось под соединения. Остаток RAM (после буферного пула и резерва ОС) делённый на реалистичный overhead активного соединения на вашей нагрузке — это верхняя граница безопасного max_connections. Overhead на соединение измеряйте у себя (через performance_schema, как показано выше), а не берите готовую цифру из статьи.
  3. Заложите запас, а не работайте впритык. Пиковый расчёт — это худший случай, но неучтённые процессы (репликация, бэкап через mysqldump или xtrabackup, административные подключения) тоже занимают слоты и память. Разумно оставлять 10–20% запаса от расчётного максимума.
  4. Проверьте лимиты ОС. max_connections в MySQL бессмысленно поднимать выше, чем позволяет ulimit -n (открытые файловые дескрипторы) для пользователя, от которого работает mysqld, и системный лимит потоков. Несовпадение лимитов — частая причина, что сервер либо не стартует с новым max_connections, либо упирается в лимит ОС раньше, чем в собственный.

Пример расчёта для сервера на 16 ГБ RAM: innodb_buffer_pool_size = 9 ГБ, резерв ОС и page cache — 3 ГБ, остаётся 4 ГБ под соединения. При ориентировочном overhead активного соединения 4–6 МБ это даёт диапазон примерно 650–1000 соединений теоретического пика — само число условное, важен метод, а не конкретная цифра. С запасом 15% разумный max_connections в этом примере — около 550–850, а не «2000, чтобы точно хватило». Если реальному приложению нужно держать больше одновременных клиентов, чем позволяет такой расчёт, — это сигнал не увеличивать лимит дальше, а добавить пулинг, о котором ниже. Поведение сервера при упоре в текущий лимит разобрано в статье MySQL: ошибка Too many connections — причины и решение.

Connection pooling вместо роста max_connections

Если приложению регулярно не хватает соединений при разумном max_connections, правильный ответ — не поднимать лимит дальше (это просто отодвигает границу свопа, а не убирает проблему), а вставить пул между приложением и сервером БД. Смысл пулинга — не в том, чтобы обмануть память, а в том, чтобы разорвать прямую связь «один клиент = одно постоянное соединение к mysqld» и раздавать ограниченный набор реальных backend-соединений по очереди.

Два уровня, на которых это делается, и они не взаимоисключающие:

  • Пул на стороне приложения — встроенный в драйвер или ORM (например, пулы соединений в популярных фреймворках). Работает в рамках одного процесса приложения, ограничивает число одновременных соединений именно от этого инстанса. Если приложение развёрнуто в нескольких процессах или подах, каждый держит свой пул, и суммарное число реальных соединений к MySQL — это пул × число инстансов приложения, что легко упустить из виду при масштабировании.
  • ProxySQL — отдельный сервис-прокси между всеми клиентами и MySQL/MariaDB, который держит собственный пул backend-соединений к серверу БД независимо от того, сколько клиентов подключено к нему самому. В отличие от пула приложения, ProxySQL видит суммарную картину со всех инстансов приложения сразу и может держать фиксированное, предсказуемое число реальных соединений к серверу вне зависимости от того, сколько процессов приложения масштабировалось наверху.

Базовые параметры, которые определяют экономию соединений в ProxySQL, задаются в таблице mysql_servers и через переменные mysql-max_connections (лимит на бэкенд-сервер со стороны ProxySQL) и mysql-default_max_connections (лимит на пользователя). Логика та же, что и в пулерах для PostgreSQL: клиентских соединений к прокси может быть много и они дешёвые, а реальных соединений к базе — заметно меньше и они дорогие. Общий принцип пулинга и когда он оправдан разобран в статье Connection pooling: зачем нужен и как настроить — там же и предостережение: пул не бесплатен, добавляет сетевой хоп и собственное потребление CPU/памяти на прокси, и не решает проблему медленных запросов — только проблему числа одновременных соединений.

Честно про ограничение подхода: пулинг не спасает, если проблема не в количестве соединений, а в тяжести отдельных запросов, которые сами по себе долго держат буферы и не отпускают соединение обратно в пул. Тогда пул быстрее упрётся в свой лимит и начнёт ставить клиентов в очередь — лучше, чем своп на сервере БД, но не замена оптимизации самих запросов.

Мониторинг: как заметить приближение к границе заранее

Расчёт при первой настройке важен, но нагрузка и объём данных меняются, и разовый расчёт не остаётся верным навсегда. Стоит регулярно, а не только при инциденте, смотреть на связку метрик:

МетрикаИсточникНа что указывает рост
Threads_connected / Max_used_connectionsSHOW STATUSПриближение к max_connections, нехватка запаса
si/so в vmstatОСАктивность свопа — RAM уже не хватает под текущий пик
Innodb_buffer_pool_wait_freeSHOW STATUSБуферному пулу не хватает свободных страниц — конкуренция за память внутри InnoDB
Свободная память по free -m без учёта page cacheОСНасколько близко реальное потребление к физическому пределу
Число реальных backend-соединений через ProxySQL (если используется)stats_mysql_connection_pool в ProxySQLДержится ли пул в заданных границах или упирается в свой лимит

Если у вас уже настроен алертинг, разумно поставить пороги не только на «MySQL недоступен», но и на приближение Threads_connected к max_connections (например, 80%) и на появление ненулевого so в vmstat на сервере БД — это даёт время среагировать до того, как начнётся видимая деградация, а не после.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Правда ли, что у MySQL threads дешевле, чем процессы у PostgreSQL, и поэтому можно ставить max_connections намного выше?

Потоки дешевле процессов по overhead на переключение контекста и по базовому footprint памяти на пустое соединение, но при активных запросах разница по сессионным буферам невелика — оба сервера тратят память на сортировки и join-буферы в рамках сессии. Модель потоков снижает цену простаивающего соединения, но не отменяет пиковый расчёт под нагрузкой.

Если сервер пока не уходит в своп, значит max_connections настроен правильно?

Не обязательно — отсутствие свопа сегодня означает лишь, что фактическая активность соединений ниже теоретического пика. Если лимит выставлен без запаса, разовый всплеск активности (переподключение клиентов после сбоя, наплыв трафика) может вскрыть проблему в худший момент.

Что быстрее спасёт от свопа при пиковой нагрузке — снизить max_connections или уменьшить буферный пул?

Смотрите по vmstat и SHOW STATUS, что конкретно исчерпывает память в моменте. Но как быструю меру безопаснее снизить max_connections или добавить пул перед сервером: буферный пул снижать нежелательно — это сразу бьёт по производительности ростом чтения с диска.

Нужен ли ProxySQL на небольшом проекте с одним инстансом приложения?

Не всегда — если пул на стороне приложения уже держит число реальных соединений в расчётных границах, отдельный прокси добавляет сложность без явной пользы. Он оправдан, когда инстансов приложения несколько и нужен общий контроль числа backend-соединений.

Как быстро понять overhead одного соединения без сложных замеров?

Снимите free -m до открытия дополнительных соединений и после открытия известного числа, разница делённая на число соединений даёт грубую, но реальную для вашей схемы и версии цифру — точнее любой цифры из статьи.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →