MAATRIX / Блог / OOM killer убил базу: восстановление и профилактика

OOM killer убил базу: восстановление и профилактика

MAATRIX

Сайт лежит, в логах приложения — сухое "connection refused" к базе данных, и никакой другой зацепки. Первое, что приходит в голову — "база упала сама", но у баз данных не бывает беспричинных смертей. Почти всегда за этим стоит одна и та же история: ядро Linux решило, что памяти не хватает всем, и выбрало жертву — процесс СУБД. Разберём, как это диагностировать, что делать прямо сейчас и как настроить сервер, чтобы это не повторялось.

Первый симптом: "connection refused", а не ошибка базы

Классическая ситуация: мониторинг сайта показывает 502 или 500, в логах Nginx — connect() failed (111: Connection refused) while connecting to upstream, в логах бэкенда — исключение вида could not connect to server: Connection refused или Lost connection to MySQL server during query. Никакой ошибки самой СУБД в её собственных логах может не быть вовсе — просто последняя запись обрывается, а дальше тишина.

Первая проверка — жив ли процесс:

systemctl status postgresql
# или
systemctl status mysql
ps aux | grep -E "postgres|mysqld"

Если systemd показывает inactive (dead) или failed, а в выводе ps процесса нет вообще (не "завис", а именно отсутствует) — это уже сильный намёк не на баг в самой базе, а на то, что процесс кто-то убил снаружи. Приложения не видят разницы между "база упала из-за бага" и "процесс базы уничтожен ядром" — они получают один и тот же connection refused. Разница видна только в системных логах, а не в логах приложения — и это первая ловушка: если проверять только логи бэкенда, легко потратить час на поиск несуществующего бага в коде.

Как найти реального виновника: dmesg и журнал ядра

Прежде чем что-либо чинить, зафиксируйте причину — иначе рестарт базы просто отложит следующий инцидент на несколько часов или дней. Ключевая команда:

dmesg -T | grep -i "killed process"

Если сервер убивал процесс из-за нехватки памяти, здесь будет запись примерно такого вида:

[Wed Aug 26 03:14:07 2026] Out of memory: Killed process 18422 (postgres) total-vm:4211000kB, anon-rss:3102344kB, file-rss:0kB, shmem-rss:1024kB, UID:112 pgtables:6832kB oom_score_adj:0

Здесь сразу видно PID убитого процесса, его имя и сколько памяти он занимал на момент убийства (anon-rss — резидентная память процесса). Если dmesg уже прокрутился (кольцевой буфер ядра ограничен по размеру и легко перезаписывается на активном сервере), ищите то же самое в постоянном журнале:

journalctl -k --since "2 hours ago" | grep -i -E "oom|killed process"
grep -i "oom" /var/log/syslog       # Debian/Ubuntu
grep -i "oom" /var/log/messages     # RHEL/AlmaLinux

Помимо самой записи об убийстве, в тех же логах непосредственно перед ней обычно идёт длинный дамп oom-kill: со списком всех процессов и их oom_score — это тот "рейтинг кандидатов на убийство", по которому ядро выбирало жертву. Чем выше oom_score у процесса, тем вероятнее, что убьют именно его; крупные резиденты памяти без защиты получают его первыми — и СУБД, которая по своей природе держит в памяти большой буфер, тут частый кандидат просто по размеру, а не потому что она "виновата".

Если в dmesg совсем ничего похожего нет, а процесс всё равно исчез — стоит проверить ещё и journalctl -u имя-сервиса на предмет ручного SIGKILL (например, от watchdog-скрипта или systemd OOMPolicy), но в подавляющем большинстве похожих инцидентов виновник — именно ядерный OOM killer.

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

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

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

Почему память вообще кончилась

Найти запись об убийстве — это диагноз "что произошло", а не "почему". Дальше нужно разобраться в причине, иначе профилактика будет наугад. Обычно это одна из трёх ситуаций (или их комбинация):

1. Другой процесс на том же сервере съел память. Частый случай на серверах, где база живёт рядом с приложением, воркерами очередей, скриптами бэкапа или закешированной сборкой. Проверьте, кто занимал память на момент инцидента:

free -h
ps aux --sort=-%mem | head -20

Если время инцидента известно из dmesg -T, полезно посмотреть, не совпал ли он с cron-заданием (бэкап, архивация логов, полнотекстовая переиндексация) — grep CRON /var/log/syslog за то же время.

2. Сама база настроена с буферами, которые не помещаются в доступную RAM. Для PostgreSQL это в первую очередь shared_buffers, work_mem (умножается на число параллельных соединений и операций сортировки в запросе — легко недооценить итоговую сумму) и maintenance_work_mem. Для MySQL/MariaDB — innodb_buffer_pool_size плюс память на соединение (thread_stack, буферы сортировки) умноженная на max_connections. Если innodb_buffer_pool_size выставлен, скажем, на 80% от RAM сервера, а рядом на той же машине крутится ещё что-то — переполнение почти гарантировано при любом всплеске нагрузки.

# PostgreSQL: текущие значения
sudo -u postgres psql -c "SHOW shared_buffers;"
sudo -u postgres psql -c "SHOW work_mem;"
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity;"

# MySQL/MariaDB
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
mysql -e "SHOW VARIABLES LIKE 'max_connections';"

3. Несколько тяжёлых процессов совпали по времени. Ночной бэкап плюс ETL-джоба плюс всплеск трафика — по отдельности каждый укладывается в память, вместе — нет. Это самая коварная причина, потому что при следующем расследовании через dmesg вы увидите ту же картину ("память кончилась"), но при обычной нагрузке всё будет работать стабильно, и проблема будет казаться "случайной" — если не сопоставить время инцидента с расписанием фоновых задач.

Помогает посмотреть общую картину использования памяти сервером за период вокруг инцидента, если стоит мониторинг (см. раздел про мониторинг ниже) — иначе после рестарта причина останется на уровне догадки.

Немедленное восстановление

Пока идёт расследование, сервис должен подняться. Порядок действий:

sudo systemctl start postgresql
# или
sudo systemctl start mysql

Если сервис не стартует сразу — смотрите в его собственный лог (не в dmesg, а в лог самой СУБД), там обычно ясно видно, на чём споткнулся старт:

tail -100 /var/log/postgresql/postgresql-*.log
journalctl -u mysql -n 100 --no-pager

Убитый процесс — это SIGKILL, без шанса на штатное завершение транзакций и запись контрольной точки. Поэтому после старта критично не считать, что "раз поднялось — значит всё в порядке", а проверить целостность данных:

PostgreSQL сам проигрывает WAL при старте после нештатного завершения (crash recovery) — это видно в логе как database system was not properly shut down; automatic recovery in progress. После того как процесс поднялся и это сообщение отработало, стоит дополнительно прогнать проверку на битые страницы, если есть подозрение на повреждение:

sudo -u postgres psql -d ваша_база -c "SELECT datname FROM pg_stat_database;"
sudo -u postgres pg_dump ваша_база > /tmp/integrity_check.sql

Успешный pg_dump всей базы — неплохой практический тест целостности: если он прошёл без ошибок вида invalid page или checksum mismatch, данные читаемы. Если такие ошибки есть — потребуется восстановление из бэкапа для затронутых объектов, а не попытка "почистить руками".

MySQL/MariaDB с InnoDB тоже делает crash recovery автоматически при старте (InnoDB: Starting crash recovery в логе), но дополнительно стоит явно проверить таблицы:

mysqlcheck -u root -p --all-databases --check

Если mysqlcheck находит таблицы со статусом, отличным от OK, — по каждой такой смотрите, применим ли REPAIR TABLE, или проще и надёжнее восстановить конкретную таблицу из последнего бэкапа.

Отдельно проверьте свежесть данных: приложение, скорее всего, теряло последние незакоммиченные транзакции за секунды до убийства процесса — это нормально и ожидаемо, важно убедиться, что бизнес-логика приложения корректно обработала обрыв (не осталось "подвисших" наполовину записанных сущностей на стороне приложения). Если сомневаетесь в целостности глубже, чем может проверить pg_dump/mysqlcheck, процедура восстановления из свежего бэкапа расписана в статье про восстановление базы данных из бэкапа.

Профилактика: разумные лимиты памяти для СУБД

"Просто добавить RAM" снимает симптом, но не чинит архитектурную проблему: без лимитов один процесс всегда может вырасти до размера, при котором ему становится тесно, сколько бы памяти вы ни докупили. Более надёжный путь — явно ограничить, сколько памяти может занять СУБД, и настроить, что произойдёт при подходе к пределу.

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

# PostgreSQL: postgresql.conf
shared_buffers = 2GB          # подставьте своё значение под реальный объём RAM
work_mem = 32MB                # умножается на число параллельных сортировок/хэшей!
maintenance_work_mem = 256MB

# MySQL: my.cnf / mysqld.cnf
[mysqld]
innodb_buffer_pool_size = 4G
max_connections = 150          # каждое соединение съедает свою долю памяти сверху буфера

Во-вторых, ограничьте память самого процесса на уровне systemd, чтобы при аномальном росте (например, из-за утечки или неудачного запроса) процесс упирался в собственный лимит и получал управляемую ошибку, а не тянул за собой соседей:

sudo systemctl edit postgresql
[Service]
MemoryMax=3G
MemoryHigh=2560M

MemoryHigh — мягкий порог: при его превышении ядро начинает throttling и активнее вытесняет память процесса, но не убивает его. MemoryMax — жёсткий потолок: превышение приводит к OOM-убийству, но уже локально, в рамках cgroup этого сервиса, а не решением общесистемного OOM killer'а, которое может с равной вероятностью выбрать что угодно другое на сервере. Разница принципиальная: вы сами определяете, что "жертва" — это конкретный, заранее выбранный процесс, а не результат непредсказуемого выбора ядра.

Также стоит явно защитить сам процесс СУБД от общесистемного OOM killer'а, если критичнее потерять что-то другое:

# понизить (сделать более отрицательным) "желание" ядра убивать этот процесс
echo -500 > /proc/$(pgrep -x postgres | head -1)/oom_score_adj

Обратный приём — наоборот, повысить oom_score_adj для менее критичных фоновых задач (например, разовых batch-скриптов), чтобы при нехватке памяти ядро выбирало сначала их, а не базу.

cgroups-изоляция: чтобы один процесс не топил остальные

Лимиты выше уже используют cgroups v2 через systemd — это не отдельная экзотика, а штатный механизм современных дистрибутивов. Смысл в том, чтобы явно разделить память сервера между сервисами, а не полагаться на то, что все процессы "договорятся" сами.

Проверить, что cgroups v2 активны и посмотреть текущее потребление по юнитам:

systemctl status | grep -i cgroup
systemd-cgtop

systemd-cgtop в реальном времени показывает потребление памяти и CPU по каждому systemd-юниту — если один сервис регулярно занимает непропорционально много, это видно сразу, без разбора логов постфактум.

Для сервисов, которые запущены не как systemd-юнит, а в Docker, тот же принцип реализуется через лимиты контейнера:

docker run -d --name db --memory=4g --memory-swap=4g postgres:16

--memory-swap, равный --memory, отключает своп для контейнера (значение выше — разрешает своп сверх лимита памяти). Подробнее про то, как это работает под капотом и почему грань между "мягким" и "жёстким" лимитом важна, разобрано в статьях про лимиты CPU и памяти в Docker и про то, как cgroups в принципе ограничивают контейнер.

Смысл изоляции — не "сделать так, чтобы OOM никогда не случался" (это невозможно в принципе при ограниченной памяти), а сделать так, чтобы при нехватке памяти пострадал предсказуемый, наименее критичный процесс, а не тот, который выберет по своим внутренним эвристикам ядро.

Мониторинг памяти с алертом заранее

Лимиты и правильные буферы снижают частоту инцидентов, но по-настоящему профилактика — это узнать о проблеме до того, как ядро начнёт убивать процессы, а не постфактум через dmesg.

Минимальный вариант без сторонних систем — регулярная проверка свободной памяти через cron с алертом, например в Telegram или на почту, при пересечении порога. Более устойчивый вариант — метрики, которые накапливают историю и позволяют увидеть тренд (память кончается не мгновенно, а постепенно, и по графику это обычно видно за часы или дни до критической точки):

# node_exporter уже отдаёт node_memory_MemAvailable_bytes,
# правило алерта в Prometheus:
- alert: LowAvailableMemory
  expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.15
  for: 5m
  labels:
    severity: warning

Порог в 15% — тоже ориентир, а не универсальная константа: для сервера с равномерной нагрузкой можно смотреть на более узкий порог, а для сервера со скачками (batch-задачи, редкие тяжёлые отчёты) разумнее ставить порог выше и смотреть не только на текущее значение, но и на скорость его падения.

Отдельно стоит мониторить не только общую память, но и специфичные для СУБД метрики — число активных соединений, размер временных файлов на диске (если PostgreSQL начал сбрасывать сортировки на диск из-за нехватки work_mem, это заметно раньше, чем свободная память дойдёт до нуля). Как собрать такие метрики через Grafana конкретно для баз данных, подробно разобрано в статье про мониторинг баз данных через Grafana.

Ещё один практичный инструмент — earlyoom или встроенный в новые ядра systemd-oomd: они убивают процессы-кандидаты раньше и мягче, чем ядерный OOM killer, ориентируясь не только на объём памяти, но и на давление (PSI — pressure stall information), что часто даёт более разумный выбор жертвы, чем классический механизм.

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

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

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

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

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

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

Как понять, что базу убил именно OOM killer, а не сама СУБД упала из-за бага?

Смотрите dmesg -T | grep -i "killed process" — если там есть запись с PID и именем процесса вашей СУБД примерно на момент падения, это ядро. Если такой записи нет, а процесс всё равно исчез без следа в своих логах — ищите ручной kill/SIGKILL от других скриптов или сработавший OOMPolicy в самом systemd-юните.

Можно ли настроить, чтобы OOM killer никогда не трогал процесс базы данных?

Полностью защитить процесс через oom_score_adj = -1000 можно, но это не убирает проблему, а просто перекладывает выбор жертвы на кого-то другого — при исчерпании памяти система всё равно должна кого-то убить или зависнуть. Разумнее ограничить саму базу через MemoryMax в systemd или лимиты контейнера, чтобы она не выходила за разумные рамки, а не пытаться сделать её неубиваемой.

Своп поможет избежать OOM killer?

Немного отодвинет момент, но не решит проблему в принципе, а активный своп резко замедлит работу базы (диск на порядки медленнее RAM для случайного доступа), что для СУБД под нагрузкой само по себе станет отдельным инцидентом. Разумнее использовать умеренный своп как запас на короткие пики, но не как замену правильно посчитанным буферам.

После восстановления база работает, нужно ли что-то ещё проверять?

Да — прогоните pg_dump целиком (PostgreSQL) или mysqlcheck --all-databases --check (MySQL/MariaDB), чтобы убедиться, что нет повреждённых страниц после нештатного завершения. И обязательно найдите первопричину нехватки памяти (раздел выше) — иначе рестарт даст лишь временную передышку.

Стоит ли сразу переезжать на сервер с большим объёмом RAM?

Если после анализа видно, что памяти объективно недостаточно даже при корректно настроенных буферах и без посторонних процессов — да, это оправданно. Но если причина в неверно выставленных shared_buffers/innodb_buffer_pool_size или в конфликте с соседними процессами на той же машине, увеличение RAM без исправления конфигурации лишь отодвинет следующий инцидент на некоторое время.

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

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

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