Асинхронное логирование давно считается “очевидной оптимизацией”: вынесли запись в отдельный поток — и всё стало быстрее.
Но если копнуть глубже, оказывается, что это не совсем так.
В предыдущей статье я разбирал производительность популярных C++ логгеров и показывал реальные цифры:
👉 https://habr.com/ru/articles/1012874/
Там уже было видно, что хорошо оптимизированное синхронное логирование может быть очень быстрым.
В этой статье разберёмся, почему async logging не делает логирование быстрее само по себе, и что на самом деле происходит внутри:
где тратится CPU
что происходит с очередями
почему возникает перегрузка
и почему deferred formatting работает далеко не всегда
Как устроено асинхронное логирование
Асинхронное логирование — это не что-то единое и монолитное. Это комбинация двух механизмов:
асинхронное форматирование
асинхронная запись (output)
И у них разные ограничения.
Асинхронное форматирование
Сюда входит:
захват аргументов
копирование или сериализация
возможно — отложенное форматирование
Главный вопрос:
Можно ли безопасно восстановить данные позже?
Если нет — форматирование откладывать нельзя.
Асинхронная запись
Это:
очереди
backend поток
sinks (файл, консоль, сеть)
flush / fsync
И именно это определяет реальную производительность.
Визуально
Синхронно:caller → format → write → doneАсинхронно:caller → capture → enqueue → [queue] → backend → format → write
Асинхронное логирование не убирает этапы — оно добавляет новые
Почему отложенное форматирование не всегда работает
Идея выглядит красиво:
положили формат + аргументы → потом отформатировали
Но это работает только если аргументы будут живы до момента начала форматирования.
Пример
std::string s = MakeText(); std::string_view sv = s; LOG_INFO("{}", sv); s.clear();
Если логгер сохранил только string_view, данные уже невалидны.
Та же проблема с:
char*на временные буферыссылками на изменяемые объекты
view на контейнеры
объектами с внутренними указателями
Это фундаментальное ограничение.
Как это решается на практике
Есть всего три варианта.
1. Копирование (safe capture)
Логгер копирует данные:
string_view→ копия строкиchar*→ копияобъекты → копия или сериализация
Это безопасно.
Но это значит:
Отложенное форматирование зачастую требует копирования данных. Отсюда следует, что отложенное форматирование может отнимать суммарно больше ресурсов, чем форматирование выполняемое синхронно
2. Немедленное форматирование
Если копировать нельзя:
LOG_INFO("{}", complex_object);
Форматирование происходит сразу.
Плюс:
безопасно
Минус:
увеличивает latency вызывающего потока
3. Своя сериализация
Для сложных типов:
пользователь сам определяет, что сохранять
или пишет codec
Это гибко, но усложняет систему. Кроме того наличие кодека не отменяет необходимость сохранять его копию (либо кодек берет на себя ответственность о том, что данные будет доступны в момент форматирования)
Важный вывод
Реальный pipeline:
копирование → очередь → форматирование → запись
А не:
очередь → форматирование → запись
Async logging не ускоряет запись саму по себе
Это ключевой момент.
Если диск/консоль умеет записывать 100k сообщений/сек а ты генерируешь 1M сообщений/сек, то 900k сообщений/сек накапливаются
Очередь — это не ускорение
Очередь — это не ускорение, а склад долга
Она просто откладывает выполнение задачи.
Один backend поток — жёсткий предел
Во многих реализациях (например, Quill):
много producer потоков
один backend поток
Масштабирования нет
Сколько бы ни было потоков:
consumer один
он bottleneck
Несколько sinks
Если писать в:
файл
консоль
то backend делает:
format + file + console
Следствие
Медленный sink замедляет всё логирование
Пример:
файл: 10 µs
консоль: 200 µs
→ итог: 210 µs
Что происходит при перегрузке
Если:
producer > backend
есть только 4 варианта:
Рост очередей: растёт используемая память за счет накопления данных
Блокировка и сброс данных в sink-и: async теряет смысл
Отбрасывание сообщений: теряется информация (случайные пропуски)
Перезапись старых сообщений: теряется информация (случайные пропуски)
Главный тезис
Async logging не убирает перегрузку — он определяет, как она обрабатывается
Почему async formatting переоценён
Он не уменьшает работу. Он её переносит.
Что происходит
producer легче
backend тяжелее
Если backend уже bottleneck — станет хуже.
Реальная польза
уменьшение latency горячего пути
Когда это имеет смысл
очень частые лог-вызовы
много потоков
простые аргументы
burst нагрузка
Когда нет
медленные sinks
сложные объекты
постоянная перегрузка
Сложность и новые баги
Async логирование меняет модель.
Синхронно
“что залогировал — то и увидел”
Асинхронно
нужно учитывать:
lifetime
копирование
сериализацию
timing
Новые проблемы
dangling references
испорченные строки
гонки
нестабильные логи
Почему часто выбирают проще
На практике многие приходят к:
синхронное форматирование + асинхронную запись
Потому что:
проще
безопаснее
предсказуемее
Реальные библиотеки
Рассмотрим три популярных логгера:
spdlog — https://github.com/gabime/spdlog
Quill — https://github.com/odygrd/quill
logme — https://github.com/efmsoft/logme
Что показывают бенчмарки
Использовался проект:
👉 https://github.com/efmsoft/logbench
Неочевидный результат
В синхронном режиме:
spdlog
logme
показывают очень высокую производительность, иногда выше async.
Что это значит
базовые операции (форматирование + запись) уже очень быстрые
Async добавляет
очереди
синхронизацию
память
потоки
стоимость = базовая + накладные расходы async
Ключевой вывод
Async logging не ускоряет логирование — он перераспределяет нагрузку
Quill как пример
Quill — fully async дизайн:
очередь на поток
один backend
политики block/drop
При перегрузке
заполняется backend buffer
backend перестаёт читать очереди
заполняются frontend очереди
дальше block или drop
Вывод
Quill не “решает” перегрузку.
Он делает её управляемой.
Итог
async снижает latency, но не уменьшает работу
throughput ограничен sinks
очереди откладывают проблему
deferred formatting требует копирования
один backend поток — предел
Финальная мысль
Асинхронное логирование — это не про “сделать быстрее”.
Это про выбор: где и как платить за логирование
