Как Apache Arrow поможет управиться JS с большими данными
Привет. Меня зовут Николай Шувалов, я занимаюсь коммерческим программированием около семи лет, владею Rust, JavaScript, PHP. Сейчас я работаю в отделе данных билайна. Наша платформа позволяет делиться с партнерами данными, не раскрывая их. Например, можно расширить данные с помощью фильтра Блума.
Arrow в сравнении со строковыми форматами
Возьмём простую таблицу, которая состоит из трех столбцов: телефона, даты и имени. Рассмотрим, как она будет выглядеть в строковом и столбчатом форматах. Для строкового формата мы возьмем csv и json, для столбчатого формата структура будет одинаковой. Если же таблица состоит, например, из миллиона строк, а нужно получить имя на строке с номером 10 000, то придется бежать по всей строчке. В json то же самое. А в столбчатом формате ситуация иная — значения привязаны к столбцам. Когда мы хотим получить имя на строке 10 000, то сразу обращаемся к этому столбцу и получаем все его данные.
Существуют RA (random access) файлы, в которых можно пропускать заданное количество строк, но все равно парсеру нужно читать и анализировать пройденные строчки.
Также формат Arrow поддерживает native number, list, dates и другие, а csv и json — text-based форматы, все хранится в строках. Некоторые значения по типу NaN не заменяются, нужны какие-то соглашения. Важно понимать, что в Arrow все данные в бинарном формате, это уменьшает расходы на парсинг. Стоит сказать, что Arrow хранится в оперативной памяти, он ничего не весит, это про runtime. Когда мы хотим что-то сделать, он запускает процесс и работает уже не на жестком диске.
Как появился Apache Arrow
Крупные корпорации давно начали работать с огромными массивами данных. В какой-то момент бизнес захотел получать от этих данных прибыль. Тогда начали появляться разные профессии по работе с данными, такие как аналитики, инженеры данных, которым нужны были какие-то инструменты. Одним из основоположников был Pandas, написанный на Python, затем к нему прибавилась библиотека PyArrow, которая помогает Pandas работать с форматом Arrow. Чуть позже появился Polars, он был намного быстрее Pandas. Polars написан на Rust и в основе использует Apache Arrow.
Вот как система в Apache работала без этого формата.
Было большое количество сервисов, которые хотели обмениваться между собой данными. Когда один сервис хотел передать свои данные другому, ему приходилось работать с сериализацией и десериализацией. Для этого нужны большая производительность и приличное количество времени. Поэтому была придумана схема, показанная справа, смысл которой состоит в том, что представление в памяти должно быть идентично на обоих концах стрелки, так что на сериализацию и десериализацию время уходить не будет.
В итоге мы отказываемся от сериализации и десериализации и просто передаем данные в бинарном формате, благодаря чему экономим большое количество времени. Этот формат служит для того, чтобы эффективно производить аналитические операции, речи о хранении здесь не идет. Можно сказать, что Arrow — это «представление в памяти во время выполнения какой-то операции».
Логический и физический уровни
Обратим внимание на схемы для логического и физического уровней.
Почти все столбчатые форматы начинаются со схемы, которая хранит в себе метаданные о столбцах, то есть из схемы можно определить, что столбец «Телефон» хранит исключительно целочисленные значения. Я расширил тип данных столбца date/name, теперь они могут быть null. В name есть строковые значения в виде chars и null. Важно уточнить, что речь идет о буферной памяти. В аналитических операциях зачастую нужно понимать, сколько в столбцах пустых значений, поэтому сначала вычисляется этот буфер.
На первом физическом уровне идет схема. Она описывает какие данные у нас хранятся в столбцах. Например, колонка phone будет состоять только из целочисленных значений. Колонка date из date и null и так далее. На следующих уровнях будут данные столбцов. Так как колонка date может быть null или date, то для экономии памяти сначала высчитываются все null из колонки и помещаются в отдельный буфер null bits. В аналитических операциях часто нужно понимать сколько пустых значений, поэтому эффективно будет сразу знать где эти null находятся. Столбец phone еще немного интереснее. Данные могут быть chars или null. Сначала также высчитываем null. После идут два буфера, это отступы (offsets) и сами символы (chars). Отступы можно расценивать как карту. Так как в буфере chars все символы склеиваются, и только с помощью буфера offset мы можем восстановить слова.
Где применяется Apache Arrow
Допустим, наш сервис хочет очень быстро получать данные. У нас есть развернутый ClickHouse, это столбчатая база данных. ClickHouse работает с SQL-синтаксисом, у него есть небольшие преимущества, например, возможность добавить формат. Пишем формат «Arrow», скачиваем с ClickHouse данные уже в нем и далее производим аналитические операции. Также Arrow используется в вебе, примеры рассмотрим дальше.
Реализация
Apache Arrow реализован почти на всех языках программирования. Apache Arrow JS отличается тем, что он работает в вебе. Библиотека написана на TypeScript, доступна для node и браузера, есть поддержка ESM/CJS. Сравним запрос и получение данных от csv/json и от библиотеки Arrow. Допустим, у нас есть приложение, которое запрашивает данные от сервера и после небольшого времени ожидания получает ответ. Начинается загрузка данных. Так как Arrow хранит все данные в бинарном формате, он уже будет быстрее скачиваться. Из коробки идет поддержка streaming format, поэтому можно начинать обработку данных. В csv/json нам нужно еще сделать парсинг и десериализовать, чтобы было понятно, с какой структурой мы работаем.
Рассмотрим загрузку более подробно.
Передача данных потоковая, поэтому сначала приходит один батч. Как только он пришел, с ним можно начинать работать. Так потихоньку приходит батч за батчем, вычисляются запросы. Мы уже заканчиваем вычисления, а в csv/json до момента вычислений должен быть еще парсинг.
Как ускорить csv/json
Можно использовать gRPC c Protobuf.
Protocol Buffers представляет собой двоичный формат данных. Для десериализации у нас будет отдельный .proto-файл на клиенте и на сервере, сюда же можно прикрутить потоковую передачу данных. При этом все равно нужно будет понимать, какая структура данных. Также, так как это строковый формат, придется тратить время на обработку строк вместо того, чтобы бегать по столбцам.
Как это выглядит в коде
Импортируем Apache Arrow JS. На шестой строчке я показываю, как можно получать данные с сервера, однако у меня не было подходящего сервера, который мог бы отдавать Arrow. Поэтому я просто скопировал бинарник, вставил его в файлик и прочитал. Получаем классический reader, делим его на батчи. Хотим, например, посмотреть столбец name. Идем по строкам, не забываем, что это бинарный формат, поэтому нам нужно декодировать. Ради интереса посмотрим, сколько строк приходит в один батч, и как выглядит сам датасет в формате Arrow.
Мы видим схему сверху, дальше все в бинарном формате, это можно декодировать. Видно, что в первом батче всего одна строка, во втором батче уже семь оставшихся. Затем получаем данные уже в читаемом виде.
Про WASM
Так как этот формат реализован почти во всех языках программирования, то почему бы не скомпилировать пакет Rust Apache Arrow в WebAssembly? Такой проект был, оказалось, что он много весит и слишком медленный. Главная проблема в скорости контекста между JS и WASM. При этом Apache Arrow JS еще может быть отличным инструментом для связи между контекстами JS и WebAssembly. Вот график скачиваний Apache Arrow и WASM, из которого видно, что вариант с WebAssembly неконкурентоспособен.
Работа с DuckDB
Теперь посмотрим DuckDB — библиотеку для баз данных, реализованную на C++. Она скомпилирована с помощью WebAssembly, туда добавлен пакет Apache Arrow JS. Посмотрим, как быстро это все отрабатывает.
Основные команды можно посмотреть с помощью .help. Есть возможность как добавлять файлы из компьютера, так и делать на них ссылку. Я подготовил файл с синтетическими табличными данными, добавим его. Напишем простые запросы: посмотрим, сколько в датасете строк, и выведем все имена. Мало того, что данные синтетические, так мы еще и в хэш загнали. То есть мы ничего не поймем. Срабатывает все очень быстро.
Теперь рассмотрим схему.
На ней WebAssembly запущен в отдельном треде Worker, а в JS Context пишется SQL-запрос, передается в DuckDB Client и отправляется в DuckDB. После обработки данных Apache Arrow передает их в JS Context и появляется визуализация. Именно в этом формате Apache Arrow выступает как связь между контекстами.
Если рассматривать другие примеры использования, то один из самых популярный сейчас — библиотека Graphistry, которую поддерживает Nvidia. Из их продукта мы видим, как с помощью Apache Arrow JS визуализируются данные. Кроме того, можно вспомнить про D3, который применяется в той же сфере. Apache Arrow JS очень активно используется с этой библиотекой для быстрой визуализации.
Есть еще несколько известных в узких кругах проектов: Falcon и Vega-loader, которые работают все с той же визуализацией.