AdTechSep 10, 202610 мин чтения

Измерение рекламы теряет события на пике: стыки, дублирующиеся ключи и джойн с биддером

Пайплайны событийИзмерение рекламыАтрибуцияЦелостность данных
Ошибка загрузки изображения

Ваш измерительный пайплайн теряет события на пике трафика, а цифры атрибуции никогда не сходятся с логами биддера. Это два отказа за одним симптомом: события, которые не дошли, на стыке, который никто не считал, и события, которые дошли и были посчитаны по другому правилу. Здесь — как строятся стыки, ключи, окно опозданий и мост сверки.

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

Обычная постановка — «пайплайн теряет события, значит, надо переписать пайплайн» — чинит в лучшем случае одну из них. Дальше эти две разделены и сказано, что даёт сверка, а даёт она не равенство. Дефолты, которые цитируются ниже, — Kafka на транспорте и ClickHouse на хранении.

Короткий ответ структурный: один идентификатор на аукцион, проносимый насквозь, счётчик по обе стороны каждого перехода и сверка по уже закрытому окну. Что amBrain может подтвердить публично — это работа в AdTech: разработка DSP, платформ real-time bidding и ad exchange. В любом RTB-стеке именно с этой стороны приходят записи о ставке, выигрыше и показе. Пайплайн ниже описан из механики задачи, а не из нашего кейса.

Один запрос отличает потерянное событие от опоздавшего

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

Второй — вообще не отказ. Сторона измерения считает событие, инициированное клиентом; биддер записывает результат аукциона на серверной стороне. Одно — присуждение, другое — наблюдение за тем, что с ним дальше случилось. Руководства MRC по измерению трактуют pre-fetch, pre-render и auto-refresh как отдельные вещи, которые нужно детектировать и раскрывать: это правило подсчёта, а не сбой транспорта. Отличить одно от другого стоит одного запроса и некоторого терпения.

  • Прогоните то же окно по времени события через час, через шесть часов и через полные сутки после времени, которое оно покрывает
  • Недостача, которая уменьшается с каждым прогоном, означает, что события опоздали, а не потерялись, и с транспортом всё в порядке
  • Считайте уникальные ключи дедупликации, а не строки: транспорт at-least-once гарантирует повторные доставки, и кривая по строкам прячет завышенный счёт
  • Недостача, которая не меняется, означает, что события потеряны, и вопрос в том, на каком стыке
  • Тесту нужны отметка времени события, идентификатор, переживающий повторы, и retention, достаточный, чтобы прогнать окно заново
  • Публикуйте кривую доуточнения графиком, по каждому типу событий, рядом с цифрой, которую она объясняет

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

События пропадают на именованных стыках, а стык, который не считают, обвинить нельзя

Есть семь мест, где рекламное событие создаётся, а потом тихо перестаёт существовать, плюс одна настройка, которая выглядит гарантией и ею не является.

  • Клиентский сбор: бикон срабатывает, но документ выгружается раньше, либо креатив был закеширован, отдан по pre-fetch или обновлён по auto-refresh и считает то, чего нет в записи аукциона
  • Приём на эдже: лимиты соединений, исчерпание keep-alive, перезапуски во время деплоев и опасный вариант — успех, возвращённый до того, как событие сохранено надёжно
  • Буфер продюсера: клиент блокируется на ограниченное время, потом бросает исключение, и код, который ловит ошибку и ничего не считает, — это место, где умирают данные
  • Сохранность на брокере: если подтверждение не требуется, ничто не гарантирует, что запись дошла; если хватает одного лидера, запись теряется при отказе этого лидера до того, как фолловеры отреплицируют
  • Подтверждение от всех реплик — это не сохранность: оно ждёт текущий in-sync набор, минимальный размер которого по умолчанию равен единице, поэтому пик, отбрасывающий фолловеров назад, коммитится на одном лидере
  • Консьюмер: коммит оффсета до обработки — это at-most-once, и это дефолт, а не решение: клиент коммитит по таймеру, пока это не выключили
  • Выход за retention: консьюмер, отставший дальше окна хранения, обнаруживает, что его следующий оффсет удалён, и политика сброса по умолчанию перебрасывает его в конец лога
  • Загрузка в колоночное хранилище: вставки fire-and-forget подтверждаются, как только попали в буфер, а зависимые материализованные представления дедуплицируются отдельной настройкой — здесь сырая таблица и отчёт расходятся

Правило здесь отчётное, а не инженерное: потеря относится к названному стыку либо не относится никуда. Тихие стыки держат на виду два аларма — отставание консьюмера, измеренное во времени относительно retention, и политика сброса, которая падает с ошибкой, а не прыгает.

Дедупликации нужен ключ, который существует до первой повторной отправки

Ключ дедупликации — не удобный первичный ключ, выбранный на приёмнике. Он назначается выше по потоку от любой повторной отправки — в момент аукциона или в момент создания события, — и получатель никогда его не выдумывает. Отметки времени прибытия в него не входят: повтор несёт новое время прибытия и новый ключ.

Ключ обязан быть одинаковым во всех попытках, и это определяет его части: биржа или seat, идентификатор аукциона, идентификатор показа и тип события. Партиционируйте топик по этому ключу, чтобы повторы попадали вместе и порядок внутри ключа их переживал. Эта инструкция только про транспорт: то же слово, применённое к колоночному хранилищу, даёт по партиции на событие, и вставка умирает на лимите партиций в блоке. Хранилище партиционируйте по времени, а сортируйте по ключу.

  • Идемпотентный продюсер убирает дубликаты от повторов продюсера внутри одной сессии, и Kafka включает его по умолчанию начиная с 3.0 вместе с подтверждением от всех реплик
  • Этот дефолт условен: конфликтующая настройка из старой конфигурации молча выключает идемпотентность, поэтому спрашивайте у запущенного процесса, что у него стоит
  • Она не видит дубликат на уровне приложения: упавший процесс, который переслал заново, дважды сработавший бикон, оператора, перезапустившего задачу загрузки
  • Дедупликация вставок в ClickHouse хеширует содержимое блока, поэтому консьюмер, который пересобирает батчи после ребаланса, шлёт те же строки в новой форме, и хеш промахивается
  • Окно ограничено и по числу блоков, и по времени, а на нереплицируемых таблицах по умолчанию равно нулю, то есть выключено; токен вставки снимает эту зависимость
  • Exactly-once внутри лога покрывает consume-transform-produce, а переход в аналитическую базу лежит за этой границей, что бы транспорт ни обещал

Поэтому рабочая схема — транспорт at-least-once с идемпотентными ключами. Дедупликация на записи держит счёт за хранение в разумных пределах; верной цифру делает дедупликация на чтении. Слияния никогда не объединяют куски из разных партиций, поэтому дубликат, попавший в следующую партицию, разрешается только тогда, когда его спросит запрос.

Back pressure решает, будет потеря цифрой или слухом

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

  • Ограниченные очереди на каждом переходе, с явным отказом вместо роста. Неограниченная очередь переносит потерю в давление на память и перезапуск
  • Время блокировки продюсера и размер буфера — решения о ёмкости: считайте их от пика, который вы измерили, и ставьте аларм на время, проведённое в блокировке
  • Сбрасывайте нагрузку по классам, а не случайно: показы и оплачиваемые события выживают, диагностика уходит первой, и каждое сброшенное событие увеличивает размеченный счётчик
  • Отставание консьюмера — это back pressure, ставший видимым. Ставьте аларм на возраст самого старого необработанного события и на скорость изменения отставания

Отброс с размеченным счётчиком — известная величина, её можно свести позже. Отброс без счётчика — это не потерянные данные, это потерянная цифра.

Опоздание структурно, и половина расхождения — это календарь

Руководство по внедрению OpenRTB говорит прямо: последовательность от рекламного запроса через аукцион к рендерингу и биллингу принципиально не транзакционна. Между двумя цифрами стоит слишком много сторон.

Задержка ожидаема, а не исключительна. Bid request может нести срок истечения показа, а ставка — задержку, которую биддер терпит, и то же руководство даёт прикидки от порядка минуты для веба до куда больших значений для кешированных in-app форматов и сшитого видео.

Ни одно из этих полей не контракт. Руководство прямо говорит, что уведомление о биллинге, пришедшее позже объявленного биддером срока истечения, всё равно может быть оплачиваемым, — это разговор о политике между биддером и биржей, а не то, что навязывает протокол.

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

Событие, пришедшее после окна, — не дефект пайплайна, а свойство среды. Настоящий выбор здесь один: движется ли цифра публично, пока окно открыто, или меняется непублично уже после.

Сверка джойнится по идентификаторам, которые протокол уже несёт

Идентификаторы, которые биржа подставляет в URL уведомлений и трекинга, — это идентификатор аукциона из bid request, идентификатор показа и, если биддер его создал, идентификатор ставки. Ни один из трёх сам по себе не ключ.

Спецификация называет идентификатор аукциона уникальным в пределах биржи, а не глобально: две биржи могут выдать вам одну и ту же строку в один день. Идентификатор показа уникален только внутри своего bid request и часто равен буквально 1. Идентификатор ставки необязателен.

Держится составной ключ: биржа или seat, через которые прошла сделка, плюс идентификатор аукциона, плюс идентификатор показа. Создавайте его на стороне биддера в момент аукциона, а всё, что короче, считайте префиксом, а не ключом.

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

  • Выигранные аукционы, из лога биддера — единственный счёт, который целиком ваш
  • Уведомления о выигрыше, полученные биржей, — разница это потери уведомлений и таймауты, а по спецификации уведомление о выигрыше не обязательно означает доставку
  • Биконы, принятые на вашем эдже, — разница это клиентский сбор и все стыки выше него
  • События после дедупликации — разница это повторы, и она должна быть стабильной от недели к неделе
  • События после фильтрации невалидного трафика — разница это доля фильтрации, которую вы публикуете, а не обнаруживаете
  • Оплачиваемые события — разница это правило биллинга, а уведомление живёт на серверной стороне, где биржа проводит выручку

Цель — стабильное отношение между шагами, тревога — необъяснённое движение. Внутри одной системы отношения на переходах должны стоять на единице, и любое отклонение — сигнал. На границе между аукционом и измерением подозрительное показание — как раз единица.

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

Чего не даёт переписанный пайплайн

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

Решите заранее, какой счёт является системой учёта для расхода и при каком расхождении примечание в отчёте становится тикетом к бирже. Работа выше покупает атрибуцию потерь, честные дубликаты и сверку, объяснённую построчно. Она не покупает следующего.

  • Она не уравнивает два счёта: обе стороны намеренно считают разные события, и разница объясняется, а не устраняется
  • Она не возвращает события, отброшенные до того, как появилось инструментирование, — кривая доуточнения начинается в тот же день, что и счётчики
  • Она не убирает пересчёт задним числом: вчерашний день движется, пока открыто окно опозданий, и бизнесу, который этого не терпит, нужно более позднее закрытие
  • Она не переживает отсутствующие макросы: без идентификаторов аукциона в биконах никакая схема хранения не даст джойн на уровне событий
  • Она не делает сэмплированные данные соединяемыми задним числом, потому что сэмплирование решает, на какие вопросы останется ответ, ещё до того, как строка записана
  • Она не заменяет список раскрытия, которого ждёт аудит в стиле MRC: точка захвата, частота логирования, оценки задержки, правила для расхождений

У пайплайна, который может ответить на эти вопросы о раскрытии, есть история целостности. У того, который не может, есть мнение, а мнение — это то, о чём спорят в конце квартала.

Отказ, от которого стоит защищаться проектированием, — не пропавший час, с которого начинается расследование. Это тихая версия: стык, который сбрасывает без счётчика, вставка, подтверждённая до сохранности, и сверка по окну, которое ещё открыто.

Что amBrain может подтвердить публично: amBrain — инженерная компания из Еревана, Армения: строим low latency торговые платформы, matching engine и системы real-time bidding на Rust. amBrain пишет софт с 2019 года. Переписывание измерительного пайплайна — не та работа, которая здесь описана. Если цифры перестают сходиться на стороне биддера и биржи — на самих записях о ставке, выигрыше и показе, — вот об этом стоит разговаривать, и начинается он с кривой доуточнения, а не с переписывания.

На столе похожая архитектура?

Принесите текущую архитектуру и тот сценарий отказа, который вас беспокоит, — разберём его вместе за полчаса.

Похожие статьи

Ошибка загрузки изображения
AdTech
Sep 10, 202610 мин чтения

Паузы GC в Go на RTB-биддере: mark assist, дедлайны и решение о Rust

Читать
Ошибка загрузки изображения
AdTech
Mar 5, 20267 мин чтения

Как AI меняет programmatic-рекламу в 2026 году

Читать
Ошибка загрузки изображения
AdTech
Feb 14, 20266 мин чтения

Таргетинг с приоритетом приватности: AdTech без сторонних cookie

Читать