Ворклог по задаче "Разобраться почему на хеппибеби mysql много памяти сжирает"
Диагностика аномального потребления памяти 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-окружении.