Содержание урока · 12 разделов
- zap sampling: как устроен сэмплер
- Пример 100/100: что уже включено в проде
- Параметры NewSamplerWithOptions
- Зачем: шумные ретраи и циклы
- Риски: пропустили уникальную ошибку
- Читаем прореженный поток
- SamplerHook: метрики дропов
- Сэмплирование против других рулей
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пятница, 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, она же даёт опции вроде хука решений, о нём ниже.
Сэмплер на ручной сборке
GObase := 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 — тишина после first | 100 |
| 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 — хук из конфига получает те же решения, если вы живёте на пресете, а не на ручной сборке.
Счётчик отброшенных записей
GOvar 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, отдельное ядро для ошибок, метрики дропов через хук. Отклоняйтесь от этого по факту нагрузки, а не на всякий случай: лишние ручки — лишние сюрпризы.
Соберите сэмплированный конфиг
Восстановите обёртку сэмплера и поле конфига.
Соберите сэмплированный логгер
Расставьте строки: от базового ядра до первой записи.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: сэмплирование
Что дальше по курсу#
Почему прореживание нужно именно 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 со счётчиком дропов и проверьте, что он совпал с вашими подсчётами. Напишите три потока вашего сервиса: какой из них вы оставите без сэмплирования и почему.
Личные заметкиПусто
Вопросы и ответы по уроку
Сэмплирование искажает отладку?
Да, и об этом надо помнить: прореженный поток показывает форму шторма, а не каждую запись. Для расследования конкретного инцидента поднимайте уровень подробности динамически через AtomicLevel, а не отменяйте сэмплирование: одно окно подробных логов дешевле, чем тонущий диск.
Чем Initial отличается от Thereafter?
Initial — сколько записей проходят целиком в начале каждого окна; Thereafter — шаг прореживания после этого: каждая M-я. Пара 100/100 означает сто записей без купюр и далее каждую сотую; Thereafter: 0 — ничего после первых Initial.
Сэмплирует ли сэмплер по разным сообщениям отдельно?
Да: счётчики ведутся по паре уровень и сообщение. Десять разных текстов ошибок — десять независимых счётчиков, каждый получит свои Initial записей. Именно поэтому подробные сообщения вместо общих «failed» сохраняют уникальные события.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси