Ни GA, ни ЯМ. Как мы сделали собственный кликстрим

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


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



    Обо мне


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


    Задача


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


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


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


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


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


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


    Пример 2.


    Готовые инструменты


    Знаем о Яндекс Метрике и 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. Винегрет. Можно посмотреть
    презентацию или видео со встречи.

    Авито

    259,69

    У нас живут ваши объявления

    Поделиться публикацией

    Похожие публикации

    Комментарии 28
      0
      Как впечатления от Tableau? Что можете подобного посоветовать?

      Сейчас столкнулся с подобным кейсом когда GA слишком мало.
      Есть внутренняя система распределения ads трафика (2кк юзеров в сутки, около 10кк событий), все хранится в ClickhouseDB. Пока были тугие попытки написать свой интерфейс отчетов, но хотелось бы чего-то гибкого и приятного вместо велосипедов.
        0
        PowerBI как альтернатива. Интересно есть ли open source аналоги…
          +1

          Посмотрите metabase, пока это лучше что нашёл из open-source.

            +1
            Есть, но разной степени open sourceсности и степени готовности. Есть kibana, симпатичная, но не хочет работать без elastiса. Есть занятный SuperSet от Airbnb, который гордо именует себя версией 0.26, что включает режим паранои на максимум и желание загружать это в продакшн стремительно тает. Если у вас cloudera, то там еще есть Hue dashboards, который работает с Solr Search. Это как эластик, только все на xml.
            А можно очень сильно упороться и генерировать html/excel отчеты в pandas/R/shiny/plotly. Последний обладает очень смешной open source версией, выкатывая ваш BI во внейшний мир, будьте осторожны. Так что варианты вроде бы и есть, но у каждого есть свои недостатки :)
              0

              Не увидел сильного преимущества суперсета перед той же графаной. Может плохо искал?

                0
                Что-то в этом треде все в одну кучу смешали, grafana, kibana, metabase… Это все же инструменты под разные задачи, как мне кажется.
                  0
                  Очень сильно зависит от того, что для вас является преимуществом, а что недостатком. У нас кластер на cloudera и тащить туда или ставить отдельно elastic для kibana как-то не хочется. Суперсет подключается к любой базе данных и друиду, который в том же hortonworks можно поднять двумя кликами (впечатления коллег, сам не пробовал).

                  У суперсета мне нравится интеграция с Уберовским deck.gl и красивыми карточными визуализациями, но в вашем случае это может быть нерелеватно.
                  В общем «зависит».
                  Ну и дело вкуса, вам (вашему менеджеру) просто визуально могут нравиться больше те или иные графы :)

                  п.с. забыл еще о Zeppelin
                    0
                    Ну, Графана в теории тоже подцепляется к любой БД. Суперсет, конечно, из коробки выглядит более серьезным и аналитическим инструментом. Но фактически — визуализации и там, и там богатые, но у графаны, такое ощущение, источников больше (включая различные TSDB).

                    Касательно друида — вообще сомнительно нужен ли он. Многие Clickhouse используют и не знают проблем.
                      +1
                      Внимательно прочитал ваш комментарий, понял что ответил на вопрос, который мне не задавали :)

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

                      Так что кроме эстетического аспекта, функционально они и правда очень похожи. В конце выбрал бы суперсет, потому что больше пишу на питоне, чем на Java, так что при необходимости что-то допилить напильником, будет чуть легче.
                0
                Есть еще redash.io. Умеет работать с 23 хранилищами, включая ClickHouse.
                metabase, к сожалению, не поддерживет ClickHouse и Apache Cassandra.
                +1
                Ответ наших аналитиков:

                От Tableau приятные впечатление (если вас не смущает цена), выбирали еще в 2013 сравнивая с QlikView. Основным преимущество на тот момент было наличие “Live connection” к Vertica ( -> объем рассчитываемых данных «неограничен»).

                Еще из плюсов:
                — Может работать как с оперативной памятью локальной машины, так и на сервере
                — Имеет низкий порог входа для неподготовленного пользователя
                — Красиво выглядит :)

                К Clickhouse нативного коннектора не имеет, к сожалению, а будет ли работать по ODBC шустро — вопрос открытый.
                0

                Так в каком же регионе чаще промахиваются по зеленой кнопке?

                  +1
                  При разработке удалось отделить задачу сбору событий из проектов и доставки в хранилище от задачи организации самого хранилища и построения отчётов. В итоге мне как разработчику мало приходилось видеть витрины в Tableau. Но я обязательно поинтересуюсь у аналитиков.

                  Могу предположить, что в регионах, где солнце ярко в монитор светит, по зелёной кнопке попасть гораздо труднее.
                  0
                  Интересный опыт! Как вы валидируете события и описываете структуру справочника — json schema, xml schema или что-то своё?
                    +2
                    Фронтенд справочника — админка. Первичное хранилище конфигураций — постгрес. Json schema не используется. С помощью лангпаков удалось сдвинуть валидацию в проекты. В лангпаках генерируется нативный код, схема там не пригодилась.

                    Есть интересные случаи: события могут прилетать от фронтендов и других проектов. На нашей стороны для приёма таких событий есть прокси, старающиеся как можно меньше вмешиваться в данные, но умеющие сигнализировать об ошибках. В некоторых интеграциях мы использовали protobuf. Соответственно, пришлось генерировать proto-схему.
                    +3
                    Понравилась форма изложения. Кратко, по делу, информативно. Мне бы на то же самое понадобилось несколько статей.

                    Чуть-чуть вопросов:

                    • События только в Vertica лежат, или есть ещё cold storage для старых событий?
                    • Разные версии одного события приземляются в одну таблицу или в разные?
                    • Можно ли запустить запрос, затрагивающий несколько событий, и сделать между ними JOIN? Например, построить произвольный funnel (событие А, затем B, затем C или D)?
                    • Общие для всех событий «заголовки» (окружение, версия приложения, session_id, device_id) хранятся в одной таблице вместе с телом события или лежат отдельно?


                    Спасибо :)
                      +2
                      По вопросам:
                      1. Есть cold storage с реализованной схемой подгрузки в быструю зону.
                      2. В Вертике? События раскладываются по множеству таблиц 6НФ. Новые версии попадают в те же таблицы, что и старые, но у новых новые атрибуты летят в новые таблицы. Про это большая статья тут есть, про грибницу: habr.com/company/avito/blog/322510
                      3. Конечно. Ради этого все и делается. funnel из полудюжины таблиц+данные из платежных систем+результаты прогнозов и результаты рекламных рассылок. Только так и получается нормальная аналитика.
                      4. Все отдельно. 6 нормальная форма, все описано в статье про грибницу.
                        0
                        О, спасибо. Пропустил оригинальную статью про нормализацию, хоть и слышал об этом голосом. Почитал с удовольствием.

                        Ещё один вопрос: верно ли, что при таком подходе очень заметно увеличиваются требования к I/O? Ведь нужно намного больше и писать, и читать:

                        • При импорте обязательный lookup на каждый атрибут, чтобы понять, нужно ли создавать новую запись;
                        • Много дополнительных суррогатных ключей и полей «load_date», которых не было бы в других моделях;
                        • По две проекции на «ties»-таблицы — храним двойной объём данных;


                        Насколько я помню, Vertica обычно читает HDD на select-запросы, и они конкурируют за ресурсы с ETL. А значит, при честной anchor-модели база намного быстрее упрётся в диски при увеличении нагрузки на том же оборудовании, чем в более «обычных» случаях.

                        Верно ли это?
                          +1
                          Я бы сказал, что нет, и вот почему. В один год в нашу Вертику прилетает чуть больше 2Пб данных (метрика на кликстриме). И так уже больше 5 лет, а данных в итоге 176Тб в горячем слое + ~300Тб в архивной зоне.
                          Это происходит из-за логического сжатия: если произошло 10к событий с одного большого URL, мы сохраним его только раз, а дальше будем использовать INT ключ. И так почти со всем: мы добавляем в хранилище только действительно новые данные, только реально изменившиеся поля. Из-за этого записи намного меньше, а чтения — сначала больше, а потом тоже меньше. В реальности почти все запросы упираются не в диск, а в оперативку, в которой кешируются выборки для lookup.
                      0
                      Почему в качестве стореджа дорогущая Vertica, а не бесплатный ClickHouse?
                        +2
                        Такими вопросами ведает azathot. Его комментарий:

                        1. Нет ANSI SQL. Поэтому Tableau с кликхаусом не работает. Обещают уже больше года

                        2. Нет ANSI SQL в смысле отсутствия нормальных join-ов. Только локальные джойны со словарями, а сджойнить две таблицы по 100 млрд. строк нельзя. Мы джойним десятки таких таблиц.

                        Т.е. кликхаус для задач ad-hoc аналитики не применим.
                          0
                          Нет ANSI SQL в смысле отсутствия нормальных join-ов. Только локальные джойны со словарями, а сджойнить две таблицы по 100 млрд. строк нельзя. Мы джойним десятки таких таблиц.

                          Можно джойнить таблицы. Да, синтаксис у них специфичный, но если надо сджойнить несколько таблиц, ANY LEFT JOIN замечательно с этим справляется.
                            +1
                            Конечно можно джойнить таблицы, если правая (словарь) маленькая.
                            Цитата из официальной документации ClickHouse:
                            The right table (the subquery result) resides in RAM. If there isn't enough memory, you can't run a JOIN.
                            И это только первая проблема. Вторая возникает, когда ClickHouse шардирован на несколько таблиц, и нужно сделать JOIN больших таблиц, шардированных по разному (нужна ресегментация данных). Эту проблему можно частично решить конструкцией Global, но только, опять же, для маленьких таблиц.
                              0
                              Я согласен, что join с «особенностями», меня покоробило «Только локальные джойны со словарями»… Они есть, но да, исполняются в памяти и объединять в запросе несколько таблиц с выборками 100ММ скорее всего не получится, если памяти не очень много… В своих задачах объединял несколько таблиц, примерно таких объёмов: 600М, 50М, 1М, 100К, 10К… Для ускорения использовал 64-битный хеш от идентификатора записи (UUID), потому как он давал единичные коллизии на таких объёмах, что для моих задач допустимо.
                                +2
                                Прошу прощения, у меня искаженное представление о нормальности :)… «Локальный джойн со словарем» в моем восприятии это JOIN, который может быть локализован в одной машине. Одна машина, в реалиях 18 года, может иметь 512Гб оперативной памяти и может быть весьма мощной. Там совершенно нормально может пройти локальный JOIN со словарем в 1 млрд. строк. Другое дело, когда нужно соединять несколько таблиц в десятки и сотни млрд. строк. Отвечая на вопрос, зачем платить за Вертику: чтобы соединять таблицы, не смотря на их размер. Все нужно уметь джойнить со всем.
                                  0
                                  Понял, согласен, отстал )))
                        0
                        azathot,
                        в продолжение вопросов про слой хранения…
                        почему выбрали MPP, а не hdfs + spark sql (или другой sql движок)?
                          0
                          Потому что хотели делать ad-hoc аналитику?
                          Вопрос про альтернативу в такой постановке я бы сначала предварил другим вопросом:
                          — а вы можете привести пример организации с сотнями 100Тб данных, которая успешно сделала ad-hoc аналитику на hdfs + spark sql, без MPP баз? Я бы с ними с радостью подискутировал.

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

                        Самое читаемое