MAATRIX / Блог / Потолок записи в лог: сколько строк в секунду выдержит диск до остановки приложения

Потолок записи в лог: сколько строк в секунду выдержит диск до остановки приложения

MAATRIX

Логирование редко проектируют как нагрузку — его добавляют по одной строке за раз, «на всякий случай», пока однажды под пиковым трафиком приложение не начинает подвисать не на бизнес-логике, а на банальной записи очередной строки в файл. Диск в такие моменты не сломан и не переполнен — он просто физически не успевает принимать поток записи, а стек логирования передаёт эту задержку прямо в поток приложения. Разберём, как посчитать, где проходит этот потолок, и что сделать, чтобы логи не решали, жив ваш сервис или нет.

Почему логирование вообще может стать узким местом

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

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

Второй источник риска — синхронный fsync на каждую строку. Он даёт гарантию сохранности данных при сбое, но платит за неё задержкой в разы выше обычной записи: диск должен подтвердить физическую фиксацию данных, а не просто принять их в свой внутренний кэш. Если логгер вызывает fsync после каждой строки «для надёжности» — это чаще всего избыточная предосторожность, превращающая логирование в самый медленный узел системы. Что именно происходит при вызове sync и почему «файл записан» не всегда означает «данные на диске», разбирали в статье про sync и файл, которого физически ещё нет на диске.

Как посчитать реальную пропускную способность диска под логи

Прежде чем разбираться, сколько строк в секунду выдержит ваш диск, нужно понять, что это вопрос не про IOPS в вакууме, а про конкретный паттерн записи. Логирование — почти всегда последовательная запись (append-only): новые строки дописываются в конец файла, а не разбрасываются по случайным местам диска, а последовательная запись на любом накопителе быстрее случайной, иногда в разы. Разница между этими паттернами и почему она так велика на SSD разобрана в статье про последовательную и случайную запись на SSD.

Грубая формула для оценки потолка такая:

строк_в_секунду = (пропускная_способность_диска_МБ_с × 1024) / средний_размер_строки_КБ

Если у вас плоские текстовые строки логов размером в среднем 200 байт (0.2 КБ), а диск последовательно пишет условные 500 МБ/с — это ориентировочно 2.5 млн строк в секунду по чистой пропускной способности. Но на практике вы почти никогда не упрётесь именно в мегабайты в секунду при коротких строках — раньше сработает лимит на число отдельных write-вызовов в секунду, особенно если каждая строка летит отдельным системным вызовом без буферизации. Здесь появляется второй параметр — IOPS для мелких синхронных записей, заметно ниже, чем можно предположить по паспортным цифрам последовательной пропускной способности: та цифра почти всегда меряется на паттерне, далёком от коротких append-записей с fsync.

Практический вывод: не берите паспортные цифры производителя как готовый ответ. Замерьте диск под тем паттерном, который реально будет у вашего логирования — короткие последовательные append-записи, с fsync и без него, с разным размером батча. Инструмент fio позволяет собрать честный профиль такой нагрузки, а не абстрактный «максимум диска» — подробнее в статье про честный замер диска через fio. Ориентировочный сценарий для проверки:

fio --name=log-append --filename=/var/log/test.log \
    --rw=write --bs=256 --iodepth=1 --numjobs=4 \
    --size=1G --fsync=1 --time_based --runtime=30 \
    --group_reporting

Здесь bs=256 имитирует короткие строки лога, fsync=1 — синхронный сброс после каждой записи (худший случай), numjobs=4 — несколько параллельных воркеров, как в реальном приложении. Сравните результат с тем же прогоном при fsync=0 — разница обычно и есть цена «надёжности каждой строки», и часто она измеряется не в процентах, а в разах.

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

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

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

Тип диска и порядок цифр, которые стоит держать в голове

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

Тип накопителяПаттернОтносительная скорость мелкой синхронной записи
HDD (вращающийся)случайная записьсамая низкая — ограничена механическим позиционированием головки
HDDпоследовательная записьзаметно выше случайной, но всё равно проигрывает SSD
SATA SSDслучайная/последовательнаястабильно быстрее HDD, но упирается в шину SATA
NVMe SSDпоследовательная записьмаксимальная из доступных на VPS/выделенных серверах
Сетевое хранилище (NFS/сетевой диск)любаядобавляет задержку сети поверх задержки самого диска

Важный нюанс для арендованных серверов и VPS: на виртуализированном диске поверх физического накопителя часто стоит дополнительный слой — сетевое хранилище, тонкие тома, лимиты гипервизора на IOPS для тарифного плана. Даже если физический NVMe под капотом способен на многое, ваша виртуальная машина может упираться не в диск, а в назначенную ей квоту. Если логирование — заметная часть нагрузки на прод, для этого компонента может быть разумно выбрать выделенный сервер с NVMe вместо бюджетного VPS с шэрингом дисковых ресурсов между соседями.

Что физически происходит, когда логирование становится узким местом

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

  1. Растёт latency отдельных запросов. Если логирование синхронное, каждый вызов записи лога добавляет к времени обработки запроса задержку диска: при нормальной нагрузке это единицы микросекунд, незаметно, при упоре в диск — миллисекунды и десятки миллисекунд на строку.
  2. Растёт число потоков/воркеров в состоянии io wait. В top или vmstat это видно как рост %wa — процессор простаивает не потому что ему нечего делать, а потому что множество потоков ждут ответа от диска. Сервер при этом выглядит «загруженным», хотя реальная работа не делается.
  3. Забивается буфер/очередь логгера. Если используется асинхронный логгер с ограниченной очередью в памяти, при устойчивом отставании записи от producer'ов очередь заполняется. Дальше логгер обязан выбрать: блокировать вызывающий поток (backpressure) или начать терять сообщения (drop) — оба варианта уже деградация сервиса, просто разного типа.
  4. Блокировка потоков приложения. В худшем случае, если логгер синхронный и без отдельной очереди, каждый поток, который пытается что-то залогировать, встаёт в очередь на запись напрямую. При интенсивной нагрузке это превращается в цепную реакцию: чем больше потоков ждут диск, тем меньше обрабатывают реальные запросы, тем выше нагрузка на оставшиеся, тем больше строк лога они генерируют (в том числе об ошибках таймаутов) — и петля затягивается сама на себя.

Буферизация и асинхронное логирование как основной инструмент

Главный рычаг, который отделяет «логирование как узкое место» от «логирование, которое работает в фоне» — разрыв связи между моментом генерации строки лога и моментом её физической записи на диск. Асинхронное логирование делает ровно это: вызывающий код кладёт сообщение в очередь в памяти (кольцевой буфер или lock-free структуру) и продолжает работу немедленно, а отдельный поток забирает сообщения из очереди и пишет их на диск батчами.

Ключевые параметры, которые стоит явно контролировать, а не оставлять на дефолтах библиотеки:

  • Размер очереди в памяти. Слишком маленькая — быстро переполняется под пиковой нагрузкой и вынуждает выбирать между блокировкой и потерей строк. Слишком большая — прячет проблему на время, но при аварийном перезапуске процесса теряет весь буфер целиком.
  • Политика при переполнении очереди. Явно решите: блокировать producer (защищает от потери данных ценой риска замедления), отбрасывать новые сообщения (теряете свежие данные, зато сервис не тормозит) или отбрасывать по приоритету (сохранять ERROR, сэмплировать DEBUG). Дефолт библиотеки не всегда совпадает с тем, что вы бы выбрали осознанно.
  • Размер и интервал батча на запись. Компромисс между задержкой (как быстро строка попадёт на диск) и эффективностью (меньше системных вызовов на то же количество данных). Батч в 100-500 строк или таймаут в десятки-сотни миллисекунд — разумная отправная точка, но значение стоит подбирать под профиль нагрузки.
  • fsync по расписанию, а не на каждую строку. Если файловый лог не единственная копия данных, синхронный fsync на каждую запись почти всегда избыточен. Достаточно периодического flush — раз в секунду или по накоплению батча.

Отдельно стоит развести буферизацию на уровне логгера и буферизацию на уровне ОС (page cache). Даже без явного асинхронного логгера обычная запись через write() без fsync уходит в page cache и попадает на физический диск позже, силами ядра. Это даёт естественную амортизацию всплесков — но полагаться только на неё рискованно: при аварийном отключении питания несброшенные данные в page cache теряются, а при устойчиво высокой скорости записи ядро всё равно упрётся в скорость физического сброса и начнёт притормаживать пишущие процессы (writeback throttling).

Практические решения: ротация, сэмплирование, вынос логов

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

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

Сэмплирование логов. Не каждое событие нужно писать со стопроцентной полнотой. Для высокочастотных однотипных событий (например, успешные ответы 200 на массовый endpoint) можно логировать каждое N-ное сообщение или ограничивать частоту одинаковых сообщений (rate limiting по сигнатуре сообщения). Критичные события — ошибки, аудит безопасности — сэмплировать нельзя. Хорошая практика — сэмплировать DEBUG/INFO, но сохранять WARN/ERROR полностью.

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

Вынос логов на отдельный сервис (централизованный сбор). Вместо синхронной записи в локальный файл — отправка в буферизованный агент (Vector, Fluent Bit, Filebeat), который сам батчит и асинхронно доставляет данные в центральное хранилище вроде Graylog или Grafana Loki. Приложение пишет либо в локальный сокет с минимальной задержкой, либо в память агента — тяжёлая часть (сетевая доставка, индексация) выполняется вне критического пути запроса, на отдельном сервере.

Structured logging с батчингом. Переход от произвольных текстовых строк к структурированным записям (JSON, ключ-значение) не ускоряет запись сам по себе — структурированная строка обычно даже немного длиннее обычной. Выигрыш в другом: такие логи проще фильтровать и сэмплировать программно ещё до записи на диск (например, отбросить поле с телом запроса при INFO, оставить только при ERROR), а также проще передавать батчами в сжатом виде во внешний коллектор, снижая объём фактической записи и по диску, и по сети.

Как проверить, что вы уже упёрлись в этот потолок

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

  • iostat -x 1 — колонка %util (близко к 100% на активном лог-диске — сигнал насыщения) и await (среднее время ожидания запроса к диску; резкий рост при том же объёме данных — признак упора в потолок).
  • vmstat 1 — колонка wa (iowait) стабильно высокая одновременно с ростом latency приложения указывает, что процессы ждут диск, а не CPU.
  • Метрика длины очереди логгера — если библиотека логирования экспортирует размер внутренней очереди, устойчивый рост этой метрики под нагрузкой — прямое подтверждение, что producer обгоняет consumer.
  • Корреляция latency приложения с интенсивностью логирования — если задержки ответов растут именно в моменты пиковой генерации логов (например, при массовых ошибках, которые сами плодят ещё больше строк), это тот самый порочный круг блокировки на записи.
  • Сравнение fsync=1 и fsync=0 через fio на том же томе, где реально лежат логи — если разница в разы, а у вас включён синхронный fsync на каждую строку, это первая точка для оптимизации без изменения архитектуры.

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

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

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

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

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

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

Асинхронное логирование гарантирует, что я не потеряю ни одной строки при падении процесса?

Нет. Строки, которые лежат в буфере в памяти на момент аварийного завершения процесса (OOM-kill, panic), теряются, если не были сброшены на диск. Для критичных событий стоит либо писать их синхронно, либо использовать журналируемую очередь вне процесса приложения.

Стоит ли просто снизить уровень логирования до WARN, чтобы решить проблему с нагрузкой на диск?

Это быстрая мера, но она снижает наблюдаемость — теряется контекст, нужный при расследовании инцидентов. Сэмплирование INFO/DEBUG с сохранением полноты WARN/ERROR обычно даёт лучший баланс, чем грубое отключение уровня целиком.

NVMe вместо SATA SSD решит проблему упора в логи раз и навсегда?

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

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

Считайте по факту — метрика записанных строк в секунду на уровне логгера или прирост размера файла лога (wc -l) за фиксированный интервал в проде под реальной нагрузкой, а не в синтетическом тесте.

Нужно ли выносить логи на отдельный диск даже на небольшом проекте с низкой нагрузкой?

Нет, это оптимизация под конкретный симптом. Если текущий объём логирования далёк от потолка (проверяется через fio и мониторинг %util/await), разделение дисков добавит сложности без ощутимой пользы.

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

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

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