MAATRIX / Блог / Как journald хранит логи и почему под нагрузкой он теряет строки

Как journald хранит логи и почему под нагрузкой он теряет строки

MAATRIX

Если под нагрузкой в journalctl вдруг обнаруживается дыра — сервис явно что-то писал, а строк на месте нет, — первая мысль обычно про сломанный диск или баг в приложении. Но у journald есть встроенный защитный механизм, который в такие моменты сознательно выбрасывает часть сообщений сам, оставляя явную пометку об этом. Разберём, как устроено хранилище journald изнутри, зачем ему вообще понадобилось ограничивать скорость приёма сообщений и как настроить лимиты так, чтобы они не откусывали от вас данные, которые на самом деле нужны.

Бинарный журнал вместо текстовых файлов старого syslog

Классический syslog всегда писал логи как обычный текст: одна строка — одно событие, всё дописывается в конец файла вроде /var/log/messages или /var/log/syslog. Формат простой и прозрачный — grep, awk, tail -f работают напрямую, никаких специальных инструментов не нужно. Но у этой простоты есть цена: файл — это просто последовательность байт без структуры, и любой поиск по полю (по PID, по юниту, по уровню важности) превращается в текстовый парсинг всего файла заново.

systemd-journald устроен принципиально иначе. Вместо плоского текста он хранит каждую запись как набор именованных полей — что-то среднее между строкой лога и записью в базе данных. У сообщения есть MESSAGE (сам текст), но рядом с ним лежат PRIORITY (уровень важности), _PID, _COMM (имя процесса), _SYSTEMD_UNIT (юнит systemd, если сообщение пришло от сервиса), _BOOT_ID (идентификатор конкретной загрузки системы), _HOSTNAME и ещё несколько десятков стандартных и произвольных полей. Всё это упаковано в собственный бинарный формат файлов .journal с внутренней индексацией.

Практическое следствие: journalctl -u nginx -p err --since today не читает файл построчно в поисках нужного — он использует индекс и сразу переходит к нужным записям. Это быстрее классического grep по большому текстовому файлу, особенно когда логов накопилось много. Обратная сторона — открыть .journal-файл напрямую в less или cat бессмысленно, вы увидите бинарную кашу. Работать с журналом можно только через journalctl или экспорт (journalctl -o json, -o export), это отдельная утилита, а не просто файл на диске. Для совместимости со старыми инструментами journald умеет пересылать копию потока в классический syslog-демон через ForwardToSyslog=yes — так на сервере иногда стоят одновременно journald как основное хранилище и rsyslog как приёмник для текстовых файлов или удалённой пересылки.

Что физически лежит на диске: постоянное и временное хранилище

Файлы журнала лежат в каталоге, привязанном к machine-id конкретной системы: /var/log/journal/<machine-id>/*.journal для постоянного (persistent) хранения или /run/log/journal/<machine-id>/*.journal для временного (volatile) — /run это tmpfs, оперативная память, и всё содержимое исчезает при перезагрузке. Какой вариант используется, определяет директива Storage= в /etc/systemd/journald.conf: значение auto (по умолчанию на большинстве дистрибутивов) означает «писать постоянно, если каталог /var/log/journal уже создан, иначе — только во временное хранилище». Это частая ловушка на свежих серверах: каталог /var/log/journal может не существовать вовсе, journald тихо пишет всё в tmpfs, и после первой же перезагрузки история логов исчезает целиком, хотя место на диске вроде бы есть.

Файлы .journal не растут бесконечно — они ротируются по достижении лимита размера (текущий активный файл переключается на новый), а старые файлы целиком удаляются при превышении общих лимитов SystemMaxUse= (постоянное хранилище) или RuntimeMaxUse= (временное), либо когда свободного места на разделе становится меньше SystemKeepFree=. Важный нюанс: это ротация целыми файлами, а не построчная обрезка — journald либо хранит файл целиком, либо целиком удаляет его, когда общий объём превышает лимит. Если вас интересует смежная тема — что происходит с местом на диске уже после ротации логов и почему оно не всегда сразу освобождается, — это отдельный разбор: почему после ротации логов место на диске не освобождается.

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

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

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

Почему один шумный процесс может обрушить систему логирования

systemd-journald — это один демон на всю систему, который принимает поток сообщений сразу из нескольких источников: собственный нативный протокол (sd_journal_send() из библиотек), совместимый с классическим syslog сокет /dev/log, сообщения ядра из /dev/kmsg, и — что важно для практики — стандартный вывод и вывод ошибок (stdout/stderr) каждого сервиса, у которого в юните указано StandardOutput=journal (а это поведение по умолчанию для юнитов systemd). То есть если приложение просто печатает в консоль через print() или console.log(), весь этот поток тоже идёт через journald.

Теперь представьте: сервис попадает в цикл ошибок — например, воркер падает, systemd его перезапускает, он снова падает через долю секунды и при каждом падении печатает полный стектрейс на несколько десятков строк. Или в коде случайно осталось включённым избыточное debug-логирование, и под реальной нагрузкой оно начинает сыпать тысячами строк в секунду. journald должен принять каждую такую строку, распарсить её на поля, проиндексировать и записать на диск — и делает это той же вычислительной мощностью и той же дисковой подсистемой, что нужна остальной системе для нормальной работы.

Без ограничений один такой процесс способен: забить доступное место на диске логами одного бесполезного повторяющегося сообщения, создать заметную нагрузку на дисковый ввод-вывод, конкурируя с реальными данными приложения и базы, и загрузить CPU индексированием бесполезного потока. Получается парадокс: подсистема логирования, которая должна помогать диагностировать проблемы, сама становится источником перегрузки и новой проблемой поверх исходной. Именно для защиты от этого сценария у journald есть настраиваемое ограничение скорости приёма сообщений — rate limiting.

Как устроено ограничение скорости приёма

Ограничение задаётся двумя параметрами в journald.conf (и в drop-in файлах /etc/systemd/journald.conf.d/*.conf): RateLimitIntervalSec= — длина временного окна, и RateLimitBurst= — сколько сообщений разрешено принять от одного источника за это окно. По документации systemd, ограничение применяется не глобально на всю систему, а раздельно для каждого источника (по сути — для каждого сервиса/юнита или процесса, слитого с ним по логированию), чтобы один шумный сервис не отбирал «бюджет» у остальных. В штатной поставке systemd эти параметры обычно приходят со значением по умолчанию около 10000 сообщений на 30-секундное окно на источник — но конкретные цифры стоит проверять именно на своей системе: дистрибутивы нередко переопределяют апстримные дефолты своей упаковкой, поэтому надёжнее один раз посмотреть cat /etc/systemd/journald.conf и, если параметры закомментированы, ориентироваться на актуальную версию man journald.conf в установленном у вас systemd.

Механика простая: пока источник укладывается в лимит, всё пишется как обычно. В момент, когда счётчик сообщений от источника за текущее окно превышает RateLimitBurst=, journald перестаёт принимать дальнейшие сообщения от этого источника до конца интервала — и это ключевой момент — не молча. journald вставляет в журнал собственную служебную запись о том, что часть сообщений от конкретного юнита была подавлена в это окно. То есть пропуск строк — это не потеря данных «в никуда», а осознанное решение с явной пометкой в самом же журнале. Проблема в том, что эту пометку легко пропустить: она выглядит как ещё одна строка среди тысяч остальных, и если вы ищете конкретное отсутствующее сообщение, а не читаете журнал целиком построчно, заметить её не так просто.

Отдельно стоит отличать rate limiting от другого механизма потери сообщений — переполнения сокета, через который источники передают данные в journald. Если journald физически не успевает вычитывать входящий поток (например, процессор перегружен чем-то ещё), сообщения могут теряться уже на уровне буфера сокета ядра, до того как journald вообще успел их разобрать и решить, укладываются ли они в rate limit. Это более грубая и менее предсказуемая потеря — без гарантированной пометки о том, что именно и сколько пропало, — и она обычно указывает не на настройки journald, а на то, что системе в целом не хватает ресурсов.

Как поймать причину пропавших строк на практике

Первый шаг при подозрении на rate limiting — поискать в журнале саму служебную пометку о подавлении, а не только пропавшее сообщение:

journalctl -u my-service --since "1 hour ago" | grep -i suppress

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

  • Фильтрация по уровню важности. journald может хранить не все уровни (MaxLevelStore= в конфиге) или пересылать не все (MaxLevelSyslog=, MaxLevelKMsg= и т. п.) — сообщение может честно приходить в journald, но не сохраняться из-за уровня.
  • Ротация по размеру. Если SystemMaxUse= выставлен низко, а объём логов на сервере большой, старые записи удаляются целыми файлами быстрее, чем вы успеваете их посмотреть — это не потеря «под нагрузкой», а обычное вытеснение старого новым.
  • Временное хранилище вместо постоянного. Если Storage= фактически работает в режиме volatile (см. предыдущий раздел про /run/log/journal), логи пропадают целиком при перезагрузке, а не построчно под нагрузкой.
  • Само приложение. Буферизация вывода на стороне приложения (например, построчная буферизация против блочной при выводе не в терминал) иногда приводит к тому, что строки просто не долетают до stdout вовремя — это не имеет отношения к journald вообще.

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

Как настроить лимит под реальный объём логов приложения

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

Глобально лимит меняется в /etc/systemd/journald.conf (или, что аккуратнее, отдельным drop-in файлом, чтобы не трогать системный конфиг напрямую):

# /etc/systemd/journald.conf.d/rate-limit.conf
[Journal]
RateLimitIntervalSec=30s
RateLimitBurst=50000

После правки конфига journald нужно перезапустить, чтобы применить новые значения:

systemctl restart systemd-journald

Если шумит не вся система, а конкретный сервис с ожидаемо высоким объёмом логов (например, прокси или API-шлюз под нагрузкой), в актуальных версиях systemd лимит можно переопределить точечно — в самом юните сервиса директивами LogRateLimitIntervalSec= и LogRateLimitBurst= в секции [Service], через systemctl edit имя-сервиса. Это точнее, чем поднимать лимит глобально: остальные сервисы продолжают жить с дефолтной защитой, а повышенный бюджет получает только тот, кому он реально нужен.

Полностью отключить ограничение можно, выставив RateLimitIntervalSec=0 — это прямо предусмотрено документацией journald как штатный способ снять лимит. Но здесь стоит быть честным с собой: отключение лимита возвращает именно тот риск, ради защиты от которого он существовал, — один зациклившийся процесс с багом снова получает возможность залить диск и забрать себе непропорционально много ресурсов индексирования. Разумный компромисс — не отключать лимит вслепую, а поднять его до реалистичного значения и обязательно держать рядом второй рубеж защиты: разумные SystemMaxUse=/RuntimeMaxUse=, чтобы даже при высоком лимите приёма общий объём хранимых логов был ограничен, и мониторинг свободного места на диске, чтобы узнать о проблеме раньше, чем диск заполнится полностью.

ПараметрЗа что отвечаетГде менять
RateLimitIntervalSec= / RateLimitBurst=Скорость приёма сообщений от одного источникаjournald.conf (глобально)
LogRateLimitIntervalSec= / LogRateLimitBurst=То же самое, но для одного конкретного сервисаЮнит-файл сервиса (systemctl edit)
SystemMaxUse= / RuntimeMaxUse=Общий объём хранимых логов на дискеjournald.conf
Storage=Постоянное или временное хранилищеjournald.conf

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

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

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

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

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

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

Как быстро понять, что journald уже отбрасывал сообщения на этом сервере?

Поищите в журнале служебные записи о подавлении: journalctl | grep -i suppress покажет все случаи за доступную историю, а journalctl -u конкретный-сервис --since "сегодня" сузит поиск до одного юнита за нужный период.

Что произойдёт, если полностью отключить rate limiting командой RateLimitIntervalSec=0?

journald перестанет сознательно отбрасывать сообщения по лимиту скорости, но вместе с этим исчезает и защита: зациклившийся или ошибочно настроенный процесс сможет писать логи без ограничения, упираясь уже только в размер диска и лимиты SystemMaxUse=/RuntimeMaxUse=, если они выставлены разумно.

Rate limiting в journald — это то же самое, что фильтрация по приоритетам syslog?

Нет, это разные механизмы. Фильтрация по приоритету решает, какие уровни важности (debug, info, warning и так далее) вообще сохранять, независимо от объёма. Rate limiting решает другой вопрос — сколько сообщений в единицу времени принимать от одного источника, независимо от их уровня важности.

Пропажа строк под нагрузкой — это всегда rate limiting?

Нет. Прежде чем чинить лимиты, стоит исключить переполнение хранилища (SystemMaxUse=), временное хранилище вместо постоянного (Storage= в режиме volatile) и потери на уровне сокета при явной нехватке ресурсов у сервера в целом — у каждой причины своя диагностика и свой набор исправлений.

Нужно ли трогать rate limiting на небольшом VPS с одним-двумя сервисами?

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

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

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

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