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

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

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

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

Sampling: прореживание потока

Sampling в zerolog — это семплер между уровнем и writer-ом: событие прошло порог, и семплер решает, доехать ли ему до записи. Разберём BurstSampler с всплеском и окном, BasicSampler с каждым N-м, RandomSampler, LevelSampler для debug-пилы и случай, когда семплер съедает ваши алерты.

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

Цикл, который пишет Debug-строку на каждую итерацию по миллиону элементов, отключать жалко, а оставлять страшно: диск умирает за часы, а система сбора тонет в ленте, где девяносто девять строк из ста не читал никто. Ручное решение — счётчик и условие — расползается по коду и забывается в рефакторингах. В zerolog эта задача вынесена в отдельный слой: семплер. Он встаёт между порогом уровня и writer-ом и решает для каждого события, доехать ли до записи. Sampling логов — не про экономию наносекунд, а про экономию объёма: прореженный поток остаётся репрезентативным, всплеск целиком попадает в ленту, а хвост режется по правилу, которое видно в конфигурации, а не в коде. В этом уроке — все встроенные семплеры: детерминированный, случайный, всплесковый и поуровневый, плюс правила, при которых прореживание не превращается в слепую зону.

Sampling в zerolog: интерфейс Sampler#

Семплер — это интерфейс Sampler с одним методом Sample(lvl Level) bool: вернуть true — событие пишется, false — отбрасывается. Вешается он на копию логгера методом Sample: sampled := base.Sample(&zerolog.BasicSampler{N: 10}) — и дальше все события этой копии проходят через семплер. Интерфейс экспортирован не зря: свой семплер — это один метод Sample(lvl Level) bool, и правило вроде «прореживать всё, кроме Warn и Error», пишется четырьмя строками, если встроенных не хватило. Важно понимать, где он стоит: метод should сначала проверяет порог локального уровня и глобальный SetGlobalLevel из урока про уровни, и только потом вызывает семплер — то есть прореживаются только те события, которые уже прошли порог. Уровень при этом передаётся в Sample первым аргументом, и поуровневый семплер из середины урока именно им пользуется. Счётчики детерминированных семплеров — BasicSampler и BurstSampler — крутятся на атомарных операциях, поэтому одна копия логгера с семплером безопасна для сотен горутин: счётчик общий, а не у каждого воркера. RandomSampler кладёт монетку через глобальный генератор случайных чисел — тоже потокобезопасно, но счётчика у него нет вовсе. И не забывайте про value-семантику: Sample возвращает новую копию, base продолжает писать без прореживания — включать и выключать семплер можно на отдельных ветках дерева логгеров. Порядок проверок в should при этом стоит держать в голове при отладке: если события пропали после подъёма глобального порога, семплер ни при чём — до него очередь не дошла.

BasicSampler: каждый десятый

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

func main() {
        base := zerolog.New(os.Stdout).With().Timestamp().Logger()

        sampled := base.Sample(&zerolog.BasicSampler{N: 10})

        for i := 1; i <= 25; i++ {
                sampled.Info().Int("i", i).Msg("heartbeat")
        }
}

// выйдут записи i=1, i=11, i=21 — каждая десятая

BasicSampler с полем N пропускает каждое N-е событие по атомарному счётчику: сработало условие счётчик%N==1 — запись ушла. N=0 отбрасывает всё, N=1 пропускает всё. Счётчик общий для логгера, поэтому при параллельных вызовах «каждый десятый» соблюдается глобально, а не по горутине.

BasicSampler и RandomSampler: детерминированно или случайно#

Два простых семплера отвечают на два разных вопроса. BasicSampler — детерминированный: каждый N-й, ровно и предсказуемо; удобно, когда важна равномерность потока — heartbeat-ы, статистика цикла. RandomSampler — вероятностный: zerolog.RandomSampler(chance) — преобразование uint32-а в семплер, который пропускает событие с шансом один из chance; никаких счётчиков и равномерности — чистая монетка. В пакете есть готовые ярлыки: zerolog.Often, zerolog.Sometimes и zerolog.Rarely — это RandomSampler(10), RandomSampler(100) и RandomSampler(1000) соответственно. Имена честные до прямолинейности: Often пропускает примерно каждый десятый, Rarely — примерно один из тысячи; док-комментарии пишут «~» не зря — вероятность не обещает ровности. Выбор между ними — вопрос следствия ошибки выборки: детерминированный семплер на периодическом сигнале может войти в резонанс и всегда попадать на одну и ту же фазу цикла; случайный резонансом не болеет, но при малом шансе короткий всплеск может пролететь мимо ленты целиком. Для мониторинга частоты событий детерминированный честнее — объём предсказуем; для генерации уникальных сэмплов отладки — случайный живее. Оба не смотрят на уровень: что прошло порог, то и прореживается — к последствиям этого мы вернёмся в «живом месте».

BurstSampler: всплеск пропустить, хвост отрезать#

BurstSampler — самый полезный из семьи: он пропускает первые Burst событий за окно Period, а дальше передаёт решение следующему семплеру NextSampler. Структура с тремя полями: Burst uint32, Period time.Duration, NextSampler Sampler — и собирается литералом: &zerolog.BurstSampler{Burst: 5, Period: time.Minute, NextSampler: nil}. Окно фиксированное, не скользящее: при первом событии после истечения Period счётчик обнуляется, и начало окна переносится на этот момент — «пять событий в минуту» считается от начала текущего окна, а сброс выполняется атомарным сравнением-и-заменой, без блокировок. NextSampler — место для второго правила: nil означает «после всплеска всё отбрасывать», а &zerolog.BasicSampler{N: 100} — «после всплеска писать одну из ста». Двухступенчатая схема покрывает классику: всплеск при старте или деградации попадает в ленту целиком — вы видите все первые ошибки, — а устойчивый поток прореживается, и диск живёт. Нюанс, о котором мало кто читает док-комментарий: всплеск срабатывает только когда и Burst, и Period больше нуля; при Period 0 семплер сразу передаёт решение NextSampler, а без него — отбрасывает всё. Проверяйте конфигурацию двумя событиями рядом, прежде чем доверять ей прод. Док-комментарий обещает ровно это: «BurstSampler lets Burst events pass per Period then pass the decision to NextSampler. If Sampler is not set, all subsequent events are rejected» — источник правды, когда конфигурация разошлась с поведением.

LevelSampler: проредить только debug

GO
sampled := log.Sample(zerolog.LevelSampler{
        DebugSampler: &zerolog.BurstSampler{
                Burst:       5,
                Period:      1 * time.Second,
                NextSampler: &zerolog.BasicSampler{N: 100},
        },
})

sampled.Debug().Msg("hello world")

LevelSampler держит по семплеру на уровень: поля TraceSampler, DebugSampler, InfoSampler, WarnSampler и ErrorSampler. Здесь прорежен только debug — пять событий в секунду, дальше одна из сотен, — а Info, Warn и Error проходят без семплера. Это пример из README zerolog и готовый рецепт для debug-пилы в цикле: всплеск виден, объём под контролем.

Sampling или Disabled: выбор по ситуации#

Три инструмента объёма решают разные задачи, и путать их дорого. Disabled — когда события не нужны вообще: известный горячий путь, debug-трассы в проде — нулевая цена и нулевой объём из урока про производительность. Sampling — когда события нужны, но не все: репрезентативная выборка статистики, всплески при деградации, шумные ветки, чей код вы не контролируете. Порог уровня — когда не нужен целый класс: Debug выключен глобально. Семплер выбирают, когда класс нужен, а частота — нет. У LevelSampler нет полей для Fatal и Panic: не заданный явно семплер уровня возвращает true, поэтому самые срочные события проходят даже поверх настроек прореживания. Стоп-кран для всего этого — zerolog.DisableSampling(true): глобальный переключатель, который при разборе инцидента выключает все семплеры во всех логгерах разом, и вы на время видите полный поток без переконфигурации; вернуть как было — одной строкой. Расписывайте семплеры в конфигурации рядом с уровнями из урока про окружение: «debug — всплеск 5 в секунду, дальше 1 из 100» — это читаемое правило, которое ревьюер понимает без исходников. И помните про value-семантику: Sample возвращает копию, поэтому семплер вешается на ветку — на модуль, на воркер, а не обязательно на весь сервис. Копии при этом делят один семплер-указатель: несколько веток, собранных из одной Sample-копии, считают общее окно — иногда это то, что нужно, а иногда сюрприз, который виден только под нагрузкой.

Правила sampling одним списком#

  • порог уровня срабатывает раньше семплера: прореживается только то, что прошло порог
  • ошибки, Warn и аудит не прореживаются: LevelSampler — для debug и Info
  • BurstSampler{Burst: 5, Period: time.Minute} — всплеск целиком, дальше NextSampler
  • NextSampler nil — после всплеска всё отбрасывается; BasicSampler{N: 100} — хвост 1 из 100
  • RandomSampler(chance) — шанс 1 из chance; Often, Sometimes, Rarely — 10, 100, 1000
  • zerolog.DisableSampling(true) — стоп-кран всех семплеров на время инцидента
  • счётчики семплеров атомарные: одна копия с семплером безопасна для горутин
  • Sample возвращает копию: семплер живёт на ветке дерева логгеров, не обязательно на base

Встроенные семплеры zerolog#

СемплерНастройкаПоведение
BasicSamplerN uint32каждый N-й; N=0 — ничего, N=1 — всё
RandomSamplerchance uint32шанс 1 из chance; Often, Sometimes, Rarely
BurstSamplerBurst, Period, NextSamplerвсплеск Burst за Period, потом NextSampler
LevelSamplerсемплер на каждый уровеньпрореживает только заданные уровни
DisableSampling(true)глобальный переключательвыключает семплеры у всех логгеров

Прореженный поток меняет то, что видит система сбора, — и об этом стоит подумать до, а не после. Объём записей становится неравномерным: всплеск дал двадцать строк, дальше минута тишины — графики частоты по количеству записей начинают врать, потому что частота событий и частота записей больше не совпадают. Если по логам что-то считается, считайте по данным внутри записей — счётчик в поле, а не COUNT(*) по ленте; там, где логи переходят в метрики, это правило обсуждается в уроке про логи и метрики. Оповещение по прореженному уровню — всегда компромисс: всплеск видно, устойчивый поток — только выборкой. Если алерт завязан на конкретный уровень, держите для него копию без Sample: разделение логгеров стоит одну строку на старте, а потерянная волна ошибок — разбор с фразой «в ленте этого нет». И последнее: прореживание — не замена уровням. Лента, где debug прорежен 1 из 100, а error не проречен вовсе, читается лучше, чем лента с единым семплером на всё; первым правилом настройки должен быть LevelSampler, и только потом — всплески и случайность для остальных уровней, если без них не обойтись.

Проверка: порядок проверок

Что проверяется раньше — порог уровня или семплер?

Проверка: только debug

Как проредить только debug, не трогая ошибки?

Сколько записей пройдёт BasicSampler?

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

sampled := base.Sample(&zerolog.BasicSampler{N: 10})

for i := 1; i <= 25; i++ {
        sampled.Info().Int("i", i).Msg("heartbeat")
}

Сколько пропустит BurstSampler?

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

sampled := base.Sample(&zerolog.BurstSampler{
        Burst:  2,
        Period: time.Minute,
})

for i := 0; i < 5; i++ {
        sampled.Info().Int("i", i).Msg("spike")
}

Соберите всплесковый семплер

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

func main() { base := zerolog.New(os.Stdout).With().Timestamp().Logger() sampled := base.(&zerolog.{Burst: 5, Period: time.Minute}) sampled.().Msg("heartbeat") }

Программа с семплером

Расставьте строки в правильном порядке: импорты, base, копия с семплером и запись.

Строки

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

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

Карточки: sampling

Термин

Sampler

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

Интерфейс zerolog с методом Sample(lvl Level) bool: true — событие пишется, false — отбрасывается. Вызывается после проверки порога уровня.

1 / 6

Что дальше по курсу#

Прореживание — последний инструмент объёма до продакшена: дальше курс идёт в горутины — что происходит, когда сотня воркеров пишет в один логгер, — и затем в конфигурацию по окружению, где уровни и семплеры собираются из переменных окружения. Справочное закрепление: найдите в своём сервисе самый шумный цикл, повесьте на его модуль LevelSampler с BurstSampler для debug и сравните объём ленты до и после — правило «всплеск 5 в секунду, дальше 1 из 100» обычно сокращает debug-объём на два порядка без потери всплесков деградации.

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

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

Вывод

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

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

  • Семплер встаёт после порога уровня: прореживается только то, что прошло порог.
  • BasicSampler{N} — каждый N-й по атомарному счётчику; RandomSampler(chance) — шанс 1 из chance.
  • BurstSampler{Burst, Period, NextSampler} — всплеск целиком, хвост по правилу NextSampler.
  • NextSampler nil отбрасывает всё после всплеска; BasicSampler{N: 100} — хвост 1 из 100.
  • LevelSampler прореживает заданные уровни: ошибки и Warn не трогает никогда.
  • zerolog.DisableSampling(true) — стоп-кран всех семплеров на время инцидента.
  • Счётчики атомарные: копия логгера с семплером безопасна для сотен горутин.
  • Прореженный поток ломает графики частоты: считайте по полям, а не по строкам.

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

Соберите копию логгера с BasicSampler{N: 5} и выведите двадцать записей: по терминалу посчитайте, что прошло ровно четыре — первая и каждая пятая. Замените на BurstSampler{Burst: 3, Period: time.Minute, NextSampler: &zerolog.BasicSampler{N: 2}} и проверьте двухступенчатую схему. Добавьте LevelSampler с DebugSampler и убедитесь, что Error-запись доезжает всегда, сколько бы ни было debug-событий рядом. В середине эксперимента вызовите zerolog.DisableSampling(true) и посмотрите на полный поток. Наконец, сломайте конфигурацию — BurstSampler без Period — и запишите своими словами, почему хвост исчез целиком.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Sampling: прореживание потока)

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

Как проредить debug-лог в zerolog?

LevelSampler с DebugSampler: log.Sample(zerolog.LevelSampler{DebugSampler: &zerolog.BurstSampler{Burst: 5, Period: 1 * time.Second, NextSampler: &zerolog.BasicSampler{N: 100}}}). Debug получает всплеск и хвост 1 из 100, Info, Warn и Error проходят без семплера. Это официальный рецепт из README zerolog. Числа 5 и 100 — отправная точка: подгоняйте их под бюджет диска и типичную длину всплеска.

Чем BurstSampler отличается от BasicSampler?

BasicSampler пишет каждый N-й событие без оглядки на время — поток равномерный. BurstSampler работает окнами: первые Burst событий за Period проходят целиком — видно деградацию в моменте, — а дальше решает NextSampler. Для шумных веток с всплесками берите BurstSampler, для ровной статистики — BasicSampler.

Может ли семплер отбросить Error-запись?

Да, если семплер висит на базовом логгере: BasicSampler и BurstSampler прореживают всё, что прошло порог уровня, включая ошибки. Правильная настройка — LevelSampler: семплер только на DebugSampler или InfoSampler, а WarnSampler и ErrorSampler остаются пустыми, и ошибки доезжают всегда.

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

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

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

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

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила