Наверх ▲

Управляемый code injection

Михаил Якшин Михаил Якшин Координатор команды разработчиков кластерной части Openstat.

Михаил Якшин: Здравствуйте! Добрый день всем! Мы начинаем эту секцию докладом от Openstat об управляемом code injection в Apache Hadoop, или как мы считаем все пользовательские отчеты за один проход в системе интернет-статистики Openstat.

О чем этот доклад? 

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

Будут затронуты и другие вопросы. Как можно посчитать много разнородных отчетов в одном потоке так, чтобы сэкономить время и вычислительные ресурсы кластера? Как оценить пользу, которую мы из этого извлекаем?

Если посмотреть на веб-аналитику обобщенно, то все очень банально и просто.

Есть сайт, на котором стоит счетчик. Сайт генерирует логи веб-сервера. Счетчик собирает информацию, генерирует свои логи. И все логи как-то обрабатываются, и получаются отчеты.

Если посмотреть на отчеты, то можно заметить, что их можно разделить на два типа: стандартные и нестандартные.

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

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

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

Стандартные и нестандартные отчеты представлены на этом слайде. В левой колонке (стандартные) содержатся более или менее известные отчеты, доступные практически в любой системе веб-аналитики. Можно открыть Google Analytics, Yahoo или Omnicher. У нас они тоже есть. 

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

Очень быстрый, 30-секундный экскурс в термины веб-аналитики. Мы будем говорить о просмотрах (англ. pageviews). Это события загрузки страницы сайта пользователем, посетителем. Просмотры и другие события, кроме просмотров, группируются в сессии/визиты, которые представляют собой последовательность таких обращений от одного пользователя, интервал между которыми не превышает 30-ти минут.

Также есть сами посетители (англ. visitors) – пользователи, которые совершили обращение к сайту и которых мы можем как-то идентифицировать. Это важно. Например, по IP-адресу, по настройкам cookies, по комбинации этого всего с User Agent, с настройками прокси-сервера. Есть еще какое-то количество уникальных данных, за которые можно "ухватиться".

 

Попробуем показать это в картинках. Точки – это просмотры. Прямоугольники, которыми обведены точки, это сессии, или визиты. Кстати, "визит" - весьма неудачный термин. Происходит много путаницы между понятиями "visit" и "visitor", хотя это совершенно разные вещи.

Посетители, которые расположены по вертикали, это разные посетители. Таким образом, получается, что на картинке у нас представлено 14 просмотров, 4 сессии и 3 посетителя.

Какие нестандартные отчеты заказывают потребители веб-аналитики? Типичный отчет (самый простой, который можно придумать) – это отчет по разделу. Либо из названия страницы (из URL или из Title), либо с помощью передачи какой-то переменной происходит генерация токена, который попадает в левую колонку отчета «раздел». Происходит группировка всего трафика, который попал на счетчик или в логи сервера, по разделу.

Мы рассчитываем 3 основных показателя – число просмотров, число сессий и число посетителей. Соответственно, получается такой одномерный отчет, как мы его называем. Одномерный, потому что измерение здесь, по сути, всего одно – раздел.

Двумерный отчет делается несколько более специфично. Нас интересует, посетители какого пола просматривают какой раздел. В первой колонке слева перечислены разделы сайта («Новости», «Статьи», «Магазин»). Справа идет ось со значениями: пол «мужской», «женский» и «всего». На пересечении, таким образом, можно получить только один из показателей, который выбран.

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

Каким же образом можно делать такие отчеты? Какие вообще есть подходы к расчету таких значений в отчетах?

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

Например, можно определять, какого пола посетитель – мужского либо женского, или географию для какого-то конкретно выбранного региона. Также можно фиксировать ответ на вопрос о том, совершил ли посетитель целевое действие: да или нет.

Значения сегмента могут изменяться на протяжении посещения максимум один раз. Оно может переключиться с ноля на единицу и никогда не изменяется обратно.

Таким образом, сегмент – это бинарная характеристика посещения. Посещение либо относится к сегменту (1), либо не относится (0).

Сегмент задается в виде некоторой булевой функции, оперирующей на переменных со значениями из полей лога. Например, поисковая система Google. Или: страна Россия, браузер Firefox (geo_country=”RU” AND browser=”Firefox”). 

Если хотя бы в одном из событий посещения функция вернула "true", то посещение целиком относится к сегменту.

Проиллюстрировать это можно следующим образом. Берем такое условие, например. Почему такое? Потому что, как правило, в ходе посещения просмотр с таким условием будет всего один. Пользователь приходит на сайт всего один раз. Вот эта переменная получается из источника ссылки (англ. referrer), из которого пришел пользователь, который будет в его хите.

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

Этот хит у нас переводит все на весь сегмент целиком.

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

В веб-интерфейсе это выглядит примерно так. Возьмем любой отчет – стандартный или нестандартный… Как правило, проще всего добавить сегмент к какому-то стандартному отчету вплоть до отчета «Посещаемость», который, по сути, есть просто расчет посещаемости в тех или иных показателях по всему сайту, по всему трафику в целом.

Здесь я попробовал проиллюстрировать расчет провайдеров. В левой колонке – вся аудитория, это так называемый нулевой сегмент (по умолчанию). Справа мы выделили сегмент «мобильные пользователи». Характер устройства определяется по User Agent. User Agent в течение сессии не меняется. Все нормально, можно использовать сегмент. Это хрестоматийный пример.

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

Общая схема работы с сегментами выглядит следующим образом. Логи сначала проходят этап вычисления bitmask для сегментов. Формируется некий дополнительный раздел данных под названием «битмаски сегментов». Они, объединяясь с логами, поступают в расчет стандартных отчетов. Сейчас мы говорим только о стандартных отчетах, которые расширяются этими дополнительными колонками, образованными из сегментов.

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

Например, легко выяснить, доходит или не доходит пользователь до совершения какого-то действия, до целевой страницы. Например, просто реализовать деление аудитории на какие-то сегменты – по географии, по технометрике используемых User Agent, по каким-то передаваемым переменным, значения которых не изменяются (например, по полу, по возрасту, по каким-то еще данным из анкеты в социальной сети, например).

Если нам этот механизм не подходит, если мы из него "выросли", то мы переходим на уровень 2. Это полноценные пользовательские отчеты. Сегменты в этом случае не годятся. 

Если нам нужно посчитать статистику по какой-то характеристике, которая изменяется в течение сессии (хрестоматийный пример – это раздел сайта). Если мы зададим в сегменте условие «страница=URL этого раздела сайта», то мы получим совсем не то, что нам интересно. Мы получим данные для всех посетителей и все сессии тех посетителей, которые заходили в этот раздел сайта. Мы не получим посещаемость именно этого раздела сайта в просмотрах.

Как было проиллюстрировано ранее, сегмент "расползется" на всю сессию целиком и даже на те просмотры, которые были сделаны вне этого раздела. Соответственно, появятся единицы в bitmask, и они будут засчитаны, видимо, не туда, куда надо. 

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

Как делаются традиционные отчеты на базе Apache Hadoop?

Создается код из нескольких jobs, которые реализуют цепочку mapper → reducer [→ mapper → reducer → …]. На вход подаются все логи, которые у нас есть, на выходе получаем отчеты. 

Нужно следить за корректным порядком и "work for" запуска jobs. Надо, чтобы они правильно запускались, правильно обрабатывались их "падения", перезапуски. Необходимо, чтобы мы получали промежуточный результат, чтобы временные данные, созданные первым job, корректно попадали ко второму и так далее по цепочке.

Все это достаточно затратно и, может быть, неприятно или, как минимум, медленно.

Поэтому мы уже довольно давно используем не просто Hadoop, а Hadoop с продуктом под названием Cascading. Он позволяет нам перейти от терминов map-reduce к использованию более общих и более понятных для особенно непрофильных программистов map-reduce вещей. Таких, как:

• Function – преобразование одной строчки в от одной до многих строчек.

• Filter – фильтрация, преобразование одной строчки в 0 или 1 строчку.

• Aggregator – это итератор над группой, у которого есть "iterator", есть "initializer" (работа над группой) и "finalizer". Мы получаем группу, сначала вызываем какую-то функцию, которая начинает обработку группы, потом много раз вызываем функцию для обработки каждого элемента группы и финализируем группу, закрывая ее.

• Buffer – замечательный объект, который предоставляет Java Iterator по группе. По сути, то же самое, что Aggregator, но иногда в более удобном или, наоборот, неудобном для задачи виде.

Показываю код. Это относительно важно. Я к нему еще немного вернусь. 

В самом простом виде отчет на Cascading выглядит примерно так. Мы вычисляем какую-то функцию от URL, чтобы получить название раздела, например. Задаем группировку и делаем агрегацию того, что нагруппировали.

Таким образом, основной код отчета у нас – 3 строчки, условно (если записать его в строчку). Функция, допустим, 25-30 строчек (в зависимости от того, насколько она сложная). 

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

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

Чем плох такой отчет?

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

Запускать отдельную задачу на каждый отчет тем более затратно. Когда логи хранятся в самой общей куче, на вход отчета попадают все логи сразу. Хотя результат нужен только для какого-то одного счетчика, который составляет 0,001 % трафика, нам нужно "поднимать" с диска и обрабатывать все-все-все.

Как же можно улучшить такую ситуацию?

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

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

Что предлагается в данном случае?

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

Здесь продемонстрировано, как это сделать для двумерного отчета.

 

Для трехмерного отчета это тоже делается, как ни странно. Думаю, все поверят, что это делается и для четырех-, и для пятимерных отчетов и так далее.

В чем основная идея? Она частично была уже проиллюстрирована на слайде с кодом. Все подобные отчеты можно уложить в общую схему.

1. Некая специфичная для отчета функция-обработчик.

2. Группировка.

3. Агрегация.

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

В чем проблема с полями для группировки?

В том, что здесь разные размерности полей. На первый взгляд, непонятно, как в одном потоке считать отчеты, у которых одна, две, три, десять колонок. Как это все соотносить? Ответ очень простой. 

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

Проблема № 2 чуть посложнее. Это "code injection". Нам надо предоставить пользователю возможность выполнять в рамках задач на нашем кластере свой собственный код. У этого есть целый ряд плюсов и минусов.

Чем это плохо?

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

Приведу пример. Были случаи, когда нам писали функции, состоящие из 40-50 килобайт строчек вида «if нечто equals нечто», «нечто equals нечто». Разумеется, при выполнении этой функции в контексте обработки, например, трафика порядка миллионов событий в сутки это будет страшно медленно. Это можно сделать быстрее.

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

Самое главное (или не самое, но тоже существенное) – вопрос в локальности этих данных и в том, что отчеты считаются, как правило, за один и тот же период несколько раз. Есть отчеты за час, за сутки, за неделю, за месяц. Эти периоды вкладываются один в другой. Соответственно, нам эту функцию приходится пересчитывать 4 раза. Это плохо и нерационально.

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

Что в этом хорошего? Производить первый этап стоит как можно чаще. Он ни от чего не зависит. 

Расчет значений вынесен в отдельный job. Благодаря этому мы можем (насколько это возможно, насколько мы захотим это делать), полностью контролируя эту штуку, выставлять лимиты используемых ресурсов, выделять JVM соответствующие параметры безопасности, отправлять ее в какую-то виртуальную машину и так далее.

Пользовательский код, который приходит на языке Java, мы по необходимости "на лету" компилируем с помощью компилятора – в нашем случае это Janino.

Второй этап производится по необходимости. Выполняются расчеты за отчетный период. Выбираются и используются только нужные заранее вычисленные значения. Этап совершенно чист от всяких "code injection". Имеется гарантированная (это важно) алгоритмическая сложность и четко прогнозируемое время выполнения. Все уже рассчитано, никаких сюрпризов нет.

Как мы обеспечиваем безопасность выполнения чужого кода?

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

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

Выполнение опасных действий, таким образом, можно отследить JVM с SecurityException. Перерасход ресурсов, как правило, тоже "упирается" в какие-то Exception JVM. Перерасход ресурсов процесса операционной системы (в частности, времени выполнения) приводит к тому, что JVM "убивается". Соответственно, мы не считаем этот отчет для данного конкретного пользователя.

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

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

Настройку пользовательского отчета мы делаем в виде JAMA. JAMA дает свои плюсы и минусы. У нас есть идентификатор отчета. Мы задаем группировку (GroupBy) по полям. Задаем агрегацию, в данном случае это сумма. Также мы задаем какой-то COUNT DISTINCT по каким-то полям.

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

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

Мы получили мегабайт отчетов (это уже агрегированные данные) в день. Эта технология у нас сейчас используется примерно для 10 тысяч отчетов (это 1,2 миллиона строчек).

Какую экономию мы получили?

По совершенно простым подсчетам, это 3-4 порядка. Если бы мы запускали все отчеты традиционным способом, это занимало бы порядка 300 тысяч машиночасов. Мы считаем все это примерно за 30-35 машиночасов в сутки.

Все. Спасибо большое! Есть какие-то вопросы?

Вопросы и ответы

Вопрос из зала: Спасибо за доклад. Скажите, вы каким-то образом храните исходные данные? Допустим, логи. Если да, то за какой период? Может ли пользователь, получив отчет, видеть историю за прошлый год и так далее?

Михаил Якшин: Мы совсем не храним исходные логи. Мы храним только логи обработки. Их мы храним, по-моему, минимум в течение трех месяцев (по соглашению – больше). Отчеты мы сейчас храним бессрочно. В основном, всех интересуют отчеты, а не логи.

Вопрос из зала: Отчет – это в смысле агрегация?

Михаил Якшин: Да. Спасибо.

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Илья Космодемьянский

Илья Космодемьянский

CEO и консультант в компании Data Egret, специалист по базам данных PostgreSQL, Oracle, DB2.

Илья Космодемьянский говорит о резервном копировании (Backup) и восстановлении (Recovery) в самых разных системах.

Елена Глухова

Елена Глухова

Руководитель группы общих интерфейсов в Симферополе, Яндекс.

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

Павел Дуров

Павел Дуров

Основатель и разработчик социальной сети Вконтакте.

Павел Дуров (ВКонтакте) отвечает на вопросы разработчиков об одной из самых популярных российских соцсетей.