Благодаря анализу в реальном времени мы, сотрудники компании Uber, получаем представление о состоянии дел и эффективности работы и на основе данных решаем, как повысить качество работы на платформе Uber. Например, проектная команда мониторит состояние рынка и выявляет потенциальные проблемы на нашей платформе; программное обеспечение на базе моделей машинного обучения прогнозирует предложения пассажиров и спрос на водителей; специалисты по обработке данных улучшают модели машинного обучения — чтобы, в свою очередь, повысить качество прогнозирования.  В прошлом для анализа в реальном времени мы использовали решения по базам данных от других компаний, но ни одно не отвечало всем нашим критериям функциональности, масштабируемости, эффективности, стоимости и эксплуатационным требованиям. Выпущенный в ноябре 2018 года AresDB представляет собой инструмент анализа в реальном времени с открытым исходным кодом. Он использует нетрадиционный источник питания, графические процессоры (GPU), что позволяет увеличивать масштаб анализа. Технология GPU, перспективный инструмент анализа в реальном времени, за последние годы значительно продвинулась, что делает ее идеальной для параллельных вычисления в реальном времени и обработки данных. В следующих разделах мы опишем структуру AresDB и то, как это интересное решение для анализа в реальном времени позволило нам эффективнее и более рационально унифицировать, упростить и усовершенствовать решения по базам данных Uber для анализа в реальном времени. Надеемся, после прочтения данной статьи вы опробуете AresDB в рамках собственных проектов и также убедитесь в его полезности!  Анализ данных принципиально важен для успешной деятельности Uber. Помимо других функций, аналитические инструменты используются для решения следующих задач: Мы распределяем эти функции по категориям с различными требованиями следующим образом:  Информационные панели и системы принятия решений используют системы анализа в реальном времени, чтобы создавать аналогичные запросы по относительно небольшим, но обладающим высокой важностью подмножествам данных (с максимальным уровнем актуальности данных) при высоком QPS и низком запаздывании. Наиболее распространенная проблема, для решения которой в Uber используются инструменты анализа в реальном времени, — это вычисления совокупностей временных рядов. Эти вычисления дают представление о взаимодействии с пользователями, чтобы мы могли соответствующим образом повышать качество услуг. На их основе мы запрашиваем показатели по определенным параметрам (например, день, час, идентификатор города и статус поездки) за определенный период времени для произвольно отфильтрованных (или иногда объединенных) данных. За прошедшие годы компания Uber развернула несколько систем, направленных на решение этой задачи различными способами. Вот некоторые сторонние решения, которые мы использовали для решения проблем этого типа: Хотя эти технологии имеют свои сильные стороны, им не хватало некоторых функций, необходимых для нашего варианта использования. Нам нужно было унифицированное, упрощенное и оптимизированное решение, и в его поисках мы работали в нестандартном направлении (точнее, внутри GPU). Для реалистичного рендеринга изображений с высокой частотой кадров графические процессоры параллельно обрабатывают огромное количество форм и пикселей на высокой скорости. Хотя тенденция к увеличению тактовой частоты блоков обработки данных за последние несколько лет пошла на убыль, число транзисторов в микросхеме увеличилось только по закону Мура. В результате скорость вычислений GPU, измеряемая в гигафлопсах в секунду (Гфлопс/с), быстро увеличивается. На рисунке 1 ниже представлено сравнение тенденции теоретической скорости (Гфлопс/с) GPU от NVIDIA и CPU от Intel на протяжении ряда лет:  Рисунок 1. Сравнение эффективности CPU и GPU по одинарной точности с плавающей запятой на протяжении нескольких лет. Изображение взято из руководства по программированию CUDA C от Nvidia. При разработке механизма запроса анализа в реальном времени решение об интеграции графического процессора было естественным. В Uber типичный запрос анализа в реальном времени требует обработки данных за несколько дней с миллионами, а то и миллиардами записей, затем их фильтрации и обобщения за короткий период времени. Эта вычислительная задача идеально вписывается в модель параллельной обработки GPU общего назначения, поскольку они: Остановившись на использовании аналитической базы данных на основе GPU, мы — с позиции своих потребностей — оценили несколько существующих аналитических решений, в которых используются GPU: В целом, эти системы демонстрируют огромное преимущество и потенциал обработки данных с использованием технологии GPU, и они вдохновили нас на создание нашего собственного решения по анализу в реальном времени на базе GPU, адаптированного к потребностям Uber. Опираясь на эти концепции, мы разработали и открыли исходный код AresDB. На высоком уровне AresDB хранит большую часть данных в памяти хоста (RAM, которая связана с CPU), использует CPU для обработки принятых данных и диски для восстановления данных. В период запроса AresDB передает данные из памяти хоста в память GPU для параллельной обработки в GPU. Как показано на рисунке 2 ниже, AresDB включает хранилище памяти, хранилище метаданных и диск: 
Рисунок 2. Уникальная архитектура AresDB включает хранилище памяти, диск и хранилище метаданных. В отличие от большинства систем управления реляционными базами данных (RDBMS), в AresDB отсутствует область действия базы данных или схемы. Все таблицы относятся к одной и той же области действия в одном кластере/экземпляре AresDB, что позволяет пользователям обращаться к ним напрямую. Пользователи хранят свои данные в виде таблиц фактов и таблиц измерений. Таблица фактов В таблице фактов хранится бесконечный поток событий временных рядов. Пользователи используют таблицу фактов для хранения событий/фактов, которые происходят в реальном времени, и каждое событие связано со временем события, при этом запрос таблицы часто производится по времени события. В качестве примера того типа информации, который хранится в таблице фактов, можно назвать поездки, где каждая поездка представляет собой событие, а время запроса на поездку часто обозначается как время события. Если с событием связано несколько временных меток, только одна временная метка указывается в качестве времени события и отображается в таблице фактов. Таблица измерения В таблице измерения хранятся текущие характеристики объектов (включая города, клиентов и водителей). Например, пользователи могут хранить информацию о городе, в частности название города, временную зону и страну, в таблице измерения. В отличие от таблиц фактов, которые постоянно увеличиваются, таблицы измерений всегда ограничены по размеру (например, для Uber таблица городов ограничена фактическим количеством городов в мире). Таблицы измерений не требуют наличия специального столбца времени. В таблице ниже представлены текущие типы данных, которые поддерживаются в AresDB:  В AresDB строки конвертируются в перечислимые типы (enums) автоматически перед попаданием в базу данных, чтобы повысить удобство хранения и эффективность запросов. Это позволяет выполнять проверку равенства с учетом регистра, но не поддерживает такие продвинутые операции, как конкатенация, подстроки, маски и сопоставление регулярных выражений. В будущем мы намерены добавить опцию поддержки полной строки. Архитектура AresDB поддерживает следующие функции: Вектор AresDB хранит все данные в столбчатом формате. Значения каждого столбца хранятся в виде вектора значения столбца. Маркер достоверности/неопределенности значений в каждом столбце хранится в отдельном нулевом векторе, при этом маркер достоверности каждого значения представлен в виде одного бита. Активное хранилище AresDB хранит несжатые и неотсортированные данные столбцов (активные векторы) в активном хранилище. Записи данных в активном хранилище делятся на (активные) пакеты заданного объема. Новые пакеты создаются при приеме данных, в то время как старые пакеты удаляются после архивации записей. Индекс первичного ключа используется для определения местоположения записей для дедупликации и обновления. На рисунке 3 ниже показано, как мы организуем активные записи и используем значение первичного ключа для определения их местоположения: 
Рисунок 3. Мы используем значение первичного ключа для определения местоположения пакета и позиции каждой записи внутри пакета. Значения каждого столбца в пакете хранятся в виде вектора столбца. Маркер достоверности/неопределенности значений в каждом векторе значения хранится в виде отдельного нулевого вектора, при этом маркер достоверности каждого значения представлен в виде одного бита. На рисунке 4 ниже мы предлагаем пример с пятью значениями для столбца city_id: 
Рисунок 4. Мы храним значения (фактическое значение) и нулевые вектора (маркер достоверности) несжатых столбцов в таблице данных. AresDB также хранит законченные, отсортированные и сжатые столбчатые данные (векторы архивов) в архивном хранилище через таблицы фактов. Записи в архивном хранилище также распределяются по пакетам. В отличие от активных пакетов, архивный пакет хранит записи за день по всемирному координированному времени (UTC). Архивный пакет использует количество дней в качестве идентификатора пакета еще со времен Unix Epoch. Записи хранятся в рассортированном виде в соответствии с заданным пользователем порядком сортировки столбцов. Как показано на рисунке 5 ниже, мы производим сортировку сначала по столбцу city_id, а затем по столбцу состояния: 
Рисунок 5. Мы сортируем все строки по city_id, затем по состоянию, после чего сжимаем каждый столбец путем группового кодирования. После сортировки и сжатия каждый столбец получит учетный вектор. Цель настройки порядка сортировки столбцов пользователем заключается в следующем: Столбец сжимается только в том случае, если он присутствует в заданном пользователем порядке сортировки. Мы не пытаемся сжимать столбцы с большим количеством элементов, поскольку экономию памяти это дает незначительную. После сортировки данные для каждого квалифицированного столбца сжимаются с помощью определенного варианта группового кодирования. Помимо вектора значения и нулевого вектора мы вводим учетный вектор для повторного представления того же значения. Клиенты получают данные через HTTP API путем публикации пакета обновления. Пакет обновления является специальным упорядоченным двоичным форматом, который минимизирует использование пространства, сохраняя при этом произвольный доступ к данным. Когда AresDB получает пакет обновления, он сначала записывает пакет обновления в журнал регистрации операций для восстановления. Когда в конец журнала регистрации событий добавляется пакет обновлений, AresDB идентифицирует и пропускает поздние записи в таблицах фактов для использования в активном хранилище. Запись считается «поздней», если время события расположено раньше архивного времени события отключения. В отношении записей, которые не считаются «поздними», AresDB использует индекс первичного ключа для определения местоположения пакета внутри активного хранилища, куда их нужно вставить. Как показано на рисунке 6 ниже, новые записи (не встречавшиеся ранее на базе значения первичного ключа) вставляются в пустое пространство, а существующие записи обновляются напрямую: 
Рисунок 6. При получении данных, после добавления пакета обновления в журнал регистрации событий, «поздние» записи добавляются в обратную очередь, а прочие записи — в активное хранилище. При получении данных записи либо добавляются/обновляются в активном хранилище, либо добавляются в обратную очередь, ожидающую помещения в архивное хранилище. Мы периодически запускаем плановый процесс, обозначаемый как архивирование, в отношении записей активного хранилища для присоединения новых записей (записей, которые ранее никогда не архивировались) к архивному хранилищу. Процесс архивирования только обрабатывает записи в активном хранилище со временем события в диапазоне между старым временем отключения (время отключения с последнего процесса архивирования) и новым временем отключения (новое время отключения на основе параметра задержки архивирования в схеме таблицы). Время события записей используется для определения того, в какой архивный пакет должны быть объединены записи при пакетировании архивных данных в ежедневные пакеты. Архивирование не требует дедупликации индекса значения первичного ключа при объединении, поскольку архивируются только записи в диапазоне между старым и новым временем отключения. На рисунке 7 ниже представлен график согласно времени события определенной записи. 
Рисунок 7. Мы используем время события и время отключения для определения записей как новых (активных) и старых (время события расположено раньше архивного времени события отключения). В этом случае интервал архивирования представляет собой временной промежуток между двумя процессами архивирования, а задержка архивирования представляет собой период после времени события, но до момента возможного архивирования события. Оба параметра определяются в настройках схемы таблицы AresDB. Как показано на рисунке 7 выше, старые записи (время события которых расположено раньше архивного времени события отключения) для таблиц фактов добавляются в обратную очередь и в конечном итоге обрабатываются в рамках процесса обратного заполнения. Триггерами этого процесса также являются время или размер обратной очереди, если она достигает порогового уровня. По сравнению с процессом добавления данных в активное хранилище обратное заполнение является асинхронным и относительно более дорогостоящим с точки зрения ресурсов CPU и памяти. Обратное заполнение используется в следующих сценариях: В отличие от архивирования, процесс обратного заполнения является идемпотентным и требует дедупликации на основе значения первичного ключа. Заполняемые данные в конечном итоге будут видны для запросов. Обратная очередь поддерживается в памяти с предварительно заданным размером, и при большой нагрузке обратного заполнения процесс будет заблокирован для клиента до очистки очереди путем запуска процесса обратного заполнения. В текущем варианте реализации пользователю нужно использовать язык Ares Query Language (AQL), созданный Uber для выполнения запросов в AresDB. AQL — это эффективный язык для аналитических запросов по временным рядам и не следует стандартному синтаксису SQL типа «SELECT FROM WHERE GROUP BY», как другие схожие с SQL языки. Вместо этого AQL используется в структурированных полях и может входить в объекты JSON, YAML и Go. Например, вместо ВЫБРАТЬ/SELECT пункт(*) ИЗ/FROM ГРУППЫ поездок ПО/GROUP BY city_id, ГДЕ/WHERE статус = «завершено» И/AND request_at >= 1512000000, эквивалентный вариант AQL в JSON записывается следующим образом: В формате JSON AQL предлагает разработчикам информационной панели и системы принятия решений более удобный алгоритм программного запроса, чем SQL, позволяя им легко составлять запросы и манипулировать ими с помощью кода, не беспокоясь о таких вещах, как внедрение SQL. Он выступает в качестве универсального формата запросов для типичных архитектур веб-браузеров, внешних и внутренних серверов вплоть до базы данных (AresDB). Кроме того, AQL предоставляет удобный синтаксический прием для фильтрации по времени и пакетирования с поддержкой собственной временной зоны. Кроме того, язык поддерживает ряд функций, например неявные подзапросы, для предотвращения распространенных ошибок в запросах и облегчает процессы анализа и перезаписи запросов для разработчиков внутреннего интерфейса. Несмотря на множество преимуществ, которые предлагает AQL, мы прекрасно понимаем, что большинство инженеров лучше знакомо с SQL. Предоставление интерфейса SQL для выполнения запросов является одним из следующих шагов, которые мы рассмотрим в рамках работы по улучшению взаимодействия с пользователями AresDB. Схема выполнения запроса AQL представлена на рисунке 8 ниже: 
Рисунок 8. В схеме выполнения запросов AresDB используется наш собственный язык запросов AQL для быстрой и эффективной обработки и извлечения данных. Запрос AQL компилируется во внутренний контекст запроса. Выражения в фильтрах, измерениях и параметрах анализируются в абстрактных синтаксических деревьях (AST) для последующей обработки через графический процессор (GPU). В AresDB используются предварительные фильтры для дешевой фильтрации архивных данных перед их отправкой в GPU для параллельной обработки. Поскольку архивные данные сортируются в соответствии с настроенным порядком столбцов, некоторые фильтры могут использовать этот порядок сортировки и метод двоичного поиска для установления соответствующего диапазона соответствия. В частности, эквивалентные фильтры для всех изначально отсортированных Х столбцов и дополнительный фильтр диапазона для отсортированных столбцов X+1 могут использоваться в качестве предварительных фильтров, как показано на рисунке 9 ниже. 
Рисунок 9. AresDB производит предварительную фильтрацию данных столбцов перед их отправкой в GPU для обработки. После предварительной фильтрации только зеленые значения (отвечающие условию фильтра) должны направляться в GPU для параллельной обработки. Входные данные загружаются в GPU и обрабатываются по одному пакету за раз. Сюда относятся как активные пакеты, так и архивные пакеты. В AresDB используются потоки CUDA для конвейерной загрузки и обработки данных. По каждому запросу поочередно применяются два потока для обработки в два перекрывающихся этапа. На рисунке 10 ниже мы предлагаем график, иллюстрирующий этот процесс. 
Рисунок 10. В AresDB два потока CUDA поочередно занимаются передачей и обработкой данных. Для простоты в AresDB используется библиотека Thrust для реализации процедур выполнения запроса, которая предлагает блоки точно настроенного параллельного алгоритма для быстрой реализации в текущем инструменте запросов. В Thrust входные и выходные векторные данные оцениваются с помощью случайных итераторов доступа. Каждый поток GPU ищет входные итераторы в своей рабочей позиции, считывает значения и выполняет вычисления, а затем записывает результат в соответствующую позицию в выходном итераторе. Для вычисления выражений AresDB следует модели «один оператор на одно ядро» (OOPK). На рисунке 11 ниже эта процедура демонстрируется на примере AST, сгенерированного из выражения размерности request_at – request_at % 86400 на этапе компиляции запроса: 
Рисунок 11. AresDB использует модель OOPK для вычисления выражений. В модели OOPK механизм запросов AresDB обходит каждую конечную ноду дерева AST и возвращает итератор для исходной ноды. Если корневая нода также является конечной, действие корня выполняется непосредственно на входном итераторе. Для каждой некорневой неконцевой ноды (операция по модулю в этом примере) временный вектор рабочего пространства выделяется для хранения промежуточного результата, полученного из выражения request_at% 86400. С помощью Thrust запускается функция ядра для вычисления результата по этому оператору в GPU. Результаты хранятся в итераторе рабочего пространства. Для корневой ноды функция ядра запускается таким же образом, как и для некорневой, неконечной ноды. Различные выходные действия выполняются на основе типа выражения, что подробно описано ниже: После вычисления выражения выполняются сортировка и преобразование для окончательного объединения данных. В операциях сортировки и преобразования мы используем значения вектора измерений в качестве ключевых значений сортировки и преобразования, а значения вектора параметров — в качестве значений для объединения данных. Таким образом, строки с аналогичными значениями измерений группируются и объединяются. На рисунке 12 ниже показан этот процесс сортировки и преобразования. 
Рисунок 12. После вычисления выражения AresDB сортирует и преобразует данные по ключевым значениям векторов измерений (ключевое значение) и параметров (значение). AresDB также поддерживает следующие продвинутые функции запроса: Будучи базой данных на основе внутренней памяти, AresDB должен управлять следующими типами использования памяти:  При запуске AresDB в работу он использует настроенный общий бюджет памяти. Бюджет делится на все шесть типов памяти и также должен оставлять достаточно пространства для операционной системы и других процессов. Этот бюджет также включает статически настроенную оценку перегрузки, активное хранилище данных, отслеживаемое сервером, и архивные данные, которые сервер может решить загрузить и удалить в зависимости от оставшегося бюджета памяти.
На рисунке 13 ниже представлена модель памяти хоста AresDB. 
Рисунок 13. AresDB управляет собственным использованием памяти, чтобы оно не превышало настроенный общий бюджет процесса. AresDB позволяет пользователям настроить дни предварительной загрузки и приоритеты на уровне столбцов для таблиц фактов и производит предварительную загрузку архивных данных только в дни предварительной загрузки. Данные, не загруженные предварительно, загружаются в память из диска по требованию. При заполнении AresDB также удаляет архивные данные из памяти хоста. Принципы удаления AresDB опираются на следующие параметры: количество дней предварительной загрузки, приоритеты столбцов, день составления пакета и размер столбца. AresDB также управляет несколькими устройствами GPU и моделирует ресурсы устройства как потоки GPU и память устройства, отслеживая использование памяти GPU для обработки запросов. AresDB управляет устройствами GPU через менеджер устройств, который моделирует ресурсы устройства GPU в двух измерениях (потоки GPU и память устройства) и отслеживает использование памяти при обработке запросов. После компиляции запроса AresDB позволяет пользователям оценить объем ресурсов, необходимых для выполнения запроса. Требования по памяти устройства должны быть выполнены до того, как будет разрешен запрос; если в данный момент на любом устройстве недостаточно памяти, запрос должен ожидать выполнения. В настоящее время AresDB может выполнять один или несколько запросов на одном устройстве GPU одновременно, если устройство отвечает всем требованиям по ресурсам. В текущем варианте реализации AresDB не кеширует входные данные в памяти устройства для повторного использования в нескольких запросах. AresDB нацелен на поддержку запросов к наборам данных, которые постоянно обновляются в режиме реального времени и слабо поддаются корректному кешированию. В будущих версиях AresDB мы намерены реализовать функции кеширования данных в памяти GPU, что поможет оптимизировать производительность запросов. В Uber мы используем AresDB для создания информационных панелей для получения бизнес-информации в режиме реального времени. AresDB отвечает за хранение первичных событий с постоянными обновлениями и вычисление критических метрик для них за доли секунды благодаря ресурсам GPU при низких затратах, таким образом, пользователи могут использовать информационные панели в интерактивном режиме. Например, анонимизированные данные о поездках, которые имеют длительный срок действия в хранилище данных, обновляются несколькими службами, включая нашу диспетчерскую систему, системы оплаты и оценки. Для эффективного использования данных о поездках пользователи разделяют и разбивают данные по разным измерениям, чтобы получить представление о решениях в реальном времени. При использовании AresDB сводная информационная панель Uber — это широко распространенная информационная панель анализа, которая применяется командами внутри компании для получения соответствующих произведений метрик и реагирования в реальном времени для улучшения взаимодействия с пользователями. 
Рисунок 14. В почасовом режиме обзорная информационная панель Uber использует AresDB для просмотра аналитики данных в реальном времени в течение определенных периодов. Чтобы создать макет информационной панели, приведенной выше, мы смоделировали следующие таблицы: Поездки (таблица фактов)   Города (таблица измерений)  Для создания двух моделируемых таблиц, описанных выше, сначала нам нужно создать таблицы в AresDB по следующим схемам:  Как описано в схеме, таблицы поездок создаются в качестве таблиц фактов, которые отражают события поездок, происходящие в реальном времени, а таблицы городов создаются в качестве таблиц измерений, в которых хранится информация о фактических городах. После создания таблиц пользователи могут использовать клиентскую библиотеку AresDB для загрузки данных из шины событий, например Apache Kafka, или с платформ потоковой или пакетной обработки, например Apache Flink или Apache Spark. В пробных информационных панелях мы выбираем в качестве примера две метрики, «общий тариф на поездку» и «активные водители». В информационной панели пользователи могут отфильтровать город по метрикам, например Сан-Франциско. Для отображения временных рядов по этим двум метрикам за последние 24 часа в информационных панелях можно запустить следующие запросы AQL:  Пример результатов по запросу:
Результатом приведенных выше пробных запросов будут следующие временные ряды, которые можно легко представить на графиках временных рядов, как показано ниже.   В приведенном выше примере мы продемонстрировали, как можно использовать AresDB для приема первичных событий, происходящих в режиме реального времени, в течение нескольких секунд и немедленного выполнения произвольных пользовательских запросов данных для вычисления метрик за доли секунды. AresDB помогает инженерам в создании средств обработки данных, которые определяют критически важные для бизнеса метрики, что требует понимания человеческих или автоматических решений в реальном времени. AresDB широко используется в Uber для поддержки информационных панелей анализа данных в реальном времени, что позволяет принимать соответствующие решения на основе данных по множеству аспектов бизнеса. Открывая исходный код этого инструмента, мы надеемся, что другие представители сообщества смогут использовать AresDB в своих аналитических целях. В будущем мы намерены добавить в проект следующие функции: AresDB имеет открытый исходный код по лицензии Apache. Мы приглашаем вас попробовать AresDB и присоединиться к нашему сообществу. Если вас интересует разработка технологий крупномасштабного анализа данных в реальном времени, подумайте о подаче заявки на работу в нашей команде. Особая благодарность Кейт Чанг (Kate Zhang), Дженнифер Андерсон (Jennifer Anderson), Нихилу Джоши (Nikhil Joshi), Аби Куну (Abhi Khune), Шене Цзи (Shengyue Ji), Чинмэю Сомэну (Chinmay Soman), Сяну Фу (Xiang Fu), Дэвиду Чену (David Chen) и Ли Нингу (Li Ning) за то, что обеспечили невероятный успех этого проекта!