一、数据湖与数仓对比
1.数据仓库
- 数据仓库(Data Warehouse 简称数仓、DW),是一个用于存储、分析、报告的数据系统。
- 数据仓库的目的是构建面向分析的集成化数据环境,分析结果为企业提供决策支持。
- 特点:
- 本身不生产数据,也不最终消费数据。(用分层管理与存储数据)
- 每个企业根据自己的业务需求可以分层不同的层次。但是最基础的分层思想,理论上分为三个层:操作型数据层(ods)、数据仓库层(dw)和数据应用层(da)
2、数据湖
- 数据湖(Data Lake)和数据库、数据仓库一样,都是数据存储的设计模式,现在企业的数据仓库都会通过分层的方式将数据存储在文件夹、文件中。
- 数据湖是一个集中式数据存储库,用来存储大量的原始数据,使用平面架构来存储数据。
- 定义:一个以原始格式(通常是对象块或文件)存储数据的系统或存储库,通常是所有企业数据的单一存储。
- 数据湖可以包括来自关系数据库的结构化数据(行和列)、半结构化数据(csv、日志、xml、json)、非结构化数据(电子邮件、文档、pdf)和二进制数据(图像、音频、视频)。
- 数据湖越来越多的用于描述任何的大型数据池,数据都是以原始数据方式存储,知道需要查询应用数据的时候才会开始分析数据需求和应用架构。
- 数据湖中数据,用于报告、可视化、高级分析和机器学习等任务。
二、Hudi介绍
2.1 Hudi的定义:
Apache Hudi是一种开源的数据湖表格式框架。Hudi基于对象存储或者Hdfs组织文件布局,保证ACID,支持行级别的高效更新和删除,从而降低数据ETL开发门槛。同时该框架支持自动管理及合并小文件,保持指定的文件大小,从而在处理数据插入和更新时,不会创建过多的小文件,引发查询端性能降低,避免手动监控和重写小文件的运维负担。结合Flink、Presto、Spark等计算引擎进行数据入湖和计算分析,常用来支持DB入湖加速、增量数据实时消费和数仓回填等需求。
2.2 Hudi特性
- 支持ACID:支持ACID语义,提供事务的线性隔离级别。
- 支持UPSERT语义:UPSERT语义即就是INSERT和UPDATE两种语义的合并。在UPSERT语义时,如果记录不存在则插入;如果记录存在则更新。通过INSERT INTO语法可以大幅简化开发代码的复杂度,提升效率。
- 支持Data Version:通过时间旅行(Time Travel)特性,提供任意时间点的数据版本历史,便于数据运维,提升数据质量。
- 支持Schema Evolution:支持动态增加列,类型变更等Schema操作。
三、概要
3.1 TimeLine
Hudi 的核心是维护timeline
在不同时间对表执行的所有操作,instants
这有助于提供表的即时视图,同时还有效地支持按到达顺序检索数据。Hudi Instant 由以下组件组成
- Instant action:在表上执行的操作类型
- Instant time:即时时间通常是一个时间戳(例如:20190117010349),它按照动作 (action)开始时间的顺序单调递增。
state
: 时刻(事件)的当前状态
Hudi 保证在时间轴上执行的动作是原子的并且基于即时时间的时间轴是一致的。
action的主要操作包括:
- commits:表示将一批记录原子写入表中。
- cleans:清除表中不再需要的旧版本文件的后台活动。
- delta_commit:增量提交是指将一批记录原子写入MergeOnRead 类型的表,其中部分/全部数据可以写入增量日志。
- compaction:在 Hudi 中协调差异数据结构的后台活动,例如:将更新从基于行的日志文件移动到列格式。在内部,压缩表现为时间线上的特殊提交
- rollback:表示提交/增量提交不成功并回滚,删除在写入期间产生的任何数据
- savepoint:将某些文件标记为“已保存”,以便清理程序时不会被清除,在发生故障时,有助于把数据恢复还原到时间线(timeLine)上的某个点。
任务:任何时刻都会会处于以下state:
REQUESTED
- 表示一个动作已被安排,但尚未启动INFLIGHT
- 表示当前正在执行该操作COMPLETED
- 表示时间线上的动作完成
上面的示例显示了 Hudi 表上 10:00 到 10:20 之间发生的 upserts,大约每 5 分钟一次,在 Hudi 时间轴上留下提交元数据,以及其他后台清理/压缩。要进行的一项关键观察是,提交时间表示数据到达的时间(上午 10:20),而实际的数据组织反映了实际时间或event time
数据的预期用途(从 07:00 开始的每小时存储桶)。在推理延迟和数据完整性之间的权衡时,这是两个关键概念。
当有迟到的数据(预计 9:00 的数据在 10:20 迟到 > 1 小时)时,我们可以看到 upsert 将新数据生成到更旧的时间桶/文件夹中。在时间线的帮助下,尝试获取自 10:00 后成功提交的所有新数据的增量查询能够非常有效地仅使用更改的文件,而无需扫描所有时间段 > 07:00。
3.2 Table & Query Types
Hudi表类型定义了如何在DFS上对数据进行索引和布局,以及如何在此类组织之上实现上述操作和时间线活动(即如何写入数据)。反过来,查询类型(query types)定义底层数据如何暴露给查询(即如何读取数据)。
表类型(Table Type) | 支持的查询类型(Supported Query types) |
---|---|
Copy On Write 写时复制 | 快照查询+增量查询 |
Merge On Read 读时合并 | 快照查询+增量查询+读优化查询 |
Hudi 支持以下表存储类型:
- Copy on Write:使用列式存储来存储数据(例如:parquet),通过在写入期间执行同步合并来简单地更新和重写文件,(通过在数据写入的时候,复制一份原来的拷贝,再其基础上添加新数据)。
例如:有1g的数据,但只需要新增1kb,它会把1g的数据全部拷贝出来进行重写。
优点:读取时,制度去对应分区的一个数据文件即可,较为高效。
缺点:数据写入时,需要先复制对应先前的分区数据文件再再其基础上生成新的数据文件,这个过程比较耗时。
- Merge on Read:使用列式存储(parquet)+行式文件(arvo)组合存储数据。更新记录到增量文件中,然后进行同步或异步压缩来生成新版本的列式文件。 类似NoSQL中的LSM-Tree更新。
优点:由于写入数据先写delta log,且delta log较小,所以写入成本较低;
缺点:需要定期合并整理compact,负责碎片文件较多。读取性能较差,因为需要将delta log和老数据合并
下表总结了这两种表类型之间的权衡:
权衡 | CopyOnWrite | Merge on Read |
---|---|---|
数据延迟 | 更高 | 降低 |
查询延迟 | 降低 | 更高 |
Update(I/O) 更新成本 | 更高(重写整个Parquet文件) | 较低(附加到增量日志) |
Parquet File Size | 更小(高更新(I/0)成本) | 更大(更新成本低) |
Write Amplification(WA写入放大) | 更高 | 较低(取决于压缩策略) |
适用场景 | 写少读多 | 写多读少 |
Hudi 支持以下查询类型:
- Snapshot Queries 快照查询:查询某个提交或压缩操作的数据集的最新快照。在读取数据集时先进行动态合并最新的基本文件(parquet)和增量文件(Avro)的情况下来展示近乎实时的数据(几分钟)。对于copy on write的表,它提供了对现有 parquet 表的直接替换,同时提供了 upsert/delete 和其他写入功能。
- Incremental Queries 增量查询:只查询新写入数据集的文件,需要指定一个Commit/Compaction的即时时间(位于TimeLine上的某个Instant)作为条件,来查询此条件之后的新数据。有效的提供变更流来启用增量数据管道。
- Read Optimized Queries 读取优化查询:查询查看给定提交/压缩操作的表的最新快照。仅公开最新文件切片中的列式文件(Parquet),并保证与非 hudi 列式数据集相比,具有相同的列查询性能。
下面总结了两种查询的权衡:
权衡 | Snapshot | Read Optimized |
数据延迟 | 低 | 高 |
查询延迟 | 高(合并列式基础文件+行式增量日志文件) | 低(原始列式数据) |
写:
1.Copy on Write:
Copy on Write表中的文件切片仅包含基本/列文件,并且每次提交都会生成新版本的基本文件。换句话说,每次提交操作都会被压缩,以便存储列式数据,因此Write Amplification写入放大非常高(即使只有一个字节的数据被提交修改,我们也需要重写整个列数据文件),而读取数据成本则没有增加,所以这种表适合于做分析工作,读取密集型的操作。
下图说明了copy on write的表是如何工作的
随着数据被写入,对现有文件组的更新会为该文件组生成一个带有提交即时间标记的新切片,而插入分配一个新文件组并写入该文件组第一个切片。这些切片和提交即时时间在上图用同一颜色标识。针对图上右侧sql查询,首先检查时间轴上的最新提交并过滤掉之前的旧数据(根据时间查询最新数据),如上图所示粉色数据在10:10被提交,第一次查询是在10:10之前,所以出现不到粉色数据,第二次查询时间在10:10之后,可以查询到粉色数据(以被提交的数据)。
Copy on Write表从根本上改进表的管理方式
- 在原有文件上进行自动更新数据,而不是重新刷新整个表/分区
- 能够只读取修改部分的数据,而不是浪费查询无效数据
严格控制文件大小来保证查询性能(小文件会显著降低查询性能)
2.Merge on Read:
Merge on Read表是copy on write的超集,它仍然支持通过仅向用户公开最新的文件切片中的基本/列来对表进行查询优化。用户每次对表文件的upsert操作都会以增量日志的形式进行存储,增量日志会对应每个文件最新的ID来帮助用户完成快照查询。因此这种表类型,能够智能平衡读取和写放大(wa),提供近乎实时的数据。这种表最重要的是压缩器,它用来选择将对应增量日志数据压缩到表的基本文件中,来保持查询时的性能(较大的增量日志文件会影响合并时间和查询时间)
下图说明了该表的工作原理,并显示两种查询类型:快照查询和读取优化查询
- 如上图所示,现在每一分钟提交一次,这种操作是在别的表里(copy on write table)无法做到的
- 现在有一个增量日志文件,它保存对基本列文件中记录的传入更新(对表的修改),在图中,增量日志文件包含从10:05到10:10的所有数据。基本列文件仍然使用commit来进行版本控制,因此如果只看基本列文件,那么表的表的布局就像copy on write表一样。
- 定期压缩过程会协调增量日志文件和基本列文件进行合并,并生成新版本的基本列文件,就如图中10:05所发生的情况一样。
- 查询表的方式有两种,Read Optimized query和Snapshot query,取决于我们选择是要查询性能还是数据新鲜度
- 如上图所示,Read Optimized query查询不到10:05之后的数据(查询不到增量日志里的数据),而Snapshot query则可以查询到全量数据(基本列数据+行式的增量日志数据)。
- 压缩触发是解决所有难题的关键,通过实施压缩策略,会快速缩新分区数据,来保证用户使用Read Optimized query可以查询到X分钟内的数据
Merge on Read Table是直接在DFS上启用近实时(near real-time)处理,而不是将数据复制到外部专用系统中。该表还有些次要的好处,例如通过避免数据的同步合并来减少写入放大(WA)
3.3 Index
Hudi通过索引机制将映射的给定的hoodie key(record key+partition path)映射到文件id(唯一标示),从而提供高效的upsert操作。记录键和文件组/文件ID之间的这种映射,一旦记录的第一个版本写入文件就永远不会改变。
3.4 File Layout
Hudi会在DFS分布式文件系统上的basepath基本路径下组织成目录结构。每张对应的表都会成多个分区,这些分区是包含该分区的数据文件的文件夹,与hive的目录结构非常相似。
在每个分区内,文件被组织成文件组,文件id为唯一标识。每个文件组包含多个切片,其中每个切片包含在某个提交/压缩即时时间生成的基本列文件(parquet文件),以及自生成基本文件以来对基本文件的插入/更新的一组日志文件(*.log)。Hudi采用MVCC设计,其中压缩操作会将日志和基本文件合并成新的文件片,清理操作会将未使用/较旧的文件片删除来回收DFS上的空间。
MVCC(Multi-Version Concurrency Control):多版本并行发控制机制
Multi-Versioning:产生多版本的数据内容,使得读写可以不互相阻塞
Concurrency Control:并发控制,使得并行执行的内容能保持串行化结果
四、典型场景
- DB入湖加速
相比昂贵且低效的传统批量加载和Merge,Hudi提供超大数据集的实时流式更新写入。通过实时的ETL,您可以直接将CDC(change data capture)数据写入数据湖,供下游业务使用。典型案例为采用Flink MySQL CDC Connector将RDBMS(MySQL)的Binlog写入Hudi表。
- 增量ETL
通过增量拉取的方式获取Hudi中的变更数据流,相对离线ETL调度,实时性更好且更轻量。典型场景是增量拉取在线服务数据到离线存储中,通过Flink引擎写入Hudi表,借助Presto或Spark引擎实现高效的OLAP分析。
- 消息队列
在小体量的数据场景下,Hudi也可以作为消息队列替代Kafka,简化应用开发架构。
- 数仓回填(backfill)
针对历史全量数据进行部分行、列的更新场景,通过数据湖极大减少计算资源消耗,提升了端到端的性能。典型案例是Hive场景下全量和增量的打宽。