Ворклог по задаче "Разобраться почему на хеппибеби mysql много памяти сжирает"

23 сент. 2026 г., 03:56:44

Диагностика аномального потребления памяти MySQL 5.7 в Docker и устранение причины

В процессе работы с Docker-окружением была обнаружена нетипичная проблема при старте контейнера mysql:5.7: перед нормальным запуском MySQL контейнер в течение нескольких минут резко увеличивал потребление памяти, доходя примерно до четверти доступной RAM хоста, затем освобождал память и повторял цикл. На машине с ~64 ГБ RAM всплески доходили примерно до 16 ГБ; в среде с ~2 ГБ RAM наблюдалось порядка 500 МБ. После нескольких таких циклов потребление стабилизировалось на нормальном уровне.

Исходная конфигурация MySQL была практически минимальной:

mysql:
  image: mysql:5.7
  environment:
    - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-your_mysql_password}

Отдельных настроек innodb_buffer_pool_size или memory limit не было.

Первичное наблюдение

По графику docker stats было видно характерную «пилу»: процесс быстро набирал гигабайты памяти, доходил до некоторого предела, затем память почти полностью освобождалась, после чего цикл повторялся. При этом в начале старта логов почти не было, что создавало впечатление, будто MySQL просто долго инициализируется без каких-либо сообщений.

После запуска с очищенным datadir удалось увидеть точную последовательность событий. До исправления лог выглядел так:

03:33:05 Entrypoint script for MySQL Server 5.7.44 started
        ... 37 секунд тишины ...
03:33:42 Switching to dedicated user 'mysql'
03:33:42 Entrypoint script for MySQL Server 5.7.44 started
        ... ещё 37 секунд тишины ...
03:34:19 Initializing database files

То есть практически всё аномальное время уходило не на инициализацию InnoDB и не на создание файлов базы, а на действия docker-entrypoint.sh, выполняемые до полноценного запуска сервера.

При этом сам MySQL явно сообщал:

InnoDB: Initializing buffer pool, total size = 128M

Это исключило гипотезу, что огромный расход памяти связан с InnoDB buffer pool. По умолчанию в данной конфигурации он составлял всего 128 МБ.

Причина

Проблема оказалась связана с известным поведением MySQL 5.7 при чрезмерно большом лимите открытых файлов (RLIMIT_NOFILE).

Официальный Docker entrypoint для MySQL перед реальным запуском сервера делает служебные вызовы mysqld, в частности в режиме вида:

mysqld --verbose --help

Это используется для проверки конфигурации и получения значений вроде datadir, socket и других параметров.

В MySQL 5.7 есть известная проблема: если RLIMIT_NOFILE аномально большой, служебный запуск mysqld --verbose --help может инициировать очень большую временную аллокацию памяти. В Docker такое проявляется особенно заметно, если контейнер наследует огромный nofile от Docker daemon / systemd.

Из-за этого происходила следующая последовательность:

docker-entrypoint.sh
    ↓
служебный запуск mysqld --verbose --help
    ↓
огромная временная аллокация из-за RLIMIT_NOFILE
    ↓
рост RAM и CPU в течение десятков секунд
    ↓
служебный процесс завершается
    ↓
память освобождается
    ↓
entrypoint выполняет следующий аналогичный вызов
    ↓
повторный всплеск

Это объяснило сразу несколько наблюдений:

  • большие всплески памяти происходили до нормального запуска MySQL;
  • память после каждого всплеска возвращалась системе;
  • было несколько одинаковых по длительности циклов;
  • в этот момент почти не было обычных серверных логов;
  • после завершения bootstrap-процедуры MySQL работал с нормальным потреблением памяти.

Решение

В Docker Compose был явно задан нормальный предел количества открытых файлов:

mysql:
  image: mysql:5.7
  environment:
    - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-your_mysql_password}

  ulimits:
    nofile:
      soft: 65536
      hard: 65536

Важно: этот параметр не является ограничением памяти контейнера. Он ограничивает только число файловых дескрипторов, которые процесс может одновременно открыть. В дескрипторы входят файлы, сокеты и соединения.

То есть nofile: 65536 не означает 64 МБ, 64 ГБ или какой-либо иной объём памяти и никак напрямую не задаёт memory limit контейнера.

Значение 65536 выбрано как нормальный и достаточно большой предел для обычного MySQL в веб-проекте. Оно оставляет большой запас по количеству файлов/сокетов, но не позволяет MySQL 5.7 попасть в проблемный сценарий с огромным RLIMIT_NOFILE.

Проверка результата на существующей базе

После добавления ulimits.nofile повторный запуск существующего datadir изменился радикально:

03:38:54 Entrypoint started
03:38:54 Switching to dedicated user 'mysql'
03:38:54 Entrypoint started
03:38:54 mysqld starting
03:38:54 mysqld: ready for connections

Обе прежние паузы примерно по 37 секунд исчезли полностью. Сервер стал готов к подключениям практически мгновенно.

Проверка на полностью чистом datadir

Для корректной проверки первичной инициализации данные MySQL были реально очищены с учётом того, что использовались bind mounts:

volumes:
  - ./mysql/data:/var/lib/mysql
  - ./mysql/conf.d:/etc/mysql/conf.d

После этого был выполнен полный cold start. Новый лог:

03:41:42 Entrypoint started
03:41:42 Switching to dedicated user 'mysql'
03:41:42 Entrypoint started
03:41:43 Initializing database files
03:41:45 Database files initialized
03:41:45 Starting temporary server
03:41:49 MySQL init process done. Ready for start up
03:41:49 mysqld: ready for connections

Таким образом, полный первый запуск с пустой базой занял примерно 7 секунд вместо нескольких минут.

При этом InnoDB по-прежнему использовал штатный буфер:

InnoDB: Initializing buffer pool, total size = 128M

То есть исправление не уменьшало рабочую память MySQL искусственным лимитом — оно устранило именно ошибочную временную аллокацию на bootstrap-этапе.

Фактическое потребление памяти после исправления

После нормального запуска docker stats показывал:

173.4MiB / 61.5GiB

То есть контейнер реально использовал около 173 МБ RAM. Число 61.5GiB справа — это доступная контейнеру память хоста при отсутствии отдельного memory limit, а не резерв MySQL.

ulimits.nofile на это значение напрямую не влияет. Для ограничения RAM контейнера потребовался бы отдельный Docker memory limit (mem_limit, resources limits и т. п.), но в рамках данной проблемы это не требовалось.

Итог

Причина долгого старта и огромных всплесков памяти была не в реальном рабочем потреблении MySQL, не в InnoDB buffer pool и не в объёме данных. Проблему вызывал MySQL 5.7 на служебных запусках из Docker entrypoint при чрезмерно большом RLIMIT_NOFILE.

Исправление:

ulimits:
  nofile:
    soft: 65536
    hard: 65536

Результат:

  • исчезли повторные гигабайтные всплески RAM;
  • исчезли паузы по ~37 секунд на служебных вызовах entrypoint;
  • cold start пустой базы сократился примерно до 7 секунд;
  • рабочее потребление памяти стабилизировалось примерно на 170–180 МБ;
  • лимит памяти контейнера при этом не вводился и не изменялся;
  • nofile=65536 оставлен как постоянный workaround для mysql:5.7 в текущем Docker-окружении.