MAATRIX / Блог / Сервер лёг в три ночи: восстанавливаем картину по логам

Сервер лёг в три ночи: восстанавливаем картину по логам

MAATRIX

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

Что стало известно утром

Первый сигнал — сообщение от системы мониторинга (Uptime Kuma, Zabbix, healthchecks.io или встроенный алертинг панели) о том, что сервис не отвечает. Если у вас настроены алерты в Telegram, сообщение уже лежит в чате с точным временем первого неудачного пробоя — это ваша отправная точка для всего дальнейшего расследования.

Дальше — быстрая проверка руками:

systemctl status myapp.service
curl -I http://localhost:8080/
ss -tlnp | grep 8080

Если сервис в статусе active (running), но не отвечает — это одна история (зависший процесс, исчерпанный пул соединений). Если сервис failed или inactive — совсем другая: значит, он упал сам или его убили. В разбираемом случае systemd показывал именно второе:

● myapp.service - Main application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
     Active: failed (Result: signal) since Tue 2026-08-25 03:14:52 UTC; 6h ago
    Process: 18422 ExecStart=/usr/bin/node /opt/myapp/server.js (code=killed, signal=KILL)

code=killed, signal=KILL — важная деталь. Процесс не упал сам от необработанного исключения (это было бы code=exited, status=1 или трейс в логах приложения) — его убили извне. Кто и почему — предстоит выяснить.

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

Первая версия, которая не подтвердилась

Самое очевидное объяснение ночного падения — сетевая проблема: DDoS, авария у провайдера, проблемы с DNS. Проверяется быстро и первым, потому что если причина внешняя — чинить нечего, нужно просто ждать или переключаться на резерв.

# доступен ли хост вообще снаружи
ping -c 5 1.1.1.1
# резолвится ли домен
dig +short example.com
# не изменился ли маршрут
mtr -rw -c 10 8.8.8.8
# статус провайдера (в консоли или через API, если есть)

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

# частые IP в логах nginx за окно инцидента
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# неудачные попытки входа по SSH
grep "Failed password" /var/log/auth.log | tail -50

Аномального трафика не нашлось — обычная ночная тишина, десяток ботов сканирующих порты, ничего похожего на атаку. Обе первые версии закрыты за 10–15 минут проверки. Это нормально: в разборе инцидента задним числом первые гипотезы почти всегда самые очевидные и почти всегда не подтверждаются — но проверить их быстро и вычеркнуть всё равно необходимо, иначе можно потратить час на догадки вместо фактов. Если тема внешней недоступности вам знакома с другой стороны — сравните с разбором когда сервер недоступен по SSH, там похожая логика исключения версий, но с другим финалом.

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

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

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

Восстанавливаем точную хронологию: journalctl и dmesg

Раз внешних причин нет, дело в самом сервере. Здесь нужен не общий просмотр логов, а точное временное окно — минус 15–20 минут от момента первого фейла мониторинга и до момента, когда вы начали разбираться. Без окна journalctl выдаст экран за экраном лишнего.

journalctl --since "2026-08-25 02:55:00" --until "2026-08-25 03:25:00"

Именно так, а не «журнал за сегодня» — иначе в потоке рутинных cron-задач и системных сообщений реальная причина потеряется. Смотрим сначала ядро отдельно — там чаще всего и находится настоящая причина силового завершения процесса:

dmesg -T | grep -i -E "kill|oom|error|segfault"
journalctl -k --since "2026-08-25 02:55:00" --until "2026-08-25 03:25:00"

-T у dmesg переводит внутренние метки времени ядра в человекочитаемый формат — без этого флага время указано в секундах с момента загрузки, что бесполезно для сопоставления с остальными логами. journalctl -k — то же самое, но через systemd-journal, если dmesg-буфер уже перезаписан (по умолчанию он кольцевой и ограничен по размеру).

В нашем случае в выводе нашлась характерная строка:

Out of memory: Killed process 18422 (node) total-vm:2847312kB, anon-rss:1893440kB, file-rss:0kB, shmem-rss:0kB, UID:1001 pgtables:4128kB oom_score_adj:0

Вот и объяснение сигнала KILL из статуса systemd. Ядро само решило, какой процесс принести в жертву — механику этого решения (oom_score, oom_score_adj) подробно разбирали в статье как ядро выбирает жертву OOM killer, если хотите понять логику выбора процесса, а не только сам факт убийства.

Что показали логи приложения и systemd

OOM killer — это следствие, а не причина. Нужно понять, откуда взялся дефицит памяти: медленная утечка в приложении, разовый всплеск нагрузки, или память просто не была ограничена и один процесс съел всё, что было. Смотрим лог самого приложения за то же окно:

journalctl -u myapp.service --since "2026-08-25 01:00:00" --until "2026-08-25 03:15:00" | less

Расширяем окно на пару часов назад специально — утечка обычно нарастает постепенно, и в логе приложения по мере роста потребления памяти часто видны косвенные признаки: замедление ответов, таймауты к базе, GC-паузы (если это Node.js или JVM), в логе может даже мелькать FATAL ERROR: Reached heap limit до самого убийства процесса ядром. В разбираемом случае в логе за пару часов до инцидента росло число медленных запросов к базе — по логам самой БД видно, что запросы, обычно занимающие миллисекунды, к моменту инцидента шли секундами:

journalctl -u postgresql.service --since "2026-08-25 01:00:00" --until "2026-08-25 03:15:00" | grep -i "duration:"

Дополнительно смотрим, что творилось с памятью системы в целом — если есть исторические данные мониторинга (netdata, node_exporter + Grafana, или хотя бы sar):

sar -r -f /var/log/sysstat/sa25 | head -30

Если истории по памяти нет вообще — это тоже находка, и повод её завести (об этом ниже). Смотрим также, не совпало ли время инцидента с каким-то плановым заданием:

grep CRON /var/log/syslog | grep "02:5\|03:0\|03:1"
crontab -l
cat /etc/cron.d/*

В нашем случае совпадение нашлось: в 02:58 по расписанию запускалась ежедневная джоба формирования отчёта, которая грузила в память весь набор данных за месяц одним запросом вместо постраничной обработки. При обычной дневной нагрузке процесс укладывался в доступную память с запасом, но в ночь инцидента одновременно с джобой ещё и подрос основной процесс приложения (утечка, накопленная за несколько дней аптайма) — и суммарного запаса не хватило.

Настоящая причина: совпадение утечки памяти и ночной джобы

Собираем хронологию воедино:

Время (иллюстративно)Событие
5 дней аптаймаПроцесс приложения постепенно растёт по RSS из-за утечки в обработке WebSocket-соединений
02:58Запускается cron-джоба формирования отчёта, разом выделяет несколько сотен МБ
03:14Суммарное потребление памяти превышает физическую RAM + swap
03:14:52Ядро вызывает OOM killer, убивает процесс с наибольшим oom_score — им оказался основной сервис приложения, а не джоба-виновница
03:15+Мониторинг фиксирует недоступность, алерт уходит в Telegram

Важный и обидный нюанс: OOM killer не всегда убивает виновника. Он убивает процесс с самым высоким расчётным oom_score на момент паники памяти — а это может оказаться совсем не та программа, которая память съела. В данном случае разовая cron-джоба отработала и корректно завершилась, а под раздачу попал основной долгоживущий процесс, потому что суммарно к моменту паники именно он занимал больше памяти (утечка копилась днями, джоба — секунды).

Проверить объём выделенной ранее памяти можно через free, но это уже постфактум — сам момент паники безвозвратно потерян, если только не вёлся сбор метрик:

free -h
cat /proc/meminfo | grep -i swap

В инциденте своп был выставлен в 0 — то есть у ядра не было вообще никакого запаса на случай кратковременного всплеска. Это отдельная и частая грабля: без свопа система не успевает среагировать раньше OOM killer'а, любой временный пик сразу превращается в убийство процесса.

Фикс и защита от повтора

Действий сразу несколько, и они на разных уровнях — от немедленного фикса до архитектурных изменений.

1. Немедленно — добавить своп, чтобы у системы был буфер на случай пиков:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Своп не решает утечку, но даёт время среагировать до жёсткого убийства процесса — параметры подбора размера разбирали отдельно в статье про правильный размер swap для VPS.

2. Ограничить память процесса через systemd, чтобы падал контролируемо и с понятной причиной, а не утаскивал за собой всю систему:

# /etc/systemd/system/myapp.service.d/override.conf
[Service]
MemoryHigh=1200M
MemoryMax=1500M
Restart=on-failure
RestartSec=5

MemoryHigh — мягкий лимит, при превышении cgroup начинает throttlить процесс и активнее сбрасывать кэш; MemoryMax — жёсткий, при превышении процесс убивается самим systemd/cgroup, что фиксируется в journalctl понятной записью OOMKilled в контексте юнита, а не размытой строкой из общего лога ядра. Это меняет диагностику будущих инцидентов принципиально — вы сразу видите, какой юнит и почему, вместо угадывания по PID.

3. Найти и закрыть утечку в приложении. Быстрый способ подтвердить рост RSS во времени, если нет постоянного мониторинга:

watch -n 60 'ps -o pid,rss,cmd -p $(pgrep -f myapp)'

Полноценный поиск утечки — уже отдельная задача профилирования (heap snapshot для Node.js, pprof для Go, memory profiler для Python), выходящая за рамки одного разбора инцидента, но сам факт постоянного роста RSS без выхода на плато — уже достаточный повод смотреть в сторону кода, а не только в сторону лимитов.

4. Перенести тяжёлую cron-джобу и добавить лимиты ей самой. Разово помогает просто развести джобу и обычные часы наименьшей нагрузки, но правильнее — переписать выгрузку отчёта постранично, а не одним запросом в память:

# было
SELECT * FROM events WHERE date >= now() - interval '30 days';
# стало — обработка порциями
SELECT * FROM events WHERE date >= now() - interval '30 days'
  ORDER BY id LIMIT 5000 OFFSET :offset;

Про типичные ошибки самих cron-заданий и как их избежать — отдельный разбор в статье cron-задачи на сервере: частые ошибки и решения.

5. Алерт пораньше, а не по факту падения. Мониторинг доступности (Uptime Kuma, healthchecks.io) сообщает о проблеме, когда она уже случилась. Алерт по проценту использования памяти сообщил бы о ней за часы или дни до критической точки — пока показатель ещё растёт, а не когда сервис уже недоступен. Минимальный порог для алерта на память — 80–85% с оговоркой, что конкретное число зависит от вашего профиля нагрузки и запаса, который вы готовы держать. Общий подход к паре «мониторинг + алерт» разбирали в статье мониторинг и алерт при падении сайта.

Отдельно стоит проверить и логи на диске — если бы в этом инциденте причиной оказался не OOM killer, а забитый диск логами того же долгоживущего процесса, схема расследования была бы почти такой же (journalctl + df -h + du -sh /var/log/* вместо dmesg), а фикс — из статьи про ротацию логов, чтобы не забивался диск. Оба сценария — «съели память» и «съели диск» — вскрываются одним и тем же методом: точное временное окно в journalctl, потом дробление на источники (ядро, приложение, cron), потом сопоставление находок в хронологию.

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

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

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

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

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

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

Почему OOM killer убил не тот процесс, который вызвал всплеск памяти?

Ядро выбирает жертву по расчётному oom_score, который учитывает суммарное потребление памяти на момент паники, а не то, кто последним её запросил. Долгоживущий процесс с накопленной утечкой часто наберёт больший oom_score, чем разовая короткая задача, даже если формально виновата именно она.

Как узнать точное время инцидента, если алерт мониторинга пришёл с опозданием?

Ориентируйтесь на первую строку из journalctl -k, где встречается Out of memory или segfault — это и есть момент, когда что-то пошло не так на уровне ядра, обычно на несколько минут раньше, чем мониторинг успел зафиксировать недоступность и отправить уведомление.

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

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

Можно ли настроить лимиты памяти без перезагрузки сервера?

Да — правки в systemd unit override применяются командой systemctl daemon-reload и вступают в силу при следующем запуске или рестарте конкретного сервиса, полная перезагрузка сервера не требуется.

Что делать, если после разбора причина так и не нашлась?

Включите постоянный сбор метрик (netdata, node_exporter + Prometheus, или хотя бы sar из пакета sysstat) прямо сейчас, до следующего инцидента — без исторических данных по памяти и диску вторая попытка расследования упрётся в ту же стену. Лучше поймать повтор с полной картиной, чем гадать заново вслепую.

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

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

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