Сервер после ребута висит в загрузке лишнюю минуту, systemd-analyze показывает цифру, которой стыдно показать руководству, а виновником оказывается вовсе не старт nginx и не сетевой юнит, а скромная служба systemd-journal-flush.service. Ситуация знакома многим администраторам, но далеко не все понимают, почему простое "дописать логи на диск" способно тормозить всю систему и главное, что с этим делать правильно, а не костылём под названием mask. Ниже разберём диагностику через systemd-analyze blame и critical-chain, устройство journald-хранилища и рабочие настройки journald.conf, которые реально применяют в продакшене.
Симптомы проблемы и первичная диагностика через systemd-analyze
Первый шаг всегда одинаков: измерить, сколько длится загрузка и куда уходят секунды. Базовая команда выглядит так:
systemd-analyze
Типичный вывод на проблемной машине:
Startup finished in 14.245s (kernel) + 1min 34.132s (userspace) = 1min 48.377s
graphical.target reached after 1min 33.890s in userspace.
Ядро отработало за 14 секунд, а пространство пользователя молотило полторы минуты. Уже подозрительно. Смотрим, кто именно тормозит:
systemd-analyze blame
Образец вывода:
52.437s systemd-journal-flush.service
12.103s network-online.target
8.944s fstrim.service
6.231s NetworkManager-wait-online.service
1.402s plymouth-quit-wait.service
...
Первая колонка здесь - чистое время инициализации юнита, вторая - имя службы. Строка systemd-journal-flush.service с половиной минуты на первом месте - это и есть наш пациент. Blame сортирует по убыванию, так что виновник обычно торчит наверху, как гвоздь из старой доски. Важный нюанс: blame показывает изолированное время каждого юнита, но не учитывает, что службы стартуют параллельно. Чтобы увидеть реальную критическую цепочку загрузки, применяют:
systemd-analyze critical-chain
Фрагмент вывода:
The time when unit became active or started is printed after the "@" character.
graphical.target @1min 33.890s
└─multi-user.target @1min 33.889s
└─systemd-journal-flush.service @8.412s +52.437s
└─systemd-remount-fs.service @7.980s +431ms
Жирнее всего здесь бьёт строка flush: он начался на восьмой секунде (значение после @) и занял 52 секунды, при этом целевая группа multi-user ждала его завершения. Пока идёт flush, journald ещё не принимает сообщения в постоянное хранилище, и часть юнитов вынуждена стоять. Для наглядности полезно построить SVG-карту:
systemd-analyze plot > boot-trace.svg
Файл открывают в браузере и сразу видят длинную красную полосу flush, растянувшуюся через весь таймлайн.
Почему systemd-journal-flush вообще нужен и почему он тормозит
Сначала журнал systemd накапливается в оперативной памяти, в каталоге /run/log/journal. Хранилище /run - это tmpfs, оно сотрётся при выключении, но на стадии ранней загрузки диск ещё не смонтирован и писать больше некуда. Когда корневая ФС смонтирована, юнит systemd-journal-flush копирует накопленные сообщения из /run/log/journal в постоянный каталог /var/log/journal, после чего journald переключается на запись на диск. Заодно применяются ротация и очистка по лимитам. И вот тут начинается боль. Если накопилось 300 МБ логов от болтливых юнитов, диск медленный (бюджетный медленный SATA-диск или перегруженная виртуалка с iowait 40%), а ротация затрагивает десятки файлов, операция растягивается на десятки секунд. Весь userspace стоит.
Типичные причины медленного flush:
- включён
Storage=persistentпри медленном или перегруженном диске, особенно HDD и сетевые тома; - избыточное логирование: включённый debug-режим udev, юниты с
StandardOutput=journalи уровнем debug, спамный auditd; - большие лимиты
SystemMaxUse, из-за которых journald хранит гигабайты и долго их перебирает при vacuum; - фрагментированные журналы вручную после удаления части файлов, journald тратит время на сверку индексов;
- повреждённые файлы журнала, которые приходится проверять и ремонтировать.
Текущее занятое пространство проверяется командой:
journalctl --disk-usage
Вывод, например:
Archived and active journals take up 1.8G in the file system.
Полтора и более гигабайт - прямое свидетельство того, что flush и ротация будут недёшевы.
Тюнинг journald.conf: volatile-хранилище и честные лимиты
Основной файл настройки - /etc/systemd/journald.conf. Для серверов, где логи гонят в централизованный сборщик (syslog, rsyslog, journald forward), а локальная история за ребут не нужна, оптимальная конфигурация выглядит так:
[Journal]
# Storage=volatile: журнал живёт только в /run/log/journal (tmpfs),
# flush на диск не требуется вовсе, секция постоянного хранилища отключена
Storage=volatile
# Общий потолок для всех volatile-журналов в /run: 200 МБ,
# при превышении journald начинает удалять старое
RuntimeMaxUse=200M
# Максимальный размер одного файла журнала, чтобы ротация была быстрой
RuntimeMaxFileSize=20M
# Сколько файлов хранить одновременно
RuntimeMaxFiles=8
# Ограничение скорости: за 30 секунд не более 1000 сообщений от одного сервиса,
# дальше идёт подавление, защита от спамеров
RateLimitIntervalSec=30s
RateLimitBurst=1000
# Куда пересылать: syslog получает поток для централизованного сбора
ForwardToSyslog=yes
После правки применяем изменения без ребута:
systemctl restart systemd-journald
С Storage=volatile юнит systemd-journal-flush теряет смысл: переносить в /var/log/journal попросту нечего. Если journal-flush всё равно тянет время из-за проснувшегося flush-триггера, бывает удобно замаскировать его явно, но строго осознанно и только при volatile:
systemctl mask systemd-journal-flush.service
Честно говоря, mask здесь - вторая стадия решения. Первая и главная - правильный Storage и разумные лимиты. Если local-логи всё же нужны, но нормально вся история не влезает в память, есть компромисс Storage=auto: journald пишет на диск, только если каталог /var/log/journal существует, иначе работает в памяти. Хороший паттерн для golden image - просто не создавать /var/log/journal на эталонном образе, и система автоматически останется на volatile.
Различие двух каталогов и тонкости миграции. /run/log/journal - tmpfs, существует от старта ядра до выключения, содержит журналы текущей загрузки и быстрый в записи, но съедает оперативную память. /var/log/journal - постоянное хранилище на диске, переживает reboot, зато платит ценой flush и медленной ротации. Проверить, куда пишет journald сейчас, можно так:
ls -la /var/log/journal/ /run/log/journal/
Если /var/log/journal отсутствует в выводе и есть только /run - система уже в режиме volatile или auto без каталога. Сколько занято в памяти:
du -sh /run/log/journal/
Например:
96M /run/log/journal/
При RuntimeMaxUse=200M и 8 ГБ ОЗУ это несущественно. Но на контейнерном хосте со 128 МБ под /run перебор лимита приведёт к вытеснению данных. Тут математика простая: лимит не должен превышать примерно 10-15% объёма tmpfs.
Очистка старых журналов через vacuum
Если постоянное хранилище всё же используется, его надо регулярно вычищать, а не ждать, пока диск набухнет. Два ручных способа:
# По времени: удалить всё, что старше 14 дней
journalctl --vacuum-time=14d
# По объёму: оставить не более 500 МБ
journalctl --vacuum-size=500M
Вывод после выполнения:
Vacuuming done, freed 1.2G of archived journals from 38 files.
Deleted archived journal /var/log/journal/.../Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. (128.0M).
Каждая строка - удалённый архивный файл с размером. Vacuum работает только по архивированным файлам; чтобы подрезать и активный журнал, его сперва ротируют принудительно:
journalctl --rotate
journalctl --vacuum-time=1d
Для автоматики удобно повесить в cron или systemd-таймер связку rotate + vacuum-size, чтобы журналы никогда не разрастались. Тогда и flush на следующей загрузке будет коротким.
Пошаговый план действий администратора в проде
Порядок, отработанный на десятках серверов, выглядит понятно. Сначала измерить: systemd-analyze, systemd-analyze blame, systemd-analyze critical-chain - и зафиксировать время flush. Затем оценить объём через journalctl --disk-usage и du -sh /var/log/journal /run/log/journal. Следом найти спамеров: journalctl -b | awk '{print $5}' | sort | uniq -c | sort -rn | head покажет, кто генерирует львиную долю сообщений за загрузку. Дальше принять решение о хранилище: volatile с централизованным сбором журналов либо persistent с жёсткими лимитами. Потом прописать journald.conf, выполнить systemctl restart systemd-journald и journalctl --vacuum-size для расчистки старой истории. Финальный шаг - перезагрузить сервер в окне обслуживания и снова снять blame, сравнив цифры до и после: эффект честно измеряется, а не предполагается.
После перехода на volatile с лимитом 200 МБ на средней нагрузке время systemd-journal-flush падает с десятков секунд до нуля - юнит либо отсутствует, либо завершается мгновенно. Загрузка userspace из 1min 34s ужимается до 30-40 секунд, и это без единой строчки кода в приложениях.
Проверка целостности журнала и глубже в debug
Если flush медленный даже при небольшом объёме логов, подозревайте повреждённые файлы журнала. Journald при монтировании хранилища проверяет индексы и на битых файлах может зависнуть надолго. Проверка простая:
journalctl --verify
Вывод при проблемах:
FAIL: /var/log/journal/.../Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. (truncated)
PASS: /var/log/journal/.../system.journal
Строки с FAIL - повреждённые файлы, их проще всего переместить в карантинный каталог и выполнить journalctl --rotate, после чего journald создаст свежие файлы. Такие битые файлы нередко прячутся за непонятными подвисаниями. Заодно полезно глянуть, кто из юнитов пишет больше всех на прошлой загрузке, ключ -b -1 берёт предыдущий boot:
journalctl -b -1 --no-pager | awk '{print $5}' | sort | uniq -c | sort -rn | head -10
Типичный верх списка:
24103 systemd[1]:
19877 NetworkManager[812]:
8704 kernel:
4402 audit[1]:
Первое число - количество строк, второе - источник. Если NetworkManager выдал девятнадцать тысяч строк за загрузку, копать надо его логирование, а не journald. Меньше данных на входе - быстрее flush. Для особо болтливых юнитов в override-юните можно уменьшить поток:
# /etc/systemd/system/example.service.d/override.conf
[Service]
# Ошибки и выше, без информационного шума
LogLevelMax=warning
После systemctl daemon-reload и рестарта службы журнал заметно худеет.
Частые ошибки при борьбе с медленным flush.
В практике встречается несколько антипаттернов, которые маскируют симптом, но не лечат причину. Первый - слепой mask systemd-journal-flush на системе с Storage=persistent: сообщения копятся в /run, переполняют tmpfs и система валится уже в рантайме, а не при загрузке. Второй - выставление SystemMaxUse=0 в надежде снять ограничение: ноль не означает "без лимита", journald либо поднимет значение до минимально допустимого, либо вернётся к умолчаниям; для отключения записи на диск нужен Storage=volatile или Storage=none. Третий - удаление файлов журнала вручную через rm под живым journald: служба держит файловые дескрипторы, место не освобождается, а индексы расходятся с реальностью, что перебирает flush на следующем старте. Правильный путь только через journalctl --vacuum-* или через полную остановку journald перед чисткой. Четвёртая ошибка - забыть, что значения в журнале read-only на лету: после правки journald.conf без systemctl restart systemd-journald старые лимиты продолжают действовать, и администратор ждёт эффекта, которого нет. Изменения видно сразу в статусе:
systemctl status systemd-journald --no-pager
Строки вывода с Runtime journal (/run/log/journal/...): 48.0M, max 200.0M подтверждают, что лимит применён. Такой контроль заменяет час отладки, когда конфиг вдруг лежит в /etc/systemd/journald.conf.d с другими значениями.
Профилактика и что мониторить дальше
Проблема любит возвращаться, если её не прибить процессом. Минимум - контролировать journalctl --disk-usage алертом, следить за ростом логов конкретных юнитов после каждого релиза и держать RateLimitBurst на адекватном уровне. На машинах, где journald форвардит в syslog, полезно проверять, что внешний collect реально забирает поток, иначе можно остаться и без дисковых логов, и без централизованных. Для критичных серверов держите в документации эталонный journald.conf и скрипт расчёта до-после через systemd-analyze - это снимает споры "а стало ли быстрее" намертво. Быстрая загрузка - не везение, а следствие честных измерений и настроенного журнала.