Асинхронное логирование давно считается “очевидной оптимизацией”: вынесли запись в отдельный поток — и всё стало быстрее.

Но если копнуть глубже, оказывается, что это не совсем так.

В предыдущей статье я разбирал производительность популярных 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 варианта:

  1. Рост очередей: растёт используемая память за счет накопления данных

  2. Блокировка и сброс данных в sink-и: async теряет смысл

  3. Отбрасывание сообщений: теряется информация (случайные пропуски)

  4. Перезапись старых сообщений: теряется информация (случайные пропуски)

Главный тезис

Async logging не убирает перегрузку — он определяет, как она обрабатывается

Почему async formatting переоценён

Он не уменьшает работу. Он её переносит.

Что происходит

  • producer легче

  • backend тяжелее

Если backend уже bottleneck — станет хуже.

Реальная польза

уменьшение latency горячего пути

Когда это имеет смысл

  • очень частые лог-вызовы

  • много потоков

  • простые аргументы

  • burst нагрузка

Когда нет

  • медленные sinks

  • сложные объекты

  • постоянная перегрузка

Сложность и новые баги

Async логирование меняет модель.

Синхронно

“что залогировал — то и увидел”

Асинхронно

нужно учитывать:

  • lifetime

  • копирование

  • сериализацию

  • timing

Новые проблемы

  • dangling references

  • испорченные строки

  • гонки

  • нестабильные логи

Почему часто выбирают проще

На практике многие приходят к:

синхронное форматирование + асинхронную запись

Потому что:

  • проще

  • безопаснее

  • предсказуемее

Реальные библиотеки

Рассмотрим три популярных логгера:

Что показывают бенчмарки

Использовался проект:

👉 https://github.com/efmsoft/logbench

Неочевидный результат

В синхронном режиме:

  • spdlog

  • logme

показывают очень высокую производительность, иногда выше async.

Что это значит

базовые операции (форматирование + запись) уже очень быстрые

Async добавляет

  • очереди

  • синхронизацию

  • память

  • потоки

стоимость = базовая + накладные расходы async

Ключевой вывод

Async logging не ускоряет логирование — он перераспределяет нагрузку

Quill как пример

Quill — fully async дизайн:

  • очередь на поток

  • один backend

  • политики block/drop

При перегрузке

  1. заполняется backend buffer

  2. backend перестаёт читать очереди

  3. заполняются frontend очереди

  4. дальше block или drop

Вывод

Quill не “решает” перегрузку.

Он делает её управляемой.

Итог

  • async снижает latency, но не уменьшает работу

  • throughput ограничен sinks

  • очереди откладывают проблему

  • deferred formatting требует копирования

  • один backend поток — предел

Финальная мысль

Асинхронное логирование — это не про “сделать быстрее”.

Это про выбор: где и как платить за логирование