Перейти к содержимому
Последнее обновление

Использование Doris и Hudi

Новая открытая архитектура управления данными Data Lakehouse объединяет высокую производительность и возможности работы в реальном времени, свойственные хранилищам данных, с низкой стоимостью и гибкостью озёр данных. Это позволяет пользователям удобнее решать разнообразные задачи обработки и анализа данных. Такая архитектура всё шире применяется в корпоративных системах больших данных.

В последних версиях Apache Doris расширил интеграцию с озёрами данных и стал зрелым решением для построения Data Lakehouse.

  • Начиная с версии 0.15, в Apache Doris появились внешние таблицы Hive и Iceberg, что позволило исследовать возможности совместного использования с Apache Iceberg для работы с озёрами данных.
  • Начиная с версии 1.2, Apache Doris официально представил функцию Multi-Catalog, обеспечивающую автоматическое сопоставление метаданных и доступ к данным из различных источников, а также многочисленные оптимизации производительности чтения внешних данных и выполнения запросов. Теперь Doris обладает всеми возможностями для построения быстрой и удобной архитектуры Lakehouse.
  • В версии 2.1 архитектура Data Lakehouse в Apache Doris была значительно улучшена: расширены возможности чтения и записи основных форматов озёр данных (Hudi, Iceberg, Paimon и др.), добавлены совместимость с несколькими диалектами SQL и возможность беспрепятственной миграции из существующих систем в Apache Doris. Для задач науки о данных и чтения больших объёмов данных в Doris интегрирован высокоскоростной интерфейс чтения Arrow Flight, благодаря чему эффективность передачи данных выросла в 100 раз.

Построение Lakehouse с помощью Doris и Hudi

Apache Doris и Hudi​

Apache Hudi сегодня является одним из самых популярных открытых форматов озёр данных и платформой управления транзакционными озёрами данных. Он поддерживает различные распространённые движки запросов, включая Apache Doris.

Apache Doris также расширил возможности чтения таблиц данных Apache Hudi:

  • Поддержка таблиц Copy on Write: Snapshot Query
  • Поддержка таблиц Merge on Read: Snapshot Queries, Read Optimized Queries
  • Поддержка Time Travel
  • Поддержка Incremental Read

Высокая производительность выполнения запросов в Apache Doris в сочетании с возможностями Apache Hudi по управлению данными в реальном времени позволяет эффективно, гибко и с низкими затратами выполнять запросы и анализировать данные. Это сочетание также обеспечивает надёжные возможности отслеживания происхождения данных, аудита и инкрементальной обработки. Совместное использование Apache Doris и Apache Hudi уже проверено и внедрено несколькими пользователями сообщества в реальных бизнес-сценариях:

  • Анализ и обработка данных в реальном времени: типичные сценарии, такие как обновление данных и выполнение аналитических запросов в реальном времени в финансовой сфере, рекламе и электронной коммерции, требуют обработки данных в реальном времени. Hudi обеспечивает обновление данных и управление ими в реальном времени, сохраняя их согласованность и надёжность. Doris эффективно обрабатывает запросы к большим объёмам данных в реальном времени. Вместе они позволяют успешно решать задачи анализа и обработки данных в реальном времени.
  • Отслеживание происхождения данных и аудит: в отраслях с высокими требованиями к безопасности и точности данных, таких как финансы и здравоохранение, отслеживание происхождения данных и аудит имеют решающее значение. Hudi предоставляет функцию Time Travel для просмотра исторических состояний данных. В сочетании с эффективным выполнением запросов в Apache Doris это позволяет быстро анализировать данные на любой момент времени для точного отслеживания их происхождения и аудита.
  • Инкрементальное чтение и анализ данных: при масштабном анализе данных часто возникают трудности из-за больших объёмов данных и частых обновлений. Hudi поддерживает инкрементальное чтение, позволяя пользователям обрабатывать только изменённые данные без полного обновления всего набора. Функция Incremental Read в Apache Doris дополнительно улучшает этот процесс, значительно повышая эффективность обработки и анализа данных.
  • Федеративные запросы к разным источникам данных: во многих компаниях данные поступают из множества источников и хранятся в разных базах данных. Функция Multi-Catalog в Doris поддерживает автоматическое сопоставление и синхронизацию различных источников данных, позволяя выполнять федеративные запросы к ним. Это существенно сокращает путь передачи данных и повышает эффективность работы компаний, которым необходимо извлекать и объединять данные из нескольких источников для анализа.

В этой статье мы покажем, как быстро развернуть тестовую и демонстрационную среду Apache Doris + Apache Hudi в Docker, и рассмотрим различные операции, которые помогут быстро приступить к работе.

Дополнительные сведения см. в разделе Hudi Catalog

Руководство пользователя​

Все скрипты и код, упомянутые в этой статье, доступны по адресу: https://github.com/apache/doris/tree/master/samples/datalake/hudi

01 Подготовка среды​

В этой статье для развёртывания используется Docker Compose со следующими компонентами и версиями:

КомпонентВерсия
Apache DorisПо умолчанию 2.1.4, можно изменить
Apache Hudi0.14
Apache Spark3.4.2
Apache Hive2.1.3
MinIO2022-05-26T05-48-41Z

02 Развёртывание среды​

  1. Создайте сеть Docker

    sudo docker network create -d bridge hudi-net

  2. Запустите все компоненты

    sudo ./start-hudi-compose.sh

    Примечание: перед запуском можно изменить значения DORIS_PACKAGE и DORIS_DOWNLOAD_URL в start-hudi-compose.sh, чтобы выбрать нужную версию Doris. Рекомендуется использовать версию 2.1.4 или выше.

  3. После запуска можно использовать следующий скрипт для входа в командную строку Spark или Doris:

    -- Spark
    sudo ./login-spark.sh

    -- Doris
    sudo ./login-doris.sh

03 Подготовка данных​

Теперь создадим данные Hudi с помощью Spark. Как показано в коде ниже, в кластере уже есть таблица Hive с именем customer. На её основе можно создать таблицу Hudi:

-- ./login-spark.sh
spark-sql> use default;

-- create a COW table
spark-sql> CREATE TABLE customer_cow
USING hudi
TBLPROPERTIES (
type = 'cow',
primaryKey = 'c_custkey',
preCombineField = 'c_name'
)
PARTITIONED BY (c_nationkey)
AS SELECT * FROM customer;

-- create a MOR table
spark-sql> CREATE TABLE customer_mor
USING hudi
TBLPROPERTIES (
type = 'mor',
primaryKey = 'c_custkey',
preCombineField = 'c_name'
)
PARTITIONED BY (c_nationkey)
AS SELECT * FROM customer;

04 Запросы к данным​

Как показано ниже, в кластере Doris уже создан Catalog с именем hudi (его можно просмотреть с помощью SHOW CATALOGS). Ниже приведён оператор создания этого Catalog:

-- Already created, no need to execute again
CREATE CATALOG `hudi` PROPERTIES (
"type"="hms",
'hive.metastore.uris' = 'thrift://hive-metastore:9083',
"s3.access_key" = "minio",
"s3.secret_key" = "minio123",
"s3.endpoint" = "http://minio:9000",
"s3.region" = "us-east-1",
"use_path_style" = "true"
);
  1. Вручную обновите этот Catalog, чтобы синхронизировать созданную таблицу Hudi:

    -- ./login-doris.sh
    doris> REFRESH CATALOG hudi;
  2. Операции с данными Hudi, выполняемые через Spark, сразу видны в Doris без необходимости обновлять Catalog. Вставим по строке данных в таблицы COW и MOR с помощью Spark:

    spark-sql> insert into customer_cow values (100, "Customer#000000100", "jD2xZzi", "25-430-914-2194", 3471.59, "BUILDING", "cial ideas. final, furious requests", 25);
    spark-sql> insert into customer_mor values (100, "Customer#000000100", "jD2xZzi", "25-430-914-2194", 3471.59, "BUILDING", "cial ideas. final, furious requests", 25);
  3. Через Doris можно напрямую запросить только что вставленные данные:

    doris> use hudi.default;
    doris> select * from customer_cow where c_custkey = 100;
    doris> select * from customer_mor where c_custkey = 100;
  4. Вставьте с помощью Spark данные с уже существующим значением c_custkey=32, перезаписав тем самым существующие данные:

    spark-sql> insert into customer_cow values (32, "Customer#000000032_update", "jD2xZzi", "25-430-914-2194", 3471.59, "BUILDING", "cial ideas. final, furious requests", 15);
    spark-sql> insert into customer_mor values (32, "Customer#000000032_update", "jD2xZzi", "25-430-914-2194", 3471.59, "BUILDING", "cial ideas. final, furious requests", 15);
  5. С помощью Doris можно запросить обновлённые данные:

    doris> select * from customer_cow where c_custkey = 32;
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
    | c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
    | 32 | Customer#000000032_update | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 15 |
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
    doris> select * from customer_mor where c_custkey = 32;
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
    | c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
    | 32 | Customer#000000032_update | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 15 |
    +-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+

05 Incremental Read​

Incremental Read — одна из функций Hudi. Она позволяет получать инкрементальные данные за указанный временной диапазон и выполнять их инкрементальную обработку. В данном примере Doris может запросить данные, изменённые после вставки c_custkey=100. Как показано ниже, мы вставили данные с c_custkey=32:

doris> select * from customer_cow@incr('beginTime'='20240603015018572');
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
| c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
| 32 | Customer#000000032_update | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 15 |
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
spark-sql> select * from hudi_table_changes('customer_cow', 'latest_state', '20240603015018572');

doris> select * from customer_mor@incr('beginTime'='20240603015058442');
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
| c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
| 32 | Customer#000000032_update | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 15 |
+-----------+---------------------------+-----------+-----------------+-----------+--------------+-------------------------------------+-------------+
spark-sql> select * from hudi_table_changes('customer_mor', 'latest_state', '20240603015058442');

06 TimeTravel​

Doris поддерживает запросы к определённым версиям снимков данных Hudi, обеспечивая тем самым функцию Time Travel. Сначала можно запросить историю коммитов двух таблиц Hudi с помощью Spark:

spark-sql> call show_commits(table => 'customer_cow', limit => 10);
20240603033556094 20240603033558249 commit 448833 0 1 1 183 0 0
20240603015444737 20240603015446588 commit 450238 0 1 1 202 1 0
20240603015018572 20240603015020503 commit 436692 1 0 1 1 0 0
20240603013858098 20240603013907467 commit 44902033 100 0 25 18751 0 0

spark-sql> call show_commits(table => 'customer_mor', limit => 10);
20240603033745977 20240603033748021 deltacommit 1240 0 1 1 0 0 0
20240603015451860 20240603015453539 deltacommit 1434 0 1 1 1 1 0
20240603015058442 20240603015100120 deltacommit 436691 1 0 1 1 0 0
20240603013918515 20240603013922961 deltacommit 44904040 100 0 25 18751 0 0

Затем с помощью Doris можно выполнить запрос с условием c_custkey=32, чтобы получить снимок данных до вставки. Как показано ниже, данные с c_custkey=32 ещё не обновлены:

Примечание: новый оптимизатор пока не поддерживает синтаксис Time Travel. Сначала необходимо выполнить set enable_nereids_planner=false;, чтобы отключить новый оптимизатор. Эта проблема будет исправлена в будущих версиях.

doris> select * from customer_cow for time as of '20240603015018572' where c_custkey = 32 or c_custkey = 100;
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
| c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
| 32 | Customer#000000032 | jD2xZzi UmId,DCtNBLXKj9q0Tlp2iQ6ZcO3J | 25-430-914-2194 | 3471.53 | BUILDING | cial ideas. final, furious requests across the e | 15 |
| 100 | Customer#000000100 | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 25 |
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
-- compare with spark-sql
spark-sql> select * from customer_mor timestamp as of '20240603015018572' where c_custkey = 32 or c_custkey = 100;

doris> select * from customer_mor for time as of '20240603015058442' where c_custkey = 32 or c_custkey = 100;
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
| c_custkey | c_name | c_address | c_phone | c_acctbal | c_mktsegment | c_comment | c_nationkey |
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
| 100 | Customer#000000100 | jD2xZzi | 25-430-914-2194 | 3471.59 | BUILDING | cial ideas. final, furious requests | 25 |
| 32 | Customer#000000032 | jD2xZzi UmId,DCtNBLXKj9q0Tlp2iQ6ZcO3J | 25-430-914-2194 | 3471.53 | BUILDING | cial ideas. final, furious requests across the e | 15 |
+-----------+--------------------+---------------------------------------+-----------------+-----------+--------------+--------------------------------------------------+-------------+
spark-sql> select * from customer_mor timestamp as of '20240603015058442' where c_custkey = 32 or c_custkey = 100;

Оптимизация запросов​

Данные в Apache Hudi можно условно разделить на две категории: базовые и инкрементальные. Базовые данные обычно представляют собой объединённые файлы Parquet, а инкрементальные данные содержат изменения, созданные операциями INSERT, UPDATE или DELETE. Базовые данные можно читать напрямую, а инкрементальные необходимо читать через Merge on Read.

При запросах к таблицам Hudi COW или выполнении запросов Read Optimized к таблицам MOR используются базовые данные. Их можно читать напрямую с помощью встроенного в Doris Parquet Reader, что обеспечивает быстрое выполнение запросов. Для чтения инкрементальных данных Doris необходимо обращаться к Java SDK Hudi через вызовы JNI. Чтобы обеспечить оптимальную производительность запросов, Apache Doris разделяет данные запроса на базовую и инкрементальную части и читает их описанными выше способами.

Чтобы проверить этот подход к оптимизации, воспользуемся оператором EXPLAIN и посмотрим, сколько базовых и инкрементальных данных содержится в приведённом ниже примере запроса. Для таблицы COW все 101 фрагмент данных относятся к базовым данным (hudiNativeReadSplits=101/101), поэтому всю таблицу COW можно прочитать напрямую с помощью Parquet Reader в Doris, обеспечив наилучшую производительность запроса. Для таблицы ROW большинство фрагментов содержат базовые данные (hudiNativeReadSplits=100/101), а один фрагмент содержит инкрементальные данные, что также обеспечивает хорошую производительность запроса.

-- COW table is read natively
doris> explain select * from customer_cow where c_custkey = 32;
| 0:VHUDI_SCAN_NODE(68) |
| table: customer_cow |
| predicates: (c_custkey[#5] = 32) |
| inputSplitNum=101, totalFileSize=45338886, scanRanges=101 |
| partition=26/26 |
| cardinality=1, numNodes=1 |
| pushdown agg=NONE |
| hudiNativeReadSplits=101/101 |

-- MOR table: because only the base file contains `c_custkey = 32` that is updated, 100 splits are read natively, while the split with log file is read by JNI.
doris> explain select * from customer_mor where c_custkey = 32;
| 0:VHUDI_SCAN_NODE(68) |
| table: customer_mor |
| predicates: (c_custkey[#5] = 32) |
| inputSplitNum=101, totalFileSize=45340731, scanRanges=101 |
| partition=26/26 |
| cardinality=1, numNodes=1 |
| pushdown agg=NONE |
| hudiNativeReadSplits=100/101 |

Чтобы подробнее проследить изменения базовых и инкрементальных данных Hudi, можно выполнить несколько операций удаления с помощью Spark:

-- Use delete statement to see more differences
spark-sql> delete from customer_cow where c_custkey = 64;
doris> explain select * from customer_cow where c_custkey = 64;

spark-sql> delete from customer_mor where c_custkey = 64;
doris> explain select * from customer_mor where c_custkey = 64;

Кроме того, можно дополнительно сократить объём читаемых данных, используя условия по разделам для их отсечения, и тем самым ускорить запрос. В примере ниже отсечение разделов выполняется по условию c_nationkey=15, благодаря чему запрос обращается к данным только одного раздела (partition=1/26).

-- customer_xxx is partitioned by c_nationkey, we can use the partition column to prune data
doris> explain select * from customer_mor where c_custkey = 64 and c_nationkey = 15;
| 0:VHUDI_SCAN_NODE(68) |
| table: customer_mor |
| predicates: (c_custkey[#5] = 64), (c_nationkey[#12] = 15) |
| inputSplitNum=4, totalFileSize=1798186, scanRanges=4 |
| partition=1/26 |
| cardinality=1, numNodes=1 |
| pushdown agg=NONE |
| hudiNativeReadSplits=3/4 |