Типичная ночная история на 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 я не соглашаюсь: вы просто вернёте ту же заявку через неделю, только с более толстым журналом и снова нулём на диске.
Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?
Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.
Обсудить задачуПомогла статья? Поблагодари автора!
Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.
Угостить автора чашечкой кофе