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

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

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

Урок 11 из 25 ~16 мин +50 XP

Сэмплирование: не тонем в логах

zap sampling — это прореживание потока логов: первые N записей за секунду проходят целиком, а дальше из сотен одинаковых остаётся каждая M-я. Сэмплирование логов спасает сервис, когда шумят ретраи, поллеры и циклы: объём падает на порядки, а картинка о проблеме сохраняется. Но у механики есть цена — пропущенная уникальная ошибка — и в этом уроке мы разберём и настройку, и риски, и места, где сэмплировать нельзя.

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

Пятница, 19:40. У третьего сервиса падает сеть до базы, и код с ретраями честно повторяет попытку каждые полсекунды, логируя каждый провал: error «dial tcp 10.0.0.5:5432: connect: connection refused». За минуту — сто двадцать тысяч записей, по содержанию одинаковых. Что произошло дальше, знакомо многим: сборщик логов захлебнулся, диск подобрался к лимиту, а алерт по ошибкам сработал сто двадцать раз подряд, и дежурный его замьютил. Ни одной новой информации после первой записи этот поток не принёс — но съел CPU на сериализации, I/O на записи и нервы на алертах. Правильный ответ — не писать все сто двадцать тысяч: первые записи пройти целиком, дальше прореживать. Это и есть сэмплирование: механизм, встроенный в zap, — и, что важно, уже включённый в production-пресете, о чём многие не догадываются.

zap sampling: как устроен сэмплер#

Сэмплирование логов в zapcore реализовано как обёртка-ядро: NewSamplerWithOptions(core, tick, first, thereafter) принимает ядро и возвращает ядро — поэтому сэмплер встанет и на ручную сборку из прошлого урока, и под пресет. Механика zap sampling — арифметика на счётчиках. Параметры: tick — длительность окна, обычно time.Second; first (Initial) — сколько записей проходят целиком в начале окна; thereafter (Thereafter) — какая по счёту из дальнейших пропускается: значение 100 означает «каждая сотая». Счётчик ведётся по паре уровень плюс сообщение: сто ошибок с текстом «connection refused» — это один счётчик, а «connection refused» и «timeout» — два разных. Ключевой момент: окно прокручивается каждые tick — в новую секунду счётчики обнуляются и снова идут первые N записей. Устаревшая функция NewSampler до сих пор в пакете, но помечена Deprecated: пишите NewSamplerWithOptions, она же даёт опции вроде хука решений, о нём ниже.

Сэмплер на ручной сборке

GO
base := zapcore.NewCore(
        zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
        zapcore.AddSync(os.Stdout),
        zapcore.InfoLevel,
)

sampled := zapcore.NewSamplerWithOptions(base, time.Second, 100, 100)

logger := zap.New(sampled)
defer logger.Sync()

for i := 0; i < 500; i++ {
        logger.Error("connection refused", zap.Int("attempt", i))
}

Базовое ядро то же, что в уроке про zapcore; NewSamplerWithOptions оборачивает его. За одну секунду пятьсот одинаковых ошибок превратятся в сто четыре записи: первые сто целиком, затем каждая сотая. Поля у записей сохраняются — экономится запись, а не формат. Пример рассчитан на локальный Go: в песочнице zap не исполнить.

Пример 100/100: что уже включено в проде#

Пара сто и сто — не случайность: ровно столько живёт в production-пресете. В zap.Config за это отвечает поле Sampling: указатель на zap.SamplingConfig с полями Initial и Thereafter; пресет NewProductionConfig выставляет Initial: 100, Thereafter: 100 при окне в одну секунду. Считается это так: в течение секунды первые сто записей с одинаковым уровнем и сообщением уходят как есть; далее zap держит счётчик и пропускает каждую сотую; в новую секунду всё заново. Практический вывод: если ваш сервис штормит, то есть пишет тысячи одинаковых записей в секунду, вы и так под сэмплером — просто не знали об этом. И обратный вывод: при спокойном трафике сэмплирование не включается вовсе, пока счётчик не дойдёт до сотни за окно; обычные сервисы с десятками записей в секунду живут без прореживания. Отключить сэмплер в конфиге просто — присвойте Sampling nil, но подумайте дважды: ваш сборщик логов скажет спасибо в следующий шторм.

Параметры NewSamplerWithOptions#

ПараметрСмыслТиповое значение
coreоборачиваемое ядро — энкодер, приёмник, уровеньрезультат NewCore
tickдлительность окна, после которой счётчики обнуляютсяtime.Second
first (Initial)сколько записей проходят целиком в начале окна100
thereafter (Thereafter)каждая M-я из дальнейших пропускается; 0 — тишина после first100
optsопции: SamplerHook для метрик решений—

Зачем: шумные ретраи и циклы#

Сэмплирование нужно там, где одна и та же запись воспроизводится в цикле, и таких мест в реальных сервисах несколько. Ретраи исходящих вызовов — хрестоматийный случай из начала урока: каждая попытка логирует провал, и при деградации зависимости частота провалов равна частоте попыток. Фоновые поллеры: очередь пуста, крон тикает раз в секунду, Info «nothing to process» капает круглосуточно. Валидаторы и парсеры в цикле по данным: один кривой файл на миллион строк превращается в миллион записей. Во всех этих случаях после сотни одинаковых записей новой информации нет — есть только стоимость: CPU на кодирование, диск и сеть на доставку, квоты в системе сбора. Сэмплирование режет именно это, сохраняя сигнал: начало шторма видно с первой записи, конец — по последней, а середина представлена десятой долей процента. Заметьте честную оговорку документации: реализация оптимизирована под скорость, а не под точность, — под нагрузкой окно может пропустить чуть больше или меньше расчётного. Для защиты от шторма это неважно, для бухгалтерии по записям — учитывайте.

Проверка: арифметика сэмплера

Сэмплер NewSamplerWithOptions(core, time.Second, 100, 100). За одну секунду пришло 500 записей с одинаковым уровнем и сообщением. Сколько из них будет записано?

Риски: пропустили уникальную ошибку#

Теперь неприятная часть, о которой молчат туториалы. Счётчик сэмплера — по уровню и тексту сообщения, и поля в этот ключ не входят. Ошибка «payment failed» с полем order_id равным «a-77» и ошибка «payment failed» с order_id «b-42» — один и тот же счётчик: детали живут в полях, а считается текст. Сценарий провала: за секунду проходит тысяча падений платежей с сотней разных order_id в полях, первые сто записываются, а ваш конкретный заказ c-99 выпадает из логов в числе прореженных — и расследование упирается в дыру. Лечение на уровне конвенций: в сообщении несут то, что отличает запись — «payment failed for order c-99» — и тогда уникальные события получают свой счётчик; либо дублируют критичные события в метрики, где потерь нет. Второй риск — искажение объёма: в логе сто ошибок, кажется, что шторм небольшой, а реально было сорок тысяч; сравните счётчик алертов с числом записей. Третий — арифметика: thereafter 100 при Initial 10 означает, что между сотыми пропускается девяносто из ста, — настраивайте оба числа осознанно.

Читаем прореженный поток#

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

SamplerHook: метрики дропов#

Одна из опций NewSamplerWithOptions закрывает вопрос «а сколько мы резали»: zapcore.SamplerHook принимает функцию, которую сэмплер вызывает на каждое своё решение. Аргументы — запись и битовая маска решения: LogDropped означает, что запись отброшена, LogSampled — что пропущена как пробная. Внутри функции удобно инкрементировать атомарный счётчик и отдавать его в метрики. Это лечит второй риск из предыдущего раздела: разница между «сэмплер пропустил 104» и «шторм был на 40 тысяч» становится видимой в дашборде, а не догадкой по логам. Заведите две метрики — дропнутые и прошедшие — по уровням, и сэмплирование перестанет быть чёрным ящиком: видно, когда оно срабатывает, на каком уровне и насколько агрессивно. Для того же предназначено поле Hook в zap.SamplingConfig — хук из конфига получает те же решения, если вы живёте на пресете, а не на ручной сборке.

Счётчик отброшенных записей

GO
var dropped atomic.Int64

sampled := zapcore.NewSamplerWithOptions(
        base,
        time.Second,
        100, 100,
        zapcore.SamplerHook(func(ent zapcore.Entry, dec zapcore.SamplingDecision) {
                if dec&zapcore.LogDropped > 0 {
                        dropped.Add(1)
                }
        }),
)

logger := zap.New(sampled)
defer logger.Sync()

Хук вызывается на каждое решение сэмплера; битовая маска проверяется оператором & — бит LogDropped означает отброшенную запись, LogSampled — пропущенную пробную. Счётчик atomic.Int64 безопасно растёт из горутин, а его значение уходит в экспортер метрик раз в несколько секунд. Документация zap прямо предлагает хук как способ наблюдать за производительностью сэмплера — у дежурного появляется честный ответ на вопрос, сколько записей зарезали сегодня.

Thereafter: 0 — что останется?

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

sampled := zapcore.NewSamplerWithOptions(base, time.Second, 50, 0)

// за одну секунду — 200 записей с одинаковым уровнем и сообщением

Проверка: где сэмплеру нет места

Какой поток логов НЕЛЬЗЯ отдавать под сэмплирование?

Сэмплирование против других рулей#

Сэмплер — не единственная ручка объёма, и полезно видеть иерархию. Уровень — грубая регулировка: подняли порог до Warn — Info исчез целиком; дешево, но грубо, и динамика уровня — тема отдельного урока про AtomicLevel. Сэмплирование — точечная: уровень сохраняется, прореживается только повторяющееся; это способ жить с Debug-подробностью в Info-штормах. Ротация из урока о lumberjack вообще не про объём: она делит поток на файлы, суммарный объём не уменьшая. Ретраи и очередь в самом коде — правильное место, чтобы не логировать каждую попытку: логируйте первую, последнюю и каждую десятую попытку руками, тогда сэмплеру останется только непредвиденное. Правильная конфигурация прод-сервиса обычно такова: прод-уровень Info, сэмплирование пресетное 100/100, отдельное ядро для ошибок, метрики дропов через хук. Отклоняйтесь от этого по факту нагрузки, а не на всякий случай: лишние ручки — лишние сюрпризы.

Соберите сэмплированный конфиг

Восстановите обёртку сэмплера и поле конфига.

sampled := zapcore.WithOptions(base, time.Second, 100, ) cfg := zap.NewProductionConfig() cfg. = &zap.SamplingConfig{Initial: 100, Thereafter: 100}

Соберите сэмплированный логгер

Расставьте строки: от базового ядра до первой записи.

Строки

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

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

Карточки: сэмплирование

Термин

Сэмплирование

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

Прореживание повторяющихся записей: первые N за окно целиком, дальше каждая M-я. Обёртка-ядро в zapcore, в прод-пресете включена по умолчанию.

1 / 6

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

Почему прореживание нужно именно zap — то есть откуда берётся его скорость при тысячах записей в секунду, — разбирает урок про производительность: пулы, ноль аллокаций и CheckedEntry. Вторая ручка объёма — уровень, меняющийся без перезапуска, — живёт в уроке про AtomicLevel. Карта курса — на странице курса.

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

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

Вывод

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

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

  • Сэмплер — обёртка-ядро: NewSamplerWithOptions(core, tick, first, thereafter); NewSampler устарел.
  • Прод-пресет уже сэмплирует: Initial 100, Thereafter 100, окно в секунду; отключение — Sampling: nil.
  • Счётчик по уровню и тексту сообщения, поля не входят: уникальные детали прореживаются вместе с шумом.
  • Кандидаты на сэмплирование — ретраи, поллеры, циклы; где НЕЛЬЗЯ — аудит, биллинг, регуляторные события.
  • Thereafter: 0 — тишина после первых N; начальные записи всегда проходят, начало шторма видно.
  • SamplerHook считает дропы: без метрик решения сэмплера — чёрный ящик.
  • Сэмплирование точечно против грубого уровня и бесполезно против ротации: у каждой ручки своя работа.

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

Соберите локально логгер с сэмплером NewSamplerWithOptions(base, time.Second, 100, 100) и прогоните цикл из 5000 одинаковых Error за секунду — посчитайте по выводу, сколько прошло. Затем поменяйте thereafter на 0 и повторите. Добавьте SamplerHook со счётчиком дропов и проверьте, что он совпал с вашими подсчётами. Напишите три потока вашего сервиса: какой из них вы оставите без сэмплирования и почему.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Сэмплирование: не тонем в логах)

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

Сэмплирование искажает отладку?

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

Чем Initial отличается от Thereafter?

Initial — сколько записей проходят целиком в начале каждого окна; Thereafter — шаг прореживания после этого: каждая M-я. Пара 100/100 означает сто записей без купюр и далее каждую сотую; Thereafter: 0 — ничего после первых Initial.

Сэмплирует ли сэмплер по разным сообщениям отдельно?

Да: счётчики ведутся по паре уровень и сообщение. Десять разных текстов ошибок — десять независимых счётчиков, каждый получит свои Initial записей. Именно поэтому подробные сообщения вместо общих «failed» сохраняют уникальные события.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси