OOM-killer убивает MySQL на BitrixVM: 23 GB RAM, 10 GB innodb и Apache, которые не помещаются
Разбор инцидента на интернет-магазине под BitrixVM: ночью система убивает mysqld из-за нехватки памяти. Виноваты три вещи разом — слишком жирный innodb_buffer_pool под маленькую базу, утечка mod_php в Apache prefork и крошечный swap.
Симптом
Клиент пишет утром: «Ночью сайт лагал, в 00:54 пропадал секунд на 15». Алерт в Telegram подтверждает: mysql_uptime метрика обнулилась в 00:54 — то есть MySQL рестартанулся. Сам себя? Под нагрузкой? Что-то непонятное.
Разведка
Лезем в journalctl за нужный момент:
May 14 00:54:05 server1 kernel: httpd invoked oom-killer: gfp_mask=0x201da
May 14 00:54:05 server1 kernel: Out of memory: Kill process 43812 (mysqld) score 145
May 14 00:54:05 server1 kernel: Killed process 43812 (mysqld), UID 27,
total-vm:16238064kB, anon-rss:3633208kB
May 14 00:54:05 server1 systemd[1]: mysqld.service: main process exited, code=killed, status=9/KILL
May 14 00:54:18 server1 systemd[1]: Started MySQL Server.
Картина в трёх строках:
- Apache (
httpd) попросил у системы памяти, которой не было. - Ядро запустило OOM-killer и убило
mysqld(у него был самый высокийoom_score). systemdчерез 13 секунд перезапустил mysqld автоматически.
Клиент в этот момент видел: 13 секунд ошибок 5xx, потом ещё 2–3 часа «штормило» — это MySQL прогревал innodb_buffer_pool после холодного старта.
Самое неприятное: за последние 30 дней OOM срабатывал 12 раз. Не разовое везение — системная проблема.
Откуда дефицит памяти
Считаем по факту, что было запущено в момент срабатывания:
80 Apache prefork воркеров × ~200 MB private RSS ≈ 16 GB
mysqld (innodb_buffer_pool 10G + остальное) ≈ 10 GB
PHP opcache, система, прочее ≈ 2 GB
────────────────────────────────────────────────────────
Итого ≈ 28 GB
Имеется RAM = 23 GB
Имеется swap = 0.5 GB (и весь занят)
Дефицит около 5–6 GB. Когда последний Apache-воркер вырастал на критические сотни мегабайт — ядро не могло выделить страницу, шёл OOM-killer, выбирал самый «жирный» процесс, и это был mysqld.
Корни проблемы — их три
1. innodb_buffer_pool_size = 10G под базу 2.3 GB
Проверяем, сколько реально занято данными:
SELECT
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_data') AS pages_data,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME='Innodb_buffer_pool_pages_free') AS pages_free;
Результат:
pages_data = 67 265 → ~1.03 GB реально занято
pages_free = 584 144 → ~8.9 GB просто пустует
Из 10 GB пула реально используется ~1 GB. Остальные 9 GB — мёртвый резерв, который ничему не помогает (Innodb_buffer_pool_reads за 16 часов uptime: 51 тыс. промахов на 52 млрд запросов — это уже 99.9999% hit ratio, кэш и так идеален). Эти 9 GB напрямую вкладываются в OOM.
2. Apache MaxRequestsPerChild = 5000
Стандартное значение в BitrixVM. Звучит безобидно, но при 80 воркерах и десятках тысяч запросов в день каждый воркер живёт ~12 часов. За это время mod_php накапливает утечку — медленную, но устойчивую.
ТОП воркеров на момент диагностики:
pid=106155 rss=1020 MB age=12ч21м
pid=106167 rss= 908 MB age=12ч20м
pid=105811 rss= 759 MB age=12ч29м
... (топ-10 от 600 MB до 1 GB, всем 12+ часов)
То есть воркеры не сидят на 50–100 MB как при старте — они раздуваются до гигабайта за полусутки. И самая жадная пачка из них накладывается на «случайный» нерабочий час, когда вместе с этим прокатывается ночной cron-агент Bitrix — и OOM.
3. Swap 511 MB (и весь забит)
На 23 GB RAM — это даже не страховка. При нехватке памяти системе деваться некуда: либо OOM, либо OOM. Нормальный размер swap’а на таком сервере — 2–4 GB: не для постоянной работы, а как буфер для пиков.
Что делаем
В порядке «сильно/слабо вмешиваемся в работающую систему»:
1. innodb_buffer_pool_size 10 GB → 4 GB, instances 8 → 4.
Освобождает 6 GB RAM в моменте, без потерь по производительности (вся база 2.3 GB всё равно умещается в 4 GB кэша с гигабайтом запаса). На BitrixVM правится через custom drop-in:
# /etc/mysql/conf.d/z_bx_custom.cnf
[mysqld]
innodb_buffer_pool_size = 4G
innodb_buffer_pool_instances = 4
Требуется рестарт mysqld (instances динамически не меняется). 10–30 секунд простоя, после рестарта на холодных кэшах ~5 минут «небыстрых» запросов — это нормально.
2. MaxRequestsPerChild 5000 → 300.
Воркеры пересоздаются в 16 раз чаще и не успевают накопить утечку. На BitrixVM — через z_bx_custom.conf:
# /etc/httpd/bx/custom/z_bx_custom.conf
<IfModule prefork.c>
MaxRequestsPerChild 300
</IfModule>
Применяется через apachectl graceful — без даунтайма.
3. Swap 512 MB → 4 GB. Создаём swapfile:
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
# в конце — убедиться что старый 512M swap отключён или объединён
Без даунтайма и рестартов.
Эффект сразу после применения
| До | После | |
|---|---|---|
| RAM used | ~7.6 GB | 1.9 GB |
| RAM available | 12 GB | 19 GB |
| Swap used | 511M / 511M (full) | 367M / 511M → дальше 0/4G |
wsd.by response | 250–800 ms | 120–270 ms |
В первые часы освободилось ~6 GB RAM. Дальше, когда воркеры стали пересоздаваться чаще, профиль стабилизировался — больше OOM на этом проекте мы не видели.
Что взять домой, если у вас Bitrix
- Проверьте
pages_dataв InnoDB черезperformance_schema.global_status. Если меньше 30% от пула — вы напрасно держите RAM. Хорошее правило: пул должен быть в 1.5–2 раза больше реально занятых страниц. - На BitrixVM по умолчанию
MaxRequestsPerChild = 5000— для сайтов с серьёзным трафиком это слишком много. 300–500 безопаснее и почти не нагружает CPU дополнительными форками. - Никогда не оставляйте swap меньше 1 GB даже на серверах с десятками гигабайт RAM. Это страховка от пиковых перерасходов, ничего более.
- OOM-killer один раз в месяц — это уже паттерн. Один раз — может быть случайность. Два — совпадение. Три — точно что-то системное.
Если хотите, чтобы похожее не повторилось у вас — запишитесь на бесплатный аудит. Прогоним 30-минутный диагностический срез по вашему серверу и пришлём отчёт с приоритетами «что чинить».