Считаем статистику по экспериментам на hh.ru

    Всем привет!

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

    О том, как считаются автоматические метрики по А/Б-экспериментам, можно почитать в нашей другой статье.

    image

    Какие данные мы анализируем и откуда они берутся


    Мы анализируем access-логи и любые кастомные логи, которые пишем сами.

    95.108.213.12 — - [13/Aug/2018:04:00:02 +0300] 200 «GET /employer/2574971 HTTP/1.1» 12012 "-" «Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)» "-" «gardabani.headhunter.ge» «0.063» "-" «1534122002.858» "-" «192.168.2.38:1500» "[0.064]" {15341220027959c8c01c51a6e01b682f} 200 https 1 — "-" — - [35827][0.000 0]
    178.23.230.16 — - [13/Aug/2018:04:00:02 +0300] 200 «GET /vacancy/24266672 HTTP/1.1» 24229 «hh.ru/vacancy/24007186?query=bmw» «Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8» "-" «hh.ru» «0.210» «last_visit=1534111115966::1534121915966; hhrole=anonymous; regions=1; tmr_detect=0%7C1534121918520; total_searches=3; unique_banner_user=1534121429.273825242076558» «1534122002.859» "-" «192.168.2.239:1500» "[0.208]" {1534122002649b7eef2e901d8c9c0469} 200 https 1 — "-" — - [35927][0.001 0]

    В нашей архитектуре каждый сервис пишет логи локально, а затем через самописный клиент-сервер логи (в том числе и access-логи nginx) собираются на центральное хранилище (далее logging). К этой машине имеют доступ разработчики и могут вручную грепать логи при необходимости. Но как же в разумное время погрепать несколько сотен гигабайт логов? Конечно, залить их в hadoop!

    Откуда данные появляются в hadoop?


    В hadoop хранятся не только логи сервисов, но и выгрузка prod-базы. Ежедневно в hadoop мы выгружаем некоторую часть таблиц, которые необходимы для аналитики.

    Логи сервисов попадают в hadoop тремя путями.

    1. Путь в лоб — с хранилища логов ночью запускается cron, и rsync выгружает сырые логи в hdfs.
    2. Путь модный — логи с сервисов льются не только в общее хранилище, но и в kafka, откуда их вычитывает flume, делает предобработку и сохраняет в hdfs.
    3. Путь старомодный — во времена до kafka мы написали свой сервис, который читает сырые логи с хранилища, делает из предобработку и заливает в hdfs.

    Рассмотрим каждый подход более подробно.

    Путь в лоб


    Крон запускает обычный bash-скрипт.

    #!/bin/bash
    
    LOGGING_DATE_PATH_PART=$(date -d yesterday +\%Y/\%m/\%d)
    HADOOP_DATE_PATH_PART=$(date -d yesterday +year=\%Y/month=\%m/day=\%d)
    
    ls /logging/java/${LOGGING_DATE_PATH_PART}/hh-banner-sync/banner-versions*.log | while read source_filename; do
    dest_filename=$(basename "$source_filename")
    /usr/bin/rsync --no-relative --no-implied-dirs --bwlimit=12288 ${source_filename} rsync://hadoop2.hhnet.ru/hdfs-raw/banner-versions/${HADOOP_DATE_PATH_PART}/${dest_filename};
    done

    Как мы помним, в хранилище логов все логи лежат в виде обычных файлов, структура папок примерно такая: /logging/java/2018/08/10/{service_name}/*.log

    Hadoop хранит свои файлы примерно в такой же структуре папок hdfs-raw/banner-versions/year=2018/month=08/day=10
    year,month,day мы используем в качестве партиций.

    Таким образом, нам надо только сформировать правильные пути (строки 3–4), а затем выбрать все нужные логи (строка 6) и с помощью rsync залить их в hadoop (строка 8).

    Плюсы этого подхода:

    • Быстрая разработка
    • Все прозрачно и понятно

    Минусы:

    • Нет предобработки

    Путь модный


    Поскольку мы заливаем логи в хранилище самописным скриптом, то логично было прикрутить возможность лить их не только на сервер, но и в kafka.

    Плюсы

    • Онлайн-логи (логи в hadoop появляются по мере заливки в kafka)
    • Можно делать предобработку
    • Хорошо держит нагрузку и можно заливать большие логи

    Минусы

    • Сложнее настройка
    • Надо писать код
    • Больше составных частей процесса заливки
    • Сложнее мониторинг и разбор инцидентов

    Путь старомодный


    Отличается от модного только отсутствием kafka. Поэтому наследует все минусы и только некоторые плюсы предыдущего подхода. Отдельный сервис (ustats-uploader) на java периодически читает нужные файлы, делает их предобработку и заливает в hadoop.

    Плюсы

    • Можно делать предобработку

    Минусы

    • Сложнее настройка
    • Надо писать код

    И вот данные попали в hadoop и готовы к анализу. Давайте немного остановимся и вспомним, что же такое hadoop и почему на нем можно погрепать сотни гигабайт гораздо быстрее, чем обычным grep.

    Hadoop


    Hadoop — это распределенное хранилище данных. Данные не лежат на каком-то отдельном сервере, а распределены между несколькими машинами, а так же хранятся не в одном экземпляре, а в нескольких — это сделано для обеспечения надежности. Основа скорости обработки данных лежит в изменении подхода в сравнении с обычными базами данных.

    В случае обычной БД мы извлекаем данные из нее и отправляем клиенту, который делает какой-то анализ и возвращает аналитику результат. Таким образом, чтобы быстрее считать нам надо иметь много клиентов и параллелить запросы (к примеру, поделить данные по месяцам — и каждый клиент может считать данные за свой месяц).

    В hadoop все наоборот. Мы отправляем код (что именно мы хотим посчитать) к данным, и этот код выполняется на кластере. Как мы знаем, данные лежат на многих машинах, таким образом, каждая машина выполняет код только над своими данными и возвращает результат клиенту.

    Многие, наверное, слышали о map-reduce, но писать код для аналитики не очень удобно и быстро, в то время как писать на SQL гораздо проще. Поэтому появились сервисы, которые умеют превращать SQL в map-reduce прозрачно для пользователя, причем аналитик может и не подозревать, как на самом деле считается его запрос.

    В hh.ru для этого мы используем hive и presto. Hive был первым, но мы постепенно переходим на presto, т. к. он гораздо быстрее для наших запросов. В качестве GUI мы используем hue и zeppelin.

    Мне удобнее считать аналитику на python в jupyter, это позволяет считать ее одним кликом и на выходе получать правильно оформленные excel-таблицы, что сильно экономит время. Пишите в комментариях, эта тема тянет на отдельную статью.

    Вернемся к самой аналитике.

    Как же понять, что мы хотим считать?


    Пришел продакт-менеджер с задачей подсчитать результаты эксперимента


    Мы отсылаем email-рассылку, в которой присылаем подходящие вакансии для соискателя (всем же нравятся такие рассылки?). Мы решили немного поменять дизайн письма и хотим понять стало ли лучше. Для этого будем считать:

    • количество переходов на вакансии из письма;
    • отклики после перехода

    Напомню, что все, что у нас есть, — это access log и база. Нам надо в терминах переходов по ссылкам сформулировать наши метрики.

    Количество переходов на вакансию из письма


    Переход — это GET-запрос на hh.ru/vacancy/26646861. Для понимания, откуда был переход, мы добавляем utm-метки вида ?utm_source=email_campaign_123. Для GET-запросов в access log будет информация о параметрах, и мы можем отфильтровать переходы только из нашей рассылки.

    Количество откликов после перехода


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

    У нас есть два варианта, как сформулировать количество откликов:

    1. Отклик — это POST на hh.ru/applicant/vacancy_response/popup?vacancy_id=26646861, у которого referer hh.ru/vacancy/26646861?utm_source=email_campaign_123.
    2. Нюанс такого подхода в том, что если пользователь перешел на вакансию, а потом походил по сайту немного и потом откликнулся на вакансию, то мы его не засчитаем.
    3. Мы можем запомнить id пользователя, который перешел на hh.ru/vacancy/26646861, и посчитать по базе количество отзывов на вакансию в течение дня.

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

    Подводные камни, которые могут встретиться


    1. Не все данные есть в hadoop, нужно добавить данные из prod-базы. К примеру, в логах обычно только id, а если нужно название — то оно в базе. Иногда надо по resume_id искать юзера, и это тоже хранится в базе. Для этого мы и выгружаем часть базы в hadoop, чтобы join был попроще.
    2. Данные могут быть кривые. Это вообще беда hadoop и того, как мы грузим в него данные. В зависимости от данных пустое значение может быть null, None, none, пустая строка и т. д. Нужно быть аккуратным в каждом отдельном случае, т. к. данные действительно разные, грузятся разными способами и для разных целей.
    3. Долго считать за весь период. К примеру, нам надо посчитать наши переходы и отклики за месяц. Это примерно 3 терабайта логов. Даже hadoop будет считать это довольно долго. Обычно написать 100%-й рабочий запрос с первого раза довольно сложно, поэтому мы пишем его путем проб и ошибок. Каждый раз ждать по 20 минут — это очень долго. Способы решения:

      • Отладка запроса на логах за 1 день. Поскольку данные в hadoop у нас партицированы, то посчитать что-то за 1 день логов — довольно быстро.
      • Выгрузить во временную таблицу нужные логи. Как правило, мы понимаем какие урлы нам интересны, и можно сделать временную таблицу по логам с этих урлов.

      Лично мне удобнее первый вариант, но, бывает, нужно сделать временную таблицу, зависит от ситуации.
    4. Перекосы в итоговых метриках
      • Лучше фильтровать логи. Нужно обращать внимание, например, на код ответа, редирект и т. д. Лучше меньше данных, но более точных, в которых вы уверены.
      • Как можно меньше промежуточных шагов в метрике. К примеру, переход на вакансию — это один шаг (GET-запрос на /vacancy/123). Отклик — два (переход на вакансию + POST). Чем короче цепочка — тем меньше ошибок и точнее метрика. Иногда бывает, что данные между переходами теряются и посчитать что-то вообще невозможно. Чтобы решить эту проблему, надо перед разработкой эксперимента подумать, что мы будет считать и как. Крайне сильно помогает свой отдельный лог нужных событий. Мы умеем отстреливать нужные события, и таким образом цепочка событий будет точнее, а считать — проще.
      • Боты могут генерировать кучу переходов. Нужно понимать, куда могут боты зайти (к примеру, на страницах, где требуется авторизация, их быть не должно), и фильтровать эти данные.
      • Большие шишки — к примеру, в одной из групп может быть один соискатель, который генерирует 50% всех откликов. Будет перекос статистики, такие данные тоже нужно фильтровать.
    5. Сложно сформулировать, что считать в терминах access log. Тут помогает знание кодовой базы, опыт и chrome dev tools. Читаем описание метрики от продакта, повторяем руками на сайте и смотрим, какие переходы генерируются.

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

    Результат подсчетов


    В нашем примере есть 2 группы и 2 метрики, которые формируют воронку.
    image
    Рекомендации по оформлению результатов:

    1. Не перегружайте деталями, пока это не требуется. Просто и меньше — это лучше (к примеру, тут мы могли показывать каждую вакансию отдельно или клики по дням). Сфокусируйте внимание на чем-то одном.
    2. Детали могут понадобиться в процессе демо-результатов, поэтому подумайте, какие вопросы могут задать, и подготовьте детали. (В нашем примере детализация может быть по скорости перехода после отправки емейла — 1 день, 3 дня, неделя, группировка вакансий по профобласти)
    3. Помните про статистическую значимость. К примеру, изменение на 1% при количестве переходов 100 и кликах 15 незначимо и могло быть случайным. Пользуйтесь калькуляторами
    4. Автоматизируйте максимально, потому что считать придется несколько раз. Обычно в середине эксперимента уже хочется понять как идут дела. После эксперимента могут возникнуть вопросы и придется что-то уточнить. Таким образом, считать придется 3–4 раза, и, если каждый расчет — это последовательность из 10 запросов и потом ручное копирование в excel, будет больно и потратите много времени. Изучите python, это сэкономит кучу времени.
    5. Используйте графическое представление результатов, когда это оправданно. Встроенные средства hive и zeppelin позволяют из коробки строить простые графики.

    Считать различные метрики приходится довольно часто, потому что мы практически каждую задачу выпускаем в рамках А/Б-эксперимента. Сложного в расчетах ничего нет, после 2-3 экспериментов приходит понимание, как это делать. Помните, что access-логи хранят много полезной информации, которая может сэкономить компании деньги, помочь вам продвинуть свою идею и доказать, какой из вариантов изменений лучше. Главное — суметь эту информацию добыть.

    HeadHunter

    108,83

    HR Digital

    Поделиться публикацией
    Комментарии 14
      0
      очень интересно почитать про аналитику на python в junyper
        +1

        Рассматривался ли вариант делать все A/B-тесты средствами Google Analytics или подобных решений?
        Если (скорее всего) не хватило функциональности, то в какие именно реальные ограничения вы упёрлись?

          +2
          привет, отличный вопрос!
          коллеги, которые непосредственно строят А/В инфраструктуру поделились вот такой инфой
          такой вариант рассматривался. Насколько я понимаю, там можно измерять абсолютные метрики, вроде абс. числа просмотров, абс. числа регистраций. Проблема возникает с конверсиями. Например, если мы измеряем конверсию поиска в отклик то нам нужно приджоинить отклик пользователя к поиску. Я не уверен, можно ли такое сделать в гугл-аналитикс — просто не знаю. Может быть и можно.
          Но мы в наших метриках джоиним с учетом того, что соискатель видел эту вакансию в поисковой выдаче.
          Ну то есть, помимо факта поиска и факта отклика на какую-то вакансию, мы требуем, чтобы эта вакансия была в поисковой выдаче. Такое, я практически уверен, гугл аналитикс не умеет делать. то есть, используя наши логи, мы в теории можем более «тонко» выполнять джоин.

          Плюс есть вопрос безопасности и надежности, что если гугл лежит (или скажем РКН заблочил GA?)
          Так же хотелось полного контроля над процессом, чтобы можно было кастомизировать под себя. Мы четко можем сказать как работает та или иная метрика и проследить ее по логам. Плюс по тем же логам мы считаем любые кастомные метрики вручную.
          Для расчета метрик в GA надо отправлять лишнюю бизнес-информацию о юзере к примеру, а это же конфиденциально.
          Плюс лишний поход вовне, дольше загрузка страницы. У нас признак работает фича или нет долетает вплоть до бэкенда, поэтому на фронт отдаются уже готовые страницы с фичей или без, а пускать GA внутрь периметра безопасного прода нельзя.
          0
          почему вы не хотите использовать подсчет на уровне приложения. к примеру, сессии. это с виду более монолитно будет и более точно.
            +1

            Вы имеете в виду сразу складывать счетчики в сессию юзера?

            –3
            В случае обычной БД мы извлекаем данные из нее и отправляем клиенту, который делает какой-то анализ и возвращает аналитику результат.
            ЧТО?? Я поздравляю IT специалистов компании hh! Пропустить 20 лет развития клиент-серверных технологий для этого надо быть настоящим специалистом. Про хранимый в БД код в виде процедур, да даже просто про SQL запросы передаваемые в БД — не слышали? И про кластеры БД тоже не слышали?
              +1
              Спасибо за наводку — обязательно изучим.
              0
              Спасибо,
              Хорошо объясняете в двух словах популярные технологии, что оно умеет и как вы это используете.
              Получается очень понятно.
                0

                Спасибо за статью. Почему выбран был Hadoop, а не elasticsearch? Да, больше мороки с трансформацией логов, зато аналитика проще.

                  +1
                  отчасти причина в наличии экспертизы по hadoop, elasticsearch мы как раз сейчас пробуем тоже, как накопим опыт, сможем сравнить и поделиться
                  0

                  Рассматривали ли вы комбинацию кафка + вертика? Проблемы с синком других баз в вертику решены, а SQL-диалект, похожий на постгрес, помощнее чем у многих решений будет.

                    0
                    добрый день. не рассматривали. дело в том, что технологий много, провести анализ всех (а зачастую это значит их реально поставить и сравнить) довольно трудоемко. причем у всех технологий есть плюсы и минусы, так что в любом случае выбор — это компромисс.
                    за идею спасибо, возможно посмотрим в ту сторону, если будут проблемы.
                    +1
                    А как-то отслеживается ситуация, когда, например, человеку пришла рассылка, он щелкнул по ссылке, увидел, что ему не интересно читать, и тут же закрыл вкладку? Т.е. есть привязка понятий «человек читал страницу» (т.е. хотя бы находился на ней больше, скажем, 10 секунд, и, желательно, скролил контент) или «дочитал почти до конца» (проскролил минимум почти до конца, желательно с учетом скорости скрола — чтобы именно «читал», а не просто нажал End).
                      +1
                      нет, мы отслеживаем целевые действия на страницах — к примеру отклик, либо переход на какие-то другие страницы.

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

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