Ни GA, ни ЯМ. Свой кликстрим в Авито

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

Систему сбора и анализа событий можно обобщённо назвать кликстримом. Расскажу о технической стороне кликстрима в Авито: устройство событий, их отправка и доставка, аналитика, отчёты. Почему хочется своё, если есть Google Analytics и Яндекс.Метрика, кому портят жизнь разработчики кликстримов и почему go-кодеры не могут забыть php.

b_yjuzzjoz1p2bhzpwnnubrhrt0.png

Дмитрий Хасанов, десять лет в веб-разработке, три года в Авито. Работаю в платформенной команде, разрабатываю общие инфраструктурные инструменты. Люблю хакатоны.

Бизнесу требуется глубокое понимание процессов, происходящих на сайте. Например, при регистрации пользователя хочется узнать, из какого региона, с какого устройства и через какой браузер зашёл пользователь. Как заполнены поля формы, отправлена ли она, или пользователь сдался. А если сдался, на каком шаге. И сколько времени это заняло.

Хочется знать, будут ли нажимать на кнопку чаще, если перекрасить её в зелёный. Будут ли на зелёную кнопку чаще нажимать в Мурманске или во Владивостоке, днём или ночью, пользователи мобильных приложений или сайта; пользователи, пришедшие с главной или из поиска; покупавшие до этого на Авито или пришедшие впервые.

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

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

С помощью кликстрима как продукта программистам должно быть легко отправить события из проекта, а аналитикам — управлять собираемыми событиями и строить разнообразные отчёты, показывающие тренды и подтверждающие гипотезы.

Отчёты, построенные на основе потока событий.
Пример 1.
ltvte3vhpz37df89hbwurg1gjno.png

Пример 2.
blhewctbesnuyilsxv4zozn7tlu.png

Знаем о Яндекс Метрике и Google Analytics, используем для некоторых задач. С их помощью хорошо и быстро можно собирать аналитические данные с фронтендов. Но для экспорта данных из бэкендов во внешние аналитические системы придётся делать хитрые интеграции.

С внешними инструментами придётся самостоятельно решить задачу расщепления потока событий.

Аналитическая информация очень ценна. Мы собираем её годами, она позволяет знать в мельчайших деталях, как ведут себя наши пользователи. Такими знаниями с внешним миром делиться не хочется.

Законодательство обязывает хранить данные на территории России.

Указанных причин вполне хватило, чтобы в качестве основного инструмента для сбора и обработки аналитических данных разработать собственное решение.

События отправляются через высокопроизводительный транспорт (Event Streaming Processing, ESP) в хранилище (Data Warehouse, DWH). На основе данных в хранилище строятся аналитические отчёты.


Событие

Центральная сущность. Само по себе оно означает факт. Случилось что-то конкретное в обозначенную единицу времени.

Нужно отличать одно событие от другого. Этому служит уникальный идентификатор события.

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


Поле

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

Признаки поля: тип (строка, число, массив), обязательность.


Окружение

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

Источники заметно отличаются друг от друга. Это могут быть внутренние демоны и кроны, фронтенд или бэкенд сервиса, мобильное приложение. Часть полей нужно отправлять с каждым событием конкретного источника.

Возникает понятие «окружение». Это логическая группировка событий по источникам с возможностью установки общих полей для всех событий источника.

Примеры окружений: «бэкенд сервиса А», «фронтенд сервиса А», «ios-приложение сервиса А».


Справочник событий

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

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


Лангпак

Мы отказались от пыток, и больше не заставляем разработчиков вручную писать код отправки событий. Вместо этого на основе справочника генерируем набор файлов для каждого из поддерживаемых в компании серверных языков: php, go или python«а. Такой сгенерированный код называем «лангпаком».

Файлы в лангпаке максимально простые, они не знают о бизнес-логике проектов. Это набор геттеров и сеттеров полей для каждого из событий и код для отправки события.

Для каждого окружения создаётся один лангпак. Он раскладывается в репозиторий пакетов (satis для php, pypi для python«а). Обновляется автоматически при внесении изменений в справочник.

Нельзя перестать писать на PHP. Код сервиса, генерирующего лангпаки, написан на Go. В компании хватает PHP-проектов, поэтому пришлось вспомнить любимый трёхбуквенный язык программирования и генерировать PHP-код на Go. Если немного увлечься, можно ещё и тестов нагенерировать, чтобы этими тестами сгенерированный код проверить.


Версионирование

Справочник править можно. Код на бою ломать нельзя. Мы генерируем боевой код на основе справочника. Опасненько.

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

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

Используем semver. Версия каждого лангпака состоит из трёх цифр. Первая всегда ноль, вторая — версия кода лангпака, третья — инкремент. Третья цифра меняется чаще всего, после каждого изменения событий.

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


Транспорт

В отличие от ребят из Badoo на LSD, мы так и не научились красиво писать файлы. И считаем, что NSQ — не только сервер очередей, но и транспорт для событий.

Скрыли NSQ за небольшим слоем кода на go, разложили коллекторы на каждую ноду в кластере Кубернетеса с помощью daemon set«ов, написали консьюмеры, которые умеют складывать события в разные источники.

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


Роутинг событий

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

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

Общая схема роутинга: у каждого события может быть указан набор получателей. Среди возможных получателей — общее аналитическое хранилище (DWH), рэббиты или монги проектов, заинтересованных в определённых событиях. Последний случай, например, используется для дообучения моделей автомодерации объявлений. Модели слушают определённые события, получая необходимую обратную связь.

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


Хранилище

Основное хранилище событий — HP Vertica на несколько десятков терабайт. Колоночная база с характеристиками, подходящими нашим аналитикам. Интерфейс — Tableau для построения отчётов.

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

Вычитка из буферного Монго на питонячьих скриптах. Скрипты ориентируются на справочник, стараемся не держать здесь бизнес-логики. На этом этапе возможно обогащение событий.


Ручные танцы в темноте

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

Часто внутри кода отправки событий жили кусочки бизнес-логики. Код с таким знанием нельзя портировать в другие проекты. При рефакторинге бизнес-логику требуется вернуть в проект, оставив в коде событий только соответствие заданному формату данных.

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

Событий было не очень много. Буферные коллекции в монго добавлялись по мере необходимости. По мере роста количества событий требовалось вручную перенаправлять события в другие коллекции, досоздавать необходимые коллекции. Решение о размещении события в той или иной буферной коллекции принималось в момент отправки, на стороне проекта. Транспортом выступал Fluent, клиентом для него — td-agent.


Осведомлённый рассинхрон

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

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

Разработчики умеют забывать. Это приводило к рассинхронизации справочника и кода, но общую картину справочник показал.

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


Новый транспорт

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


Просвещённый кликстримизм

На основе справочника сгенерировали лангпаки. Они не позволяют создавать невалидные события.

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

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

Получили систему, стремящуюся соответствовать справочнику. Бонусы: прозрачность, управляемость, скорость создания и изменения событий.

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

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

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

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

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

P.S. Доклад на эту тему я рассказывал на митапе Backend United #1. Винегрет. Можно посмотреть
презентацию или видео со встречи.

© Habrahabr.ru