湖仓一体技术调研
湖仓一体技术调研
前期任务及分工如下:
前期三个人分别调研一个数据湖仓系统,完成云平台账号注册以及基本的环境配置。
Apache Hudi(杨桂淼):https://hudi.apache.org/
Apache lceberg(王子曰):https://iceberg.apache.org/
Delta Lake(王晓妍):https://delta.io/
基本概念调研
大数据的4V特征:海量(Volume)、多样(Variety)、高速(Velocity)、价值(Value)
数据管理系统的演进过程:关系型数据库 -> 数据仓库 -> 数据湖 -> 数据湖仓
关系型数据库
:RDBMS 以关系模型为基础,将数据组织在预定义关系的二维表(即关系)中,表由列(属性)和行(记录)构成。每行数据通常拥有一个唯一标识符(主键),用于区分不同的记录。关系模型的一个重要特性是逻辑数据结构(如表、视图、索引)与物理存储结构的分离,这使得数据库管理员可以在不影响逻辑数据访问的前提下管理物理存储。结构化查询语言(SQL)是RDBMS进行数据查询和操作的标准语言。
数据仓库
:文献普遍将数据仓库定义为用于支持决策制定的、集成的、面向主题的、时变的数据集合,其核心是结构化数据、写时模式(Schema-on-Write)、ETL过程和OLAP能力,旨在为商业智能提供单一事实来源。
数据湖
:数据湖被描述为能够以原始格式存储海量、多样化数据(包括结构化、半结构化和非结构化数据)的中央存储库,采用读时模式(Schema-on-Read),支持ELT过程,并为高级分析和机器学习提供数据基础 。
数据湖仓
:湖仓一体被视为数据仓库和数据湖的融合,旨在结合两者的优点:数据湖的低成本、灵活性和开放格式,以及数据仓库的数据管理、事务能力和查询性能。其核心是通过在数据湖的开放文件格式(如Parquet)之上引入表格式(Table Format)层(如Hudi, Iceberg, Delta Lake),实现ACID事务、模式管理、数据版本控制等功能。
关系型数据库
核心功能: 主要用于在线交易处理 (OLTP),比如你购物下的订单、银行的转账记录等。它的核心是保证数据的一致性、准确性。
关系型数据库的特点是结构化数据: 数据以二维表格(行和列)的形式存储,结构非常规整。预定义模式 (Schema-on-Write): 在写入数据之前,必须先定义好表的结构(比如字段名、数据类型)。
痛点是:无法处理非结构化数据: 无法存储和处理像视频、音频、图片、社交媒体帖子这类非结构化数据。分析能力弱: 为交易而生,进行大规模、复杂的查询和分析时,会严重影响正常业务的性能。
数据仓库
核心功能: 为了解决关系型数据库分析能力弱的问题而诞生,专注于在线分析处理 (OLAP) 和**商业智能 (BI)**。
数据仓库的特点是:结构化和已处理: 只存储经过清洗、转换后的结构化数据,可以直接用于报表和分析。面向主题: 数据从多个业务数据库中抽取(ETL过程:抽取、转换、加载),并按照业务主题(如销售、库存)进行组织。历史数据: 存储了大量的历史数据,用于趋势分析和预测。
痛点是:数据类型单一: 仍然无法很好地应对非结构化和半结构化数据。数据在进入数仓前,不符合要求的部分就被丢弃了,导致无法进行更原始、更全面的数据探索。灵活性差、成本高: ETL过程复杂且耗时。由于采用“预定义模式”,任何需求的变更都可能需要重新设计数据模型和ETL流程,非常僵化。存储和计算的成本也很高。
数据湖
核心功能: 为了解决数据仓库无法处理海量、多样化数据的痛点而出现。它的理念是“先存下所有东西,以后再想怎么用”。
数据湖的特点是存储一切: 可以存储任何类型的数据,包括结构化、半结构化和非结构化(如日志文件、JSON、视频、音频等)的原始数据。后定义模式 (Schema-on-Read): 在读取和分析数据时,才根据需求去定义数据的结构。原始、未经处理: 数据以其最原始的形式被加载进来,不做任何转换。低成本存储: 通常建立在HDFS等分布式文件系统上,存储成本非常低廉。
痛点是:缺乏事务支持: 不支持ACID事务,这使得在数据湖上进行更新、删除操作变得非常复杂且不可靠,难以保证数据的一致性。数据沼泽 (Data Swamp): 由于缺乏对数据质量的管控、元数据管理和治理,数据湖很容易变成一个无人能懂、无法使用的“数据沼泽”。
湖仓一体
数据湖仓(也常被称为“湖仓一体”)的出现,正是为了解决一个核心矛盾:我们既想要数据湖的灵活性和低成本,又想要数据仓库的强大分析性能和数据治理能力。
数据湖仓是一种新型的、开放的数据架构,将数据湖的低成本、灵活性与数据仓库的数据管理和分析功能相结合,旨在在同一个系统内实现对所有数据的BI和AI应用。
文献资料整理
论文:
- Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics:https://www.cidrdb.org/cidr2021/papers/cidr2021_paper17.pdfThe
- Data Lakehouse: Data Warehousing and More:https://arxiv.org/abs/2310.08697
- Analyzing and Comparing Lakehouse Storage Systems:https://www.cidrdb.org/cidr2023/papers/p92-jain.pdf
- Assessing the Lakehouse: Analysis, Requirements and Definition:https://www.ipvs.uni-stuttgart.de/departments/as/publications/schneijn/Assessing_the_Lakehouse_ICEIS2023.pdf
- Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores:https://www.vldb.org/pvldb/vol13/p3411-armbrust.pdf
- Apache Hudi - The Data Lake Platform:https://hudi.apache.org/blog/2021/07/21/streaming-data-lake-platform/
开源文档与技术白皮书
- Delta Lake:https://docs.dremio.com/25.x/sonar/query-manage/data-formats/delta-lake/
- Hudi:https://hudi.apache.org/docs/quick-start-guide
- Data Warehouse vs. Data Lake vs. Data Lakehouse: An Overview of Three Cloud Data Storage Patterns:https://go2.striim.com/hubfs/striim_tech_brief_data_storage_overview.pdf
现有系统分析
湖仓一体架构与功能
目标与架构: 湖仓一体是一种全新的数据管理系统,它直接在低成本的数据湖存储之上,提供传统数据仓库的管理和性能特性,如ACID事务、数据版本控制、索引和查询优化 。其设计目标是融合数据湖的低成本、开放性和数据仓库的强大管理与优化能力 。
湖仓一体的核心特征
单一系统:消除了数据湖和数据仓库之间的数据移动和冗余,所有工作负载(BI、数据科学、ML)都可以在同一份数据副本上进行 。
事务性元数据管理:湖仓一体的关键实现是在对象存储之上构建一个事务性的元数据层 。这一层定义了哪些数据文件构成一个表的特定版本,从而实现了ACID事务、数据版本回溯(Time Travel)等高级管理功能 。
典型的分层架构
数据接入层 (Ingestion Layer) :负责从各种数据源(如关系型数据库、NoSQL数据库、业务应用、流式数据源、文件等)采集数据并将其导入湖仓系统。此层支持批量导入和实时流式导入,常见工具包括Apache Kafka, Apache Flume, Spark Streaming以及各类数据库连接器和CDC工具。
存储层 (Storage Layer) :通常采用可扩展、高持久且成本效益高的云对象存储服务(如AWS S3, Azure Blob Storage, Google Cloud Storage)或分布式文件系统(如HDFS)。数据在此层以开放文件格式(如Apache Parquet, Apache ORC, Apache Avro)存储,能够容纳结构化、半结构化和非结构化数据。
表格式层 (Table Format / Metadata Layer) :这是湖仓一体架构的核心与灵魂,是其区别于传统数据湖的关键。表格式(如Apache Hudi, Apache Iceberg, Delta Lake)在数据湖的原始文件之上提供了一个抽象层,负责管理元数据,包括表结构(Schema)、事务日志、数据版本、分区信息以及文件统计信息等。它使得上层计算引擎可以将存储在对象存储中的文件集合视为具有事务特性和结构化定义的表。
查询与处理引擎层 (Query/Processing Engines) :各种计算引擎(如Apache Spark, Presto, Trino, Apache Flink)和云数据服务(如AWS Athena, Google BigQuery, Snowflake)通过与表格式层交互来读取、处理和分析数据。这些引擎负责执行SQL查询、数据转换、机器学习任务等。
治理与目录层 (Governance & Catalog Layer) :提供数据发现、访问控制、数据血缘追踪、数据质量监控和审计等功能。通常与元数据存储服务(如Hive Metastore, AWS Glue Data Catalog, Project Nessie)集成,以实现统一的元数据视图和治理策略。
核心功能
ACID事务:确保数据操作的原子性、一致性、隔离性和持久性,为并发读写提供数据一致性和可靠性保障,这是从数据仓库借鉴并应用于数据湖的关键特性。
模式管理 (Schema Management) :包括模式强制(Schema Enforcement)和模式演进(Schema Evolution)。模式强制确保写入数据符合预定义结构,防止脏数据污染;模式演进则允许在不重写整个数据集的情况下修改表结构(如增删列、修改类型)以适应业务变化。
数据版本控制 (Data Versioning / Time Travel) :记录数据的历史版本,允许用户查询表的过去某个时间点的状态、回滚到历史版本或重现实验结果。
索引与数据裁剪/跳过 (Indexing & Data Skipping/Pruning) :利用文件/分区元数据统计信息(如最小/最大值)、Bloom过滤器、Z-Order等技术,在查询时跳过不相关的数据文件或分区,从而优化查询性能。
支持多样化数据与工作负载 :能够处理结构化、半结构化和非结构化数据,并支持包括BI、SQL分析、数据科学、机器学习和流处理在内的多种工作负载。
计算存储分离 (Decoupled Compute and Storage) :通常构建在云原生架构之上,允许计算资源和存储资源独立扩展,从而提供更好的灵活性和成本优化。
现有系统基本原理
Apache Hudi
Apache Hudi旨在为数据湖带来流式处理能力,特别是高效的记录级插入更新(Upsert)和删除操作。
Apache Hudi为数据加载和更新提供了灵活的策略,其核心机制围绕记录键(record key)、索引和事务时间线展开。
可以将 Hudi 的整体架构理解为由三大核心支柱支撑:
- **优化的表格式 (Optimized Table Format)**:定义了数据在数据湖中如何组织和布局。这是所有功能的基础,确保了数据的可管理性和事务性。
- **丰富的表服务 (Table Services)**:在后台运行的、用于管理和优化数据表的各种自动化服务。它们是保证数据湖高性能和低成本的关键,如同数据仓库中的 DBA 工具集。
- **统一的分析视图 (Unified Analytics Views)**:为上层查询引擎(如 Spark, Presto, Flink, Hive 等)提供统一、高性能的数据读取视图。
它通过时间轴保证了事务性,通过文件组和文件切片实现了记录级别的更新,通过两种表类型 (CoW/MoR) 在读写性能间提供了灵活选择,通过高效索引解决了数据定位难题,并利用一系列自动化表服务保证了数据湖的长期健康和高性能。
写时复制 (CoW) 与读时合并 (MoR) 策略下的加载
Hudi支持两种主要的表类型(也是数据更新策略),它们在数据加载时的行为有所不同:
- 写时复制 (Copy-On-Write, CoW): 在CoW模式下,当数据被加载或更新时,如果记录导致现有数据文件的更改,Hudi会识别包含这些记录的文件,并将更新后的数据连同未更改的数据一起重写到一个新的版本化文件中。旧版本的文件保持不变,直到被清理策略移除。这种策略的特点是写入时会产生较高的写入放大(因为即使只更新一行,也可能重写整个文件),但在读取时不需要额外的合并操作,因此读取放大较低,查询性能通常较好 。对于批量加载主要是新数据追加的场景,CoW表现为直接写入新的Parquet文件。更新操作则完全以列式Parquet文件的形式写入,创建新的数据对象或文件版本 。
- 读时合并 (Merge-On-Read, MoR): MoR模式旨在优化写入延迟。加载或更新数据时,Hudi不会立即重写包含旧数据的基础文件(通常是Parquet格式)。相反,它将记录级别的变更(插入、更新、删除)写入增量的、基于行的日志文件(通常是Avro格式)。实际的数据合并操作被推迟到查询时进行,或者通过后台的异步压缩(compaction)过程定期执行,将日志文件中的变更合并到新的基础文件中 。MoR模式的写入放大较低(写入速度快),但读取时需要合并基础文件和相关的日志文件,因此读取放大较高,查询延迟可能比CoW高,除非数据已被压缩。
记录键 (Record Key) 与索引在加载中的作用
Apache Hudi的核心设计之一是围绕记录键进行操作,这对于实现高效的upsert(更新或插入)至关重要。
- 记录键 (Record Key): 每个Hudi表中的记录都由一个唯一的记录键标识。这个键由用户指定,用于在数据加载和更新过程中唯一地识别一条记录。
- 索引 (Indexing): 为了在加载(尤其是upsert操作)时快速定位记录,Hudi采用了一种可插拔的索引机制。索引负责将记录键映射到其所在的物理文件组ID 。当一批新数据写入时,Hudi利用索引来判断每条记录是全新的插入(insert)还是对现有记录的更新(update)。常见的索引类型包括基于Bloom过滤器的索引(默认)、简单索引、HBase索引等。Bloom过滤器等索引机制可以显著加速这个打标过程,比传统的通过Spark Join进行判断要快得多 。这个索引查找和记录分类步骤是Hudi实现高效增量更新的基础 。
事务提交过程
Hudi的事务提交过程确保了数据写入的原子性和一致性。其核心是维护一个事务时间线(timeline),记录了在表上执行的所有操作(如提交、清理、压缩、回滚等)。
数据写入(包括加载)通常遵循以下步骤 :
- 去重 (Deduping): (可选)对输入批次内可能存在的重复记录键进行预处理。
- 索引查找 (Index Lookup): 使用配置的索引机制,为输入记录打上标签(插入或更新),并确定其目标文件组。
- 文件大小调整 (File Sizing): Hudi会运行启发式算法,尝试将数据打包到大小合适的文件中,以优化存储和查询性能。
- 分区 (Partitioning): 根据分区键将数据分配到相应的分区路径。
- 写入I/O (Write I/O):
- 对于CoW表,更新操作会导致生成新的版本化基础文件。插入操作会写入新的基础文件。
- 对于MoR表,插入操作会写入新的基础文件(如果文件组不存在)或追加到现有日志文件;更新操作会追加到对应文件组的日志文件中。
- 更新索引 (Update Index): 写入完成后,更新索引以反映新数据的位置。
- 提交 (Commit): 原子地将操作元数据(如写入的文件列表、记录数统计等)记录到Hudi时间线上,形成一个新的“instant”(即时点)。Hudi在写操作之间使用多版本并发控制(MVCC)来确保一致性,并在并发写操作之间使用乐观并发控制(OCC)或非阻塞并发控制(NBCC)。
Apache Iceberg
Apache Iceberg专注于提供一个开放、高性能的表格式,特别强调表结构演进的灵活性、分区管理的便捷性以及跨引擎的互操作性。
数据文件、清单文件 (Manifest File) 与清单列表 (Manifest List) 的协同
Iceberg的元数据组织呈现清晰的层次结构 :
数据文件 (Data Files): 这是实际存储表数据的物理文件,可以是Parquet、Avro或ORC格式 。在数据加载时,写入程序首先创建这些数据文件。这些文件一旦写入即不可变。
清单文件 (Manifest Files): 每个清单文件(Avro格式)跟踪一组数据文件 。它包含了这些数据文件的路径、文件格式、分区信息(每个数据文件属于哪个分区元组)、列级统计信息(如每列的最大/最小值、空值计数)、以及可能的删除文件信息(用于Merge-on-Read场景)。清单文件本身也是不可变的,并且可以被多个快照复用,以避免重写未发生变化的元数据。
清单列表 (Manifest List): 每个表的快照(snapshot)都由一个清单列表文件(Avro格式)定义 。清单列表文件包含了构成该快照的所有清单文件的路径、每个清单文件所覆盖的分区范围统计信息(用于查询时裁剪掉不相关的清单文件)以及每个清单文件包含的数据文件数量等元数据。每次提交尝试都会生成一个新的清单列表,因为它总是代表一个新的快照。
数据加载时,首先写入新的数据文件。随后,会创建一个或多个新的清单文件,这些清单文件指向新写入的数据文件并记录其元数据。最后,会创建一个新的清单列表文件,该文件指向这些新的清单文件(以及可能复用的旧清单文件,如果它们包含的数据仍然是当前快照的一部分)。
Iceberg加载机制的灵活性与元数据可扩展性
Apache Iceberg将数据文件的写入与元数据提交过程解耦,并采用分层元数据结构,这为其带来了显著的灵活性和元数据操作的可扩展性,尤其是在查询规划方面。数据文件可以首先被并行且独立地写入。随后,元数据的更新(创建清单文件、清单列表和表元数据文件)也可以高效进行。其层次结构(表元数据 -> 清单列表 -> 清单文件 -> 数据文件)使得在查询规划时能够进行有效的元数据裁剪,快速定位相关数据文件,而无需扫描所有元数据或执行大量的文件列表操作 。最终的原子提交操作仅聚焦于在Catalog中更新一个单一的指针,这相较于在分布式日志系统中协调跨多个日志段的变更(如果所有元数据都直接存储在这样的日志中),可能更为迅速和简单。
Delta Lake
Delta Lake最初由Databricks开发,现为Linux基金会项目,它通过在Apache Parquet文件之上构建一个事务日志层,为数据湖带来了ACID事务和可靠性。
与 Hudi 类似,Delta Lake也是一个构建在数据湖(如 S3, ADLS, GCS)之上的开源存储层,旨在为数据湖带来 ACID 事务、可靠性和高性能。
Delta Lake 的架构可以概括为两个层次:
- 数据文件层 (Data Files) :存储在云存储或 HDFS 上的 Parquet 文件。这是数据的“身体”,包含了所有的实际数据。Delta Lake 默认使用 Parquet 格式,因为它具备高效的压缩和列式存储特性,非常适合分析查询。
- 事务日志层 (Transaction Log):这是 Delta Lake 的“大脑和灵魂”,也是其架构的精髓所在。它是一个名为
_delta_log
的子目录,与数据文件存放在一起。这个日志完整、有序地记录了对 Delta 表所做的每一次变更。
事务日志 (Transaction Log) 的角色与数据写入
Delta Lake的核心是位于表根目录下 _delta_log
子目录中的事务日志 。这个日志是有序记录了对表进行的每一个事务(如数据加载、更新、删除、模式变更等)。
- 数据写入 (Data Writing): 当加载数据到Delta表时,数据本身以Parquet文件的形式写入表目录下的相应分区(如果表是分区的)或根目录(如果未分区)。
- 事务日志条目 (Transaction Log Entry): 在数据文件写入完成后,Delta Lake会为该事务创建一个新的JSON文件(例如
00000000000000000001.json
),并将其写入_delta_log
目录。这个JSON文件包含了该事务所执行的操作的详细信息,例如:add
操作:列出新添加的Parquet文件的路径、分区值、统计信息(如文件大小、行数、列的最小/最大值等)。remove
操作:在执行覆盖写或删除操作时,列出被移除的旧文件的路径和删除时间戳。metadata
操作:记录表的元数据信息,如模式、分区列、表属性等。protocol
操作:定义表所遵循的Delta Lake协议版本,以支持不同的特性。
- 原子提交 (Atomic Commit): Delta Lake通过确保每个JSON日志文件的写入是原子的来实现事务的原子性。这通常依赖于底层文件系统提供的原子操作,例如HDFS的
append
(虽然Delta主要使用创建新文件的方式)或云对象存储上的“如果不存在则放置”(put-if-absent)语义 。只有一个写入者能够成功创建特定版本号的JSON文件,从而确保了事务的串行化。
检查点 (Checkpoints) 与版本控制
随着事务的不断发生,_delta_log
目录中的JSON文件数量会持续增长。为了提高查询性能(特别是获取表当前状态的速度)并避免列出和读取大量小的JSON文件,Delta Lake会定期创建检查点(checkpoint)文件 。
- 检查点文件 (Checkpoint Files): 检查点文件(通常是Parquet格式,也可能是多部分Parquet或JSON格式的组合)整合了截至某个特定版本号(例如,每10个JSON提交创建一个检查点)的所有事务日志信息 。它包含了表的当前状态的快照,如当前有效的数据文件列表、表的模式、分区信息以及文件级别的统计数据。
- 版本控制 (Versioning):
_delta_log
中的每一个JSON文件(或检查点文件)都代表了表的一个新版本。这使得Delta Lake能够支持时间旅行(time travel)查询,即查询表在过去某个特定版本或时间点的状态。
写时复制 (CoW) 与读时合并 (MoR) - Deletion Vectors
Delta Lake传统上主要采用写时复制(CoW)策略。当数据被更新或删除时,包含受影响记录的Parquet文件会被重写。然而,为了优化更新和删除操作的性能,特别是减少写入放大,Delta Lake引入了“删除向量”(Deletion Vectors)特性 。
删除向量 (Deletion Vectors): 当启用删除向量后,
DELETE
、UPDATE
和MERGE
操作可以将某些行标记为已删除或已更改,而无需立即重写包含这些行的整个Parquet文件。这些标记信息存储在与数据文件分离的删除向量文件中。读取时,查询引擎会结合数据文件和删除向量来获取最新的数据视图。这种方式类似于读时合并(MoR)的行为,因为它推迟了物理数据的重写 。物理应用: 被删除向量标记的更改会在后续的
OPTIMIZE
命令执行、自动压缩(auto-compaction)触发,或REORG TABLE... APPLY (PURGE)
命令运行时被物理应用到数据文件中,即重写数据文件以移除标记的行或应用更改 。
数据加载流程对比
Apache Hudi 加载流程:
- 记录处理与索引: 输入记录被处理。如果是upsert操作或带有键的bulk_insert,Hudi会执行索引查找,将每条记录标记为插入或更新,并确定其目标文件组。
- 数据分区与文件大小调整: 根据分区键对数据进行分区,并应用文件大小调整的启发式算法,尝试将数据打包到大小合适的文件中。
bulk_insert
操作可能还会根据配置的排序模式(如GLOBAL_SORT
或PARTITION_SORT
)对数据进行排序和重分布。 - 数据写入: 数据被写入到基础文件(Parquet格式)和/或日志文件(Avro格式,针对MoR表)。
- 索引更新: 更新索引以反映新数据的位置。
- 原子提交: 将事务元数据(如写入的文件、统计信息等)原子性地提交到Hudi的时间线上。这通常涉及到乐观并发控制(OCC)下的锁机制或MVCC协调。
Apache Iceberg 加载流程:
- 数据文件写入: 数据首先被写入到数据文件(如Parquet、ORC或Avro格式)。
- 清单文件创建: 创建一个新的或多个新的清单文件(Manifest File),这些文件列出了新写入的数据文件及其统计信息(如列边界值、空值计数等)。
- 清单列表创建: 创建一个新的清单列表文件(Manifest List File),该文件指向相关的清单文件(包括新创建的和可能复用的旧清单文件)。
- 表元数据文件创建: 创建一个新的表元数据文件(JSON格式),该文件指向新的清单列表,并包含表的当前模式、快照ID、分区规范等信息。
- 原子提交: 尝试通过原子操作(通常是CAS)更新外部元数据存储(Catalog)中指向当前表元数据文件的指针。如果发生冲突(即其他写入者已提交),则当前写入操作会进行重试。
Delta Lake 加载流程:
- 数据文件写入: 数据被写入到Parquet格式的数据文件中,通常分布在相应的分区目录下。
- 事务日志条目创建: 在
_delta_log
目录中创建一个新的事务日志条目(一个JSON文件),该文件记录了此次事务中添加的数据文件列表、删除的文件列表(如有)、事务的元数据(如操作类型、时间戳)以及可能的协议或元数据更新。 - 原子提交: 尝试以原子方式将新创建的JSON日志文件写入
_delta_log
目录。这通常依赖于底层文件系统的“如果不存在则创建”的原子语义。
对比分析
特性 | Apache Hudi | Delta Lake | Apache Iceberg |
---|---|---|---|
核心优势 | 高效记录级更新/删除 (索引), 近实时流 | 强事务 (日志), Spark 深度集成, 优化管理 | 高性能元数据, 引擎无关, 高级分区/模式演化 |
事务实现 | 时间线 (Timeline) | 事务日志 (Transaction Log) | 快照隔离 + 原子元数据更新 |
关键机制 | 索引 (Index) | 事务日志 (JSON/Parquet) | 分层元数据 (Manifest Lists/Manifests) |
增量处理 | 非常强大 (Incremental Query) | 支持 (Change Data Feed) | 支持 (Incremental Scans) |
表类型/视图 | Copy-on-Write / Merge-on-Read | 单一表格式 (类似 CoW,但优化机制丰富) | 单一表格式 (物理布局优化灵活) |
与引擎集成 | 支持 Spark, Flink, Presto, Hive 等 | 深度集成 Spark (原生 API) | 广泛支持 (Flink, Spark, Trino, Hive 等) |
分区演进 | 有限支持 | 有限支持 | 原生支持 (Partition Evolution) |
隐藏分区 | 无 | 无 | 支持 (Hidden Partitioning) |
时间旅行 | 支持 | 支持 | 支持 |
模式演化 | 支持 | 支持 | 支持更复杂的演化 (Sort Order Evolution) |
文件跳过优化 | 支持 | 支持 (Data Skipping + Z-Ordering) | 支持 (基于 Manifest 详细统计) |
典型适用场景 | CDC, 频繁更新, 近实时摄入 | Spark 生态湖仓, 强事务 ETL, Databricks 用户 | 超大规模, 多引擎访问, 灵活分区, 高性能元数据需求 |
主要推动者 | Uber | Databricks | Netflix, Tabular, Dremio 等 |
湖仓系统对 HDFS 中非结构化数据的操作方式
存储方式:
- 对于半结构化数据(如JSON、XML、Avro),Lakehouse格式(Hudi, Iceberg, Delta Lake)通常将其存储在支持复杂嵌套结构的列式文件格式中,最常见的是Apache Parquet或ORC。这些格式能够高效地压缩和查询嵌套数据,使其可以直接被SQL引擎或其他分析工具处理。
- 对于非结构化数据(如图像文件
.jpg
, 视频文件.mp4
, 音频文件.mp3
, 纯文本文档.txt
, 二进制大对象BLOBs),这些文件本身会以其原始格式直接存储在HDFS中。Lakehouse表此时扮演的角色是管理这些文件的元数据。例如,表中的一行可能代表一个图像文件,包含的列有文件路径(指向HDFS中的实际文件)、文件大小、创建日期、相关的标签或描述、甚至是提取出的特征(如图像的宽度、高度、主要颜色等)。
操作方式:
- 元数据管理: Lakehouse格式的核心功能(ACID事务、版本控制、时间旅行、模式演进)主要应用于它们所管理的结构化或半结构化数据,以及非结构化文件的元数据。例如,当向表中添加一个新的图像文件引用时,这个添加操作是事务性的,并且可以被版本控制。如果更新了某个图像文件的标签,这个元数据的更新也是一个事务。
- 内容处理: 对非结构化数据内容的实际处理(例如,使用计算机视觉模型分析图像、使用自然语言处理技术分析文本、转码视频等)通常由专门的计算引擎完成,如Apache Spark结合其MLlib库、TensorFlow、PyTorch等 。这些引擎可以直接从HDFS读取由Lakehouse表元数据指向的原始文件路径。Lakehouse系统本身不直接提供对这些二进制文件内容的内部操作(例如,事务性地修改视频中的某一帧)。
- 与高级分析集成: 湖仓架构通过提供统一的数据访问接口,使得机器学习(ML)和数据科学工作负载能够更便捷地利用存储在数据湖中的各类数据。例如,ML模型训练可能需要读取大量图像文件(路径存储在Lakehouse表中)和对应的标注信息(也存储在Lakehouse表中)。最近的一个趋势是将从非结构化数据中提取的向量嵌入 (vector embeddings) 存储在Parquet文件中,并由Lakehouse格式管理,这使得非结构化内容可以通过其结构化的向量表示被索引和查询 。
Hudi加载数据慢的原因
基准测试研究(如LHBench )指出,Hudi在批量加载大型数据集(如3TB TPC-DS)时,其加载时间远超Delta Lake和Iceberg。报告将此归因于Hudi为有键更新所做的优化,导致在批量加载时执行了“昂贵的预处理步骤”,包括“键唯一性检查和键重分布” 。这些预处理步骤具体可以分解为:
索引构建与查找 (Index construction and lookup):
- Hudi的核心是记录键(record key)。为了有效地执行upsert操作(即如果记录键已存在则更新,否则插入),Hudi需要一个索引来快速将传入的记录键映射到其在存储中的物理位置(即文件组ID)。
- 在批量加载数据时,即便是使用
bulk_insert
操作(如果提供了记录键并期望去重或后续能进行键更新),或者默认的upsert
操作,Hudi都需要与这个索引机制交互。对于一个全新的表进行首次加载,这可能意味着需要为所有输入数据构建初始索引结构。如果使用upsert
,则每条输入记录都需要通过索引查找来判断是插入还是更新。 - 虽然Hudi提供了多种索引实现(如Bloom Filter索引、Simple Index、HBase Index等),并且其内置索引(如基于文件范围和Bloom Filter的索引)声称比使用Spark Join进行相同判断快数倍 ,但对于TB级别的海量数据,这个查找和标记过程本身仍然会消耗大量的计算资源和时间。
主键唯一性检查与预结合 (Primary key uniqueness checks and Precombine):
- 为了保证数据的一致性,Hudi在upsert操作中会确保主键的唯一性。这包括检查输入批次内是否有重复键,以及与表中现有数据比较。
- Hudi还支持“预结合”(Precombine)机制。如果在写入配置中指定了预结合字段(例如,通过
hoodie.datasource.write.precombine.field
配置一个时间戳或版本号列 ),那么在实际写入之前,Hudi会根据此字段对输入数据中具有相同记录键的多条记录进行合并,只保留“最新”或“最优先”的一条。这个过程虽然能确保数据质量和处理晚到数据,但在批量加载大量原始数据时,无疑增加了额外的计算开销。
数据重分布与文件大小调整 (Data redistribution and file sizing heuristics):
Hudi非常注重对底层存储文件大小的管理,以避免产生大量小文件,从而优化后续的查询性能 。在数据加载过程中,Hudi会运行启发式算法,尝试将数据打包到大小适宜(例如,接近配置的目标文件大小)的基础文件中。
对于
bulk_insert
操作,Hudi提供了多种排序模式(hoodie.bulkinsert.sort.mode
),如NONE
(不排序,最快但文件大小控制较差)、GLOBAL_SORT
(全局排序,文件大小控制最好但成本最高)、PARTITION_SORT
(分区内排序,平衡性能和文件大小)等 。选择GLOBAL_SORT
或PARTITION_SORT
模式意味着在写入数据之前需要进行大规模的Shuffle和Sort操作,这在批量加载TB级数据时是非常耗时的。即使是默认的upsert
操作,其内部的文件组分配和数据写入逻辑也会考虑文件大小的维护。这种主动的文件组织和大小控制策略,与某些系统可能先快速写入数据(可能产生小文件),然后依赖后续独立的、显式的压缩(compaction)步骤来整理文件的做法不同,Hudi在写入时就承担了部分数据布局的成本。
这些预处理步骤,虽然对于Hudi核心的增量更新和CDC场景至关重要,但在纯粹的、一次性的大批量数据追加(append-only)场景下,它们就构成了显著的性能开销。