Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~18 мин +20 XP

Производительность: Enabled и горячий путь

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

Содержание урока · 9 разделов

Лог стоит сервису дважды: на записанных строках — сборка Record и сериализация обработчиком, и на отброшенных — вычисление аргументов, которые никто не увидит. Вторая часть обычно удивляет сильнее: вызов Debug с дорогим аргументом на проде с уровнем Info платит за сборку аргумента на каждой итерации, хотя запись летит в корзину. Вопрос производительности лога поэтому не звучит как «быстрый ли slog» — он звучит как «сколько стоит мой вызов с моими полями и моим обработчиком». Ответ складывается из трёх частей: проверка Enabled до сборки дорогих полей, экономные атрибуты вместо структур в Any и честный бенчмарк своего пути. Ни одна из них не требует трюков: это дисциплина вызовов, а не борьба с библиотекой. Разберём каждую часть, посмотрим на ловушки вроде fmt.Sprintf в аргументах и соберём методику измерения, которую можно повторить на своём сервисе. Побочный эффект этой дисциплины — лог читается лучше: меньше склеек, меньше мусорных полей, каждая запись отвечает на свой вопрос.

Slog производительность: Enabled до сборки полей#

Разбор slog производительность начинается с отброшенной записи. Аргументы вызова вычисляются до того, как Logger спросит Handler, нужен ли ему этот уровень, — поэтому logger.Debug("состояние", "dump", buildDump(state)) соберёт дамп на каждой итерации даже там, где Debug отключён. Лечится это плоской проверкой: logger.Enabled(ctx, slog.LevelDebug) возвращает решение обработчика за одно сравнение, и дорогую сборку прячут под неё. Ставить проверку стоит там, где аргументы дорогие: дампы состояния, сериализация структур, запрос счётчиков. Дешёвые пары вида «user_id», id обёртывать не нужно — цена их вычисления сравнима с ценой самой проверки, и guard размажет по коду больше шума, чем сэкономит наносекунд. Отдельный приём — LogValuer из девятого урока: отложенное значение собирается только при записи, и отброшенный вызов его не вызывает; там, где поле дорогое и повторяется во многих местах, обёртка с LogValue чище, чем if на каждом вызове.

Guard на горячем пути

GO
logger := slog.New(slog.NewJSONHandler(os.Stdout,
        &slog.HandlerOptions{Level: slog.LevelInfo}))

// дорогое поле без guard: дамп собирается и летит в корзину
logger.Debug("состояние", "dump", buildDump(state))

// с guard: buildDump вызывается только при включённом Debug
if logger.Enabled(context.Background(), slog.LevelDebug) {
        logger.Debug("состояние", "dump", buildDump(state))
}

Первый вызов платит за buildDump при любом пороге уровня: аргументы вычисляются до проверки. Второй спрашивает обработчик и собирает дамп только тогда, когда запись действительно уйдёт. Порог Info здесь для примера — в стенде он часто стоит на Debug, и guard становится прозрачным.

Цена Attr: типизированные конструкторы против Any#

Значение в slog хранится в Value вместе с типовым kind: slog.Int кладёт число как int64, slog.String — строку, и встроенные обработчики печатают их напрямую, без отражения. Пара с произвольным значением идёт через Any: для структур и срезов это интерфейс и marshalling через отражение в JSONHandler — аллокации и время на каждую записанную строку. Самый частый расход — fmt.Sprintf в аргументах: строка склеивается до проверки уровня, даже если запись отбросят, и это тот случай, когда «бесплатный Debug» стоит как настоящий. Строку, без которой не обойтись, собирают внутри guard'а или выносят в поле отдельным вызовом. Константные атрибуты вроде component=api выносят в переменные пакета: Attr — иммутабельное значение, безопасное для горутин из тринадцатого урока, и переиспользование ничего не стоит. Группы и вложенные поля собирают один раз при старте логгера — на горячем пути они не добавляют скорости, только структуру.

Бенчмарк своего вызова

GO
func BenchmarkInfo(b *testing.B) {
        h := slog.NewJSONHandler(io.Discard,
                &slog.HandlerOptions{Level: slog.LevelInfo})
        logger := slog.New(h)

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                logger.Info("горячий путь", "user_id", slog.Int(42), "n", i)
        }
}

io.Discard поглощает записи без работы с диском — измеряется именно цена вызова и сериализации, а не writer. ReportAllocs показывает аллокации на вызов: их не ноль даже у чистого пути, и резкий рост после правки — сигнал. Так же меряют отброшенный вызов: замените Info на Debug и увидите цену аргументов без записи.

Бенчмарки: измеряйте свой вызов#

Числа из чужих таблиц сравнения логгеров к вашему случаю отношения не имеют: цена вызова зависит от полей, их типов, обработчика и writer. Методика простая и повторяемая: свой обработчик с вашим порогом уровня, свои поля в том сочетании, как они пишутся на горячем пути, io.Discard вместо внешнего мира. Отдельно меряют записанный вызов и отброшенный — второе число отвечает на вопрос, сколько стоит забытый Debug с дорогим аргументом. Бенчмарк отвечает «сколько», но не «где болит»: место лога в общей нагрузке покажет профиль сервиса — иногда наносекунды на вызов теряются в цене HTTP-запроса, а иногда цикл по миллиону элементов дышит логом. Для честного сравнения с zap измеряют ту же комбинацию полей на их логгере — и помнят, что у zap есть sampling, а у встроенных обработчиков slog его нет; при шторме записей помогает уровень и дисциплина полей, а не надежда на фильтр. Полезное соотношение — между двумя своими измерениями: во сколько раз отброшенный вызов дешевле записанного, настолько и тяжёлой была бы забытая отладка на проде.

Источники расхода и что с ними делать#

ИсточникКогда платимЧто делать
аргументы до проверки уровняна каждом вызове, даже отброшенномEnabled-guard или LogValuer для дорогих полей
Any со структуройна каждой записанной строкераскладывать на типизированные поля заранее
fmt.Sprintf в аргументахна каждом вызове, даже отброшенномсобирать строку под guard'ом или передавать поля
сериализация Handlerна каждой записанной строкеменьше полей, типизированные конструкторы
writer и сетьна каждой записанной строкебыстрый writer, буферизация с Flush, сбор на стороне

Быстрый логгер go: дисциплина вместо трюков#

Собрать быстрый логгер go можно без единой экзотики, на четырёх правилах дисциплины. Первое: уровни по окружению — Info в проде, Debug в стенде; раскладку по переменным окружения даёт восемнадцатый урок, и это самый крупный рычаг: половина записей исчезает без единой строчки кода. Второе: дорогие значения — под Enabled или в LogValuer, чтобы отброшенная запись не платила за сборку. Третье: лог в цикле по элементам — антипаттерн из двадцать третьего урока; агрегируйте итог или логируйте выборочно, иначе даже дешёвый вызов в миллион итераций станет статьёй расходов. Четвёртое: ключи и константные атрибуты — константами пакета, а не строками по месту. Мьютекс обработчика из тринадцатого урока в этой картине не виноват: его цена на фоне сборки записи несущественна, и разъезд логгеров по горутинам её не уберёт, зато потеряет поля.

Как найти места, где guard нужен на самом деле. Первый шаг — профиль: pprof по нагруженному сценарию покажет, дышит ли лог в кадре вообще. Второй — просмотр циклов: вызов записи внутри for по элементам — кандидат на агрегацию или выборочную запись, особенно если внутри вызова склейка строк или сбор структуры. Третий — поиск по fmt.Sprintf рядом с вызовами лога: каждая такая строка платит за склейку до проверки уровня. После правок бенчмарк из урока фиксирует результат: наносекунды и аллокации на вызов до и после. И не забывайте направление: оптимизируют измеренное и профилированное, а не то, что выглядит медленным в коде — лог в HTTP-хендлере тонет в цене запроса, тот же лог в цикле по миллиону позиций определяет профиль целиком. Последняя проверка перед коммитом — глазами: guard вокруг дешёвой пары выглядит как забота, а читает его следующий разработчик, который не видел ваш профиль.

Правила горячего пути#

  • Дорогие аргументы прячут за Enabled или LogValuer: отброшенная запись не платит за сборку.
  • Дешёвые пары guard'ом не обкладывают: цена проверки сопоставима с ценой вычисления.
  • Структуры и срезы в Any раскладывают на типизированные поля до места записи.
  • fmt.Sprintf в аргументах убирают: склейка платит до проверки уровня.
  • Лог внутри цикла по элементам агрегируют или делают выборочным.
  • Решение принимают по бенчмарку и профилю своего сервиса, а не по чужим таблицам.

Связи с другими уроками: девятый урок про LogValuer переносит сбор значения на момент записи, третий урок про уровни — база семантики, восемнадцатый — уровни по окружению. Контракт Enabled у своего обработчика описывает двенадцатый урок, цена мьютекса при конкурентной записи — тринадцатый, а лог в цикле как антипаттерн разбирает двадцать третий. Методика бенчмарков — в уроке про benchmarki курса по продвинутому Go, sampling как фильтр штормов — в уроке про sampling курса по zap. Карта курса — на странице курса.

Проверка: цена отброшенной записи

Уровень обработчика — Info. Какой вызов дороже всех на горячем пути?

Проверка: как измерять

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

Сколько раз вызовется buildDump?

Прочитайте код и выберите, что он выведет:

logger := slog.New(slog.NewJSONHandler(io.Discard,
        &slog.HandlerOptions{Level: slog.LevelInfo}))

for i := 0; i < 100; i++ {
        if logger.Enabled(context.Background(), slog.LevelDebug) {
                logger.Debug("состояние", "dump", buildDump(state))
        }
}

Соберите динамический уровень и guard

Допишите: изменяемый уровень, проверку на горячем пути, типизированный атрибут и отчёт аллокаций.

var level slog. level.Set(slog.LevelInfo) handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &level}) if logger.(ctx, slog.LevelDebug) { logger.Debug("состояние", "user_id", slog.(uid)) } func BenchmarkInfo(b *testing.B) { b.() }

Бенчмарк по строкам

Расставьте строки бенчмарка по порядку: функция, обработчик, логгер, аллокации, цикл, закрытие.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: производительность

Термин

цена отброшенной записи

нажмите, чтобы перевернуть
Определение

Вычисление аргументов: они считаются до проверки уровня, дампы платят даже в корзину.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Аргументы вычисляются до проверки уровня: отброшенный Debug платит за свою сборку.
  • Enabled-guard ставят только под дорогими полями: дампы, сериализация, счётчики.
  • LogValuer переносит сбор значения на момент записи и чистит горячий путь без if.
  • Типизированные конструкторы печатаются без отражения; структуры в Any — дорогие.
  • Бенчмарк со своим обработчиком и io.Discard даёт время и аллокации именно вашего вызова.
  • Быстрый логгер — дисциплина: уровни по окружению, поля без склеек, цикл без спама.

Домашнее задание

Соберите локально бенчмарк из урока со своим набором полей и прогоните его go test -bench: зафиксируйте наносекунды и аллокации. Затем добавьте дорогой аргумент через buildDump-подобную функцию и сравните три варианта: без guard, с Enabled и с LogValuer — разницу в отброшенных вызовах видно по бенчмарку.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Производительность: Enabled и горячий путь)

Вопросы и ответы по уроку

Насколько slog медленнее zap?

Ответ зависит от ваших полей и обработчика, поэтому честный путь — бенчмарк своего вызова на обоих логгерах. На типовых записях разница редко определяет профиль сервиса; при штормах записей zap спасает sampling, а slog — уровни по окружению и дисциплина полей.

Нужно ли оборачивать проверкой Enabled каждый вызов?

Нет: цена вычисления дешёвой пары сопоставима с ценой самой проверки, а код с guards на каждом шаге нечитаем. Проверку ставят под дорогими аргументами и в горячих циклах, которые показал профиль; остальное закрывают LogValuer и уровни по окружению.

Почему аргументы вычисляются до проверки уровня?

Go вычисляет аргументы функции в месте вызова, а slog.Debug — обычная функция: к моменту вызова dump уже собран. Механизм отложенных значений вынесен в LogValuer — обёртка отдаёт slog.Value, который обрабатчик раскроет только при записи.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

pgx25 уроков~12 чпрактика25 уроков уже доступны

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY