Примерно год назад, мне довелось работать в одной компании, где я натолкнулся на интересную проблему. Представим себе массивный поток данных об использовании мобильных приложений (десятки миллиардов событий в день), который содержит в себе такую интересную информацию, как дату установки приложения, а также рекламную акцию, которая повлекла за собой эту установку. Имея подобные данные, можно легко разбить пользователей на группы по датам установки и по рекламным акциям, чтобы понять какая из коих была наиболее успешна с точки зрения ROI (return of investment).
Рассмотрим визуальный пример (картинка найдена на просторах интернета):

Как мы видим, пользователи пришедшие с "рекламных щитов" AdWords оказались наиболее лояльными к этому конкретному приложению (продолжали активно пользоваться приложением).
Нет сомнений, что подобные методы невозможно переоценить, когда речь идет о маркетинговой оптимизации, но рассмотрим эту проблему с точки зрения нас — инженеров по работе с данными.
Все запросы такого плана, как правило, заключают в себе условие связанное с датой установки приложения, которая не является временным рядом: в нашем потоке данных присутствуют события как с приложений установленных вчера, так и с приложений установленных 2 года назад. В переводе на язык баз данных, это означает полное сканирование данных начиная с заданной точки и вплоть до текущего времени.
Эволюция решений
Проблема усугублялась тем, что от выбранного решения требовалась поддержка множества параллельных запросов, поэтому аналитические базы данных не рассчитанные на сотни пользователей, работающих одновременно с аналитической панелью нашей компании, пришлось исключить изначально.
Поначалу, для хранения подобных данных использовался MongoDB, но очень скоро мы достигли пределов этой замечательной БД, как во время запросов, так и во время записи. Можно было, конечно, "расшардировать" все это безобразие, но из-за оперативной сложности шардирования MongoDB было решено попробовать другие решения.
Второй попыткой стал Druid, но так, как эта база данных работает с данными сортированными по времени, то это нам мало чем помогло. На самом деле, если пренебречь информацией, поступающей в реальном времени, и каждый день перестраивать базу сортируя данные по дате установки, то это может работать очень даже неплохо.
Базами данных типа Cassandra, которые требуют построения таблицы "под запрос" также пришлось пренебречь из-за желания поддерживать так называемые ad-hoc запросы, когда список полей запроса может варьироваться от пользователя к пользователю.
В итоге, все закончилось выбором MemSQL — базой данных нового поколения (NewSQL), сочетающей в себе поддержку как OLTP, так и OLAP нагрузок, благодаря современной архитектуре, а также благодаря тому, что данные находятся в памяти. Но, как мы понимаем, идеального решения не существует, а потому оставались две проблемы:
1) Скорость записи оставляла желать лучшего из-за использования WAL (write-ahead log), а нам это не особо-то и нужно было, так, как данные сохранялись на S3, и мы были готовы пожертвовать быстрым восстановлением данных ради огромной скорости записи.
2) Цена
Изобретение колеса
Тогда и задумалось написать что-нибудь свое, попроще… Например держать вектор последовательных записей статического размера в памяти, обеспечивая тем самым быстрый по ним проход и фильтрацию по заданному критерию. А еще, разбить все это на процессы, и каждому процессу присвоить свою партицию данных. Так родился ViyaDB. По сути, это аналитическая база данных, которая держит агрегированные данные в памяти в колоночном формате, что позволяет довольно быстро по ним "пробегать". Запросы транслируются в машинный код, что дает избежать ненужных ветвлений и правильнее использовать кэш процессора, при повторном запросе похожего типа.
Так выглядит архитектура ViyaDB с высоты птичьего полета:

Ниже приведен пример использования этой простенькой базы данных.
Тест драйв
Первым делом устанавливаем Consul, который координирует компоненты ViyaDB кластера. Затем, добавляем в Consul необходимую конфигурации кластера.
В ключ "viyadb/clusters/cluster001/config" пишем:
{ "replication_factor": 1, "workers": 32, "tables": ["events"] }
В ключ "viyadb/tables/events/config" пишем:
{ "name": "events", "dimensions": [ {"name": "app_id"}, {"name": "user_id", "type": "uint"}, { "name": "event_time", "type": "time", "format": "millis", "granularity": "day" }, {"name": "country"}, {"name": "city"}, {"name": "device_type"}, {"name": "device_vendor"}, {"name": "ad_network"}, {"name": "campaign"}, {"name": "site_id"}, {"name": "event_type"}, {"name": "event_name"}, {"name": "organic", "cardinality": 2}, {"name": "days_from_install", "type": "ushort"} ], "metrics": [ {"name": "revenue" , "type": "double_sum"}, {"name": "users", "type": "bitset", "field": "user_id", "max": 4294967295}, {"name": "count" , "type": "count"} ], "partitioning": { "columns": [ "app_id" ] } }
(последнее значение представляет собой DDL для нашей таблицы)
Устанавливаем ViyaDB на четыре амазоновских машины типа r4.2xlarge (за основу взят Ubuntu Artful образ от Canonical):
apt-get update apt-get install g++-7 update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 60 --slave /usr/bin/g++ g++ /usr/bin/g++-7 cd /opt curl -sS -L https://github.com/viyadb/viyadb/releases/download/v1.0.0-beta/viyadb-1.0.0-beta-linux-x86_64.tgz | tar -zxf -
Создаем файл конфигурации на каждой из машин (/etc/viyadb/store.json):
{ "supervise": true, "workers": 8, "cluster_id": "cluster001", "consul_url": "http://consul-host:8500", "state_dir": "/var/lib/viyadb" }
(consul-host — адрес машины, на которой установлен Consul)
Стартуем базу данных на всех машинах:
cd /opt/viyadb-1.0.0-beta-linux-x86_64 ./bin/viyadb /etc/viyadb/store.json
Загрузка данных
В базе данных присутствует базовая поддержка языка SQL. Запустим утилиту, позволяющую посылать SQL запросы в базу:
/opt/viyadb-1.0.0-beta-linux-x86_64/bin/vsql viyadb-host 5555
(viyadb-host — один из четырех хостов на которых мы установили ViyaDB)
1 миллиард событий лежит на S3 в формате TSV; для их загрузки запустим команду:
COPY events ( app_id, user_id, event_time, country, city, device_type, device_vendor, ad_network, campaign, site_id, event_type, event_name, organic, days_from_install, revenue, count ) FROM 's3://viyadb-test/events/input/'
Запросы
Справедливости ради, все последующие запросы были запущены во второй раз, чтобы не учитывать время их компиляции в общее время запроса.
1) Типы сообщений и их количество для заданного приложения:
ViyaDB> SELECT event_type,count FROM events WHERE app_id IN ('com.skype.raider', 'com.adobe.psmobile', 'com.dropbox.android', 'com.ego360.flatstomach') ORDER BY event_type DESC event_type count ---------- ----- session 117927202 install 263444 inappevent 200466796 impression 58431 click 297 Time taken: 0.530 secs
2) Пять самых активных городов в штатах по количеству установок приложения:
ViyaDB> SELECT city,count FROM events WHERE app_id='com.dropbox.android' AND country='US' AND event_type='install' ORDER BY count DESC LIMIT 5; city count ---- ----- Clinton 28 Farmington 20 Madison 18 Oxford 18 Highland Park 18 Time taken: 0.171 secs
3) Десять самых активных рекламных агенств по количеству установок, опять же:
ViyaDB> SELECT ad_network, count FROM events WHERE app_id='kik.android' AND event_type='install' AND ad_network <> '' ORDER BY count DESC LIMIT 10; ad_network count ---------- ----- Twitter 1257 Facebook 1089 Google 904 jiva_network 96 yieldlove 79 i-movad 66 barons_media 57 cpmob 50 branovate 35 somimedia 34 Time taken: 0.089 secs
4) Ну и самое главное, ради чего и затевался весь этот сыр бор, это отчет о лояльности пользователей на основе индивидуальных сессий пользователей:
ViyaDB> SELECT ad_network, days_from_install, users FROM events WHERE app_id='kik.android' AND event_time BETWEEN '2015-01-01' AND '2015-01-31' AND event_type='session' AND ad_network IN('Facebook', 'Google', 'Twitter') AND days_from_install < 8 ORDER BY days_from_install, users DESC ad_network days_from_install users ---------- ----------------- ----- Twitter 0 33 Google 0 20 Facebook 0 14 Twitter 1 31 Google 1 18 Facebook 1 13 Twitter 2 30 Google 2 17 Facebook 2 12 Twitter 3 29 Google 3 14 Facebook 3 11 Twitter 4 27 Google 4 13 Facebook 4 10 Twitter 5 26 Google 5 13 Facebook 5 10 Twitter 6 23 Google 6 12 Facebook 6 9 Twitter 7 22 Google 7 12 Facebook 7 9 Time taken: 0.033 secs
Заключение
Иногда, решая какую-нибудь очень специфическую задачу, изобретение колеса может быть оправдано. Тестирование похожего решения показало, что можно сократить расходы примерно в четыре раза по сравнению с MemSQL, при этом не теряя ничего из функционала базы. Конечно, взять и перевести многомиллионный бизнес на базу данных, написанную одним человеком совершенно неоправданно на данном этапе, а поэтому контракт с MemSQL было решено не прерывать. Код, выложенный в Github — это проект, который был начат мной с нуля, уже после ухода из вышеуказанной компании.
