Типичная ночная история на VPS на 20–40 гигабайт. Утром сайт лежит, SSH едва открывается, в логах приложения No space left on device. df -h показывает корневой раздел на 100%. Человек уже почистил /tmp, выкинул старые дампы, даже apt clean сделал — свободно 200 мегабайт, через час снова ноль.

Смотрю du -xhd1 /var и почти всегда в топе /var/log. Внутри него не nginx/access.log на первом месте, а /var/log/journal на гигабайты. Хостер предлагает «расширить диск». Расширять можно, но это не лечение: journald по умолчанию считает нормальным занять десятую часть файловой системы. На маленьком корне это уже не «логи», это треть полезного места.

Ниже — как я отличаю «журнал имеет право быть большим» от «он просто не ограничен», чем persistent отличается от volatile на практике, и что смотреть утром, если ночью диск дошёл до нуля. Без советов вида «поставьте плагин» и без удаления файлов наугад.

Где journald вообще пишет, и почему каталог внезапно появляется

У systemd-journald два реальных места на диске (точнее, на диске и в tmpfs):

  • /run/log/journal — volatile, живёт в памяти/tmpfs, после ребута пусто.
  • /var/log/journal — persistent, переживает перезагрузку.

Режим задаётся в /etc/systemd/journald.conf параметром Storage=. По умолчанию почти везде auto: если каталог /var/log/journal существует — пишем туда и храним между ребутами. Нет каталога — сидим в /run.

Это ловушка, на которую люди наступают сами. На «голом» облачном образе журнал был volatile, места хватало. Кто-то из документации скопировал mkdir -p /var/log/journal «чтобы логи не терялись», перезапустил systemd-journald — и auto молча стал persistent. Лимиты при этом никто не ставил. Через неделю диск красный.

Проверяю так:

df -h / /var /run
du -sh /var/log/journal /run/log/journal /var/log 2>/dev/null
journalctl --disk-usage
ls -ld /var/log/journal
systemd-analyze cat-config systemd/journald.conf

journalctl --disk-usage говорит, сколько journald сам считает занятым. du — сколько видит файловая система. Если они сильно расходятся, смотрю разреженные файлы и «хвосты» после неудачного vacuum: du -sh --apparent-size /var/log/journal рядом с обычным du -sh.

Почему «дефолт 10%» на VPS — уже инцидент

В man у journald это звучит безобидно: SystemMaxUse по умолчанию — около 10% файловой системы, с потолком 4G; SystemKeepFree — около 15% свободными. На корне в 40G это до четырёх гигабайт журнала. На корне в 20G — те же проценты, только больнее.

Четыре гигабайта логов на виртуалке, где сам сайт весит 800 мегабайт, — не «запас на разбор». Это место, которое ночью отъест MySQL, очередь почты или бэкап.

Важная оговорка: лимиты — не жёсткая квота в момент записи каждой строки. Journald ротирует и чистит архивные файлы, когда доходит до порога. Активный system.journal так просто не схлопнется. Если диск уже 100%, vacuum может не стартовать нормально, потому что некуда дописать ротацию. Классический труп: раздел полный, journald не вакуумит, сервисы не пишут, SSH на ключах ещё жив, потому что сессия старая.

Поэтому на маленьком VPS я не оставляю дефолт. Дефолт рассчитан на машину, где /var не равен всему диску, и где гигабайды логов — нормальная цена за историю.

Persistent vs volatile: это про утро после ребута, не про вкус

Volatile соблазнительно прост: ребутнул — логи кончились, диск не раздувается. Я так оставляю только там, где журнал всё равно уезжает на внешний syslog и локальная история после ребута не нужна. На обычном VPS с сайтом это почти никогда не тот случай.

Типичный ночной сценарий: в 03:14 гипервизор переехал, ядро поймало OOM, сработал watchdog хостера, или сам systemd ушёл в reboot после failed unit. Утром вам нужен предыдущий бут. Он берётся так:

journalctl --list-boots
journalctl -b -1 -p err..alert --no-pager
journalctl -b -1 -u ssh -u nginx -u php-fpm -u mysql --no-pager | tail -n 200

Если Storage=volatile, после ребута -b -1 пустой. Вы будете гадать по last reboot и по графику load у хостера. Это не диагностика, это гадание.

Persistent без лимита — другая крайность: история есть, но её некуда писать, потому что диск кончился самой историей.

Рабочая схема на VPS, которую я ставлю почти всегда: persistent, но маленький. История на несколько дней, не на квартал. Внешний syslog — если он уже есть; не вместо локального журнала, а поверх, чтобы переживать смерть самой машины.

Что смотреть overnight, пока диск ещё не ноль

Если диск уже 100% — сначала воздух, потом разбор. Если ещё есть 1–2 гигабайта, ночью я смотрю не «красивый dashboard», а четыре вещи.

Первое: это вообще journald или рядом лежит второй обжора. Часто параллельно толстеют /var/log от rsyslog, бинарные логи MySQL, json-file логи Docker. Journald виноватят первым, потому что путь незнакомый.

du -xhd1 /var /var/log /var/lib 2>/dev/null | sort -h
du -sh /var/lib/docker/containers 2>/dev/null
du -sh /var/lib/mysql 2>/dev/null
ls -lh /var/log/*.log /var/log/syslog /var/log/messages 2>/dev/null

Второе: журнал вырос за ночь или копился две недели. Смотрю даты файлов и размер активного относительно архивных:

ls -lhS /var/log/journal/*/* 2>/dev/null | head
journalctl --header | head -n 40

Архивные файлы с @ в имени — уже закрытые куски. Если за ночь вырос один свежий файл на гигабайт — это не «дефолтный 10%», это болтливый юнит. Если лежат десятки старых файлов по 300–400M — это никто никогда не ставил SystemMaxUse и не делал vacuum.

Третье: был ли ребут. journalctl --list-boots, who -b, last -x reboot | head. Если бут ночью был, первым делом journalctl -b -1 -p err, а не сегодняшний хвост. Сегодняшний хвост после ребута начинается с «диска мало» и маскирует причину.

Четвёртое: не забился ли /run. Volatile журнал живёт в tmpfs. На машине с 1–2 гигабайтами RAM /run маленький. Когда он полный, симптомы странные: не стартуют юниты, Failed to create ... No space left, при этом df -h / ещё живой. Смотрю df -h /run отдельно, всегда.

Кто орёт в журнал: без этого лимит только отодвинет взрыв

Ограничить размер — обязательно. Но если каждую ночь один сервис пишет сотни тысяч строк, вы просто быстрее упрётесь в потолок и начнёте терять именно те сообщения, которые нужны для разбора.

Счётчик по юнитам за ночь, без jq:

journalctl --since "6 hours ago" -o json --no-pager | python3 -c '
import sys, json, collections
c = collections.Counter()
for line in sys.stdin:
    try:
        j = json.loads(line)
    except Exception:
        continue
    key = j.get("_SYSTEMD_UNIT") or j.get("SYSLOG_IDENTIFIER") or "?"
    c[key] += 1
for k, v in c.most_common(20):
    print("%8d  %s" % (v, k))
'

На живой машине это может минуту помолчать — json тяжёлый. Если python3 жалко, грубее по short-формату:

journalctl --since "6 hours ago" --no-pager | awk '{print $5}' | sed 's/\[.*//' | sort | uniq -c | sort -nr | head

Пятое поле в short — не священная корова, на неанглийской локали колонки плывут. Для разбора лучше json.

Что обычно оказывается наверху, когда «за ночь съело диск»:

  • systemd-таймер бэкапа с StandardOutput=journal и rsync -v / find на сотни тысяч файлов;
  • fail2ban + ssh-парольный брут: одна попытка — несколько строк в journal и ещё дубль в auth.log;
  • PHP/Node в debug, который кто-то включил «на час» в пятницу;
  • сетевой юнит, который рестартует в цикле и каждый раз пишет stack trace;
  • ядро и OOM-killer — это уже не болтовня, это авария, её резать лимитом журнала нельзя, её надо читать.

Для шумного, но не аварийного юнита правильнее прикрутить его вывод, а не бесконечно растить journal. В unit-файле: StandardOutput=append:/var/log/backup.log плюс logrotate, или убрать -v. RateLimit в journald.conf режет пачку одинаковых сообщений, но на бэкап с уникальными путями в каждой строке он почти не действует: burst исчерпывается, а диск всё равно пишется, пока не упрётся.

SystemMaxUse и соседние крутилки

Править лучше drop-in, а не жирный коммент в исходном journald.conf. Так видно, что меняли вы, а не пакет.

mkdir -p /etc/systemd/journald.conf.d
cat >/etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
Storage=persistent
SystemMaxUse=200M
SystemKeepFree=1G
SystemMaxFileSize=50M
RuntimeMaxUse=50M
MaxRetentionSec=7day
MaxFileSec=1day
Compress=yes
EOF
systemctl restart systemd-journald
journalctl --vacuum-size=200M --vacuum-time=7d
journalctl --disk-usage

Зачем каждая строка, коротко.

Storage=persistent — явно, чтобы не зависеть от «кто-то случайно создал каталог». Каталог после этого должен существовать: mkdir -p /var/log/journal, права оставить journald самому поправить.

SystemMaxUse=200M — потолок persistent-журнала. На VPS с сайтом и ssh мне для разбора инцидента обычно хватает десятков–пары сотен мегабайт, не гигабайт. Если это шлюз, с которого разбирают неделю брутфорса по сырым логам — поставьте больше, но цифрой, не дефолтом.

SystemKeepFree=1G — не съедать последний гигабайт, даже если MaxUse ещё не достигнут. На маленьком корне это важнее MaxUse: MySQL и очередь почты должны иметь куда писать.

SystemMaxFileSize — чтобы один файл не вырос в весь лимит. Vacuum чистит архивные; огромный активный файл неудобно и vacuum, и копировать.

RuntimeMaxUse — потолок для /run, чтобы volatile не убил tmpfs.

MaxRetentionSec=7day — дефолт по времени почти не режет, живёт размер. Я хочу неделю, не «пока влезает 10% диска». Для разбора overnight недели хватает. Месяц уже про хранение, его лучше делать на syslog-коллекторе, не на корне VPS.

После рестарта journald файлы сами не схлопнутся до новой цифры в ту же секунду. Поэтому сразу --vacuum-size / --vacuum-time. Рестарт журналы не удаляет — это не rm.

Vacuum — не rm -rf, особенно когда диск уже 100%

Нормальный путь:

journalctl --disk-usage
journalctl --vacuum-size=100M
journalctl --vacuum-time=2d
journalctl --verify

--verify иногда находит битый хвост после внезапного отключения питания. Битый файл vacuum может обходить стороной, и место не отдаётся. Тогда смотрю имена в /var/log/journal/<machine-id>/ и убираю архивные *@*.journal / *.journal~, не трогая текущий system.journal.

Чего не делаю, пока есть другой выход: rm -rf /var/log/journal/* на живой системе. Один раз из ста это проходит, в остальные journald остаётся с открытыми inode, место не возвращается, а после рестарта вы получаете пустой журнал ровно в тот момент, когда хотели понять, почему диск кончился.

Аварийный порядок, если писать уже некуда:

  • найти и удалить пару самых старых архивных файлов *@*.journal — это даёт воздух;
  • сразу journalctl --vacuum-size=80M;
  • поставить drop-in с лимитами и перезапустить journald;
  • только потом искать болтливый юнит, иначе пока вы читаете json, диск снова забьётся.

Сигнал «почистись прямо сейчас» у journald — SIGUSR2. SIGUSR1 — сбросить буфер на диск, не vacuum. Путать их бессмысленно, но люди путают.

Двойной учёт: journald, rsyslog и Docker

На Debian/Ubuntu часто живы оба мира. journald пишет своё. rsyslog забирает через imjournal или сокет и пишет /var/log/syslog, /var/log/auth.log. Итого одна и та же неудачная попытка SSH лежит дважды. Rate-limit в journald на файлы rsyslog не действует.

Проверяю, не тащим ли мы дубль:

systemctl is-active rsyslog syslog-ng 2>/dev/null
grep -RInE 'imjournal|imuxsock|ForwardToSyslog' /etc/rsyslog.conf /etc/rsyslog.d /etc/systemd/journald.conf /etc/systemd/journald.conf.d 2>/dev/null
du -sh /var/log/journal /var/log/syslog /var/log/auth.log 2>/dev/null

ForwardToSyslog=yes плюс persistent journald — сознательное удвоение. Имеет смысл, если внешний разбор завязан на syslog-файлы, а journal вы хотите маленьким. Не имеет смысла, если никто эти файлы не читает, а logrotate на syslog стоит «weekly, rotate 8» и молча держит ещё сотни мегабайт.

Docker — отдельная яма. json-file драйвер пишет в /var/lib/docker/containers/.../*.log, это не journald. Человек чистит journal, диск не отходит, начинается шаманство. Если в du победил docker — лимиты в daemon.json (log-opts max-size), не в journald.conf. Путь другой, болезнь похожая.

Что я реально оставляю на маленьком VPS

Для обычного сайта (nginx, php-fpm, mysql, ssh) на одном диске:

  • persistent, 150–300M потолок, неделя хранения;
  • /run ограничен десятками мегабайт;
  • таймеры бэкапа не пишут verbose в journal;
  • если rsyslog нужен файлами для fail2ban — пусть живёт, но logrotate жёсткий, не дефолтный weekly на 100M файлах;
  • внешний syslog — когда машин больше двух или когда хочется историю длиннее недели.

Volatile я ставлю на одноразовые тестовые машины и на те VPS, где журнал и так уезжает агентом, а локальный диск маленький и ребут не является загадкой.

И я не храню journal как бэкап логов. Это кольцевой буфер для разбора. Если лог должен жить три месяца «по требованиям», ему место не в /var/log/journal на корне.

Утренний чеклист, если ночью диск снова опух

Короткий порядок, которым я хожу, чтобы не начать не с того конца:

  • df -h / /run /var — какой именно раздел, не «диск вообще»;
  • du -xhd1 /var — journal это или mysql/docker/бэкап;
  • воздух: vacuum или пара архивных файлов, не apt и не /tmp;
  • journalctl --list-boots и при ночном ребуте сразу -b -1 -p err;
  • счётчик юнитов за ночь — кто писал, не «сколько процентов в SystemMaxUse»;
  • drop-in с SystemMaxUse / SystemKeepFree, vacuum до новой цифры;
  • проверка, что через час journalctl --disk-usage не ползёт обратно с той же скоростью.

Если после лимита журнал упирается в потолок каждые несколько часов — лимит сработал, но источник шума жив. Тогда это уже не настройка journald, а разбор конкретного юнита. Крутить SystemMaxUse вверх, «чтобы влезало», на маленьком VPS я не соглашаюсь: вы просто вернёте ту же заявку через неделю, только с более толстым журналом и снова нулём на диске.

Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?

Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.

Обсудить задачу

Помогла статья? Поблагодари автора!

Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.

Угостить автора чашечкой кофе