数据湖三剑客:Delta Lake、Hudi 与 Iceberg 详解 | 青训营笔记

104 阅读6分钟

数据湖三剑客:Delta Lake、Hudi 与 Iceberg 详解

这是我参加「第四届青训营」笔记创作活动的的第 6 天。

01.发展历史

1.1 数据湖发展阶段 - Hadoop

  • 数据湖最开始的概念 ——分布式存储HDFS

  • 使用目录来区分不同的数据集

  • 好处:

    • 共享存储
    • 数据访问方便、灵活性高
  • 坏处:

    • 没有记录文件的schema(包括列名、列类型)

    • 难以得知数据集包含了那些文件, 是通过怎么的分区组织的

    • 如果多个程序都在修改这个数据集

    • 数据沼泽!

1.2 数据湖发展阶段 - Hive

数据湖的演进—— Hive Metastore

对数据湖中的数据集进行了集中“定义”

  • 数据湖中存在了哪些数据集
  • 它们都存储在什么目录
  • 数据集的schema是什么样子的
  • 数据集有哪些分区,每个分区的目录是什么

1.3 数据湖发展阶段 - 湖仓一体

什么是数据仓库?

  • 数据仓库将数据从数据源提取和转换,加载到目的地
  • 数据仓库 存储+计算 不分离
  • 严格控制写入数据的schema
数据仓库数据湖
成本
存储计算分离
ACID

湖仓一体(数据湖的现状):

  • 结合了数据湖和数据仓库的优势

  • 将数据仓库中对于数据的严格管理直接实现到了低成本的分布式存储之上

  • key features

    • Transaction ACID
    • Schema 管理
    • 存储计算分离
    • 支持多种计算引擎和文件格式

1.4 业界三大数据湖

Delta Lake、Hudi 与 Iceberg

1.5 关于”数据湖“

  • 相关概念比较新,一直处在演进中
  • 一开始是HDFS,裸pb,txt日志等,叫数据湖(管不动了就叫数据沼泽)
  • 后来出现了Iceberg、Hudi、Delta Lake了,数据湖概念就基本等同于这些产品了
  • 也更贴近于Lake House的概念

02.核心技术

问题引入:设计一个简单的数据湖

  1. 想存一些数据,按照date分区,schema是
useriddateeventphone
  1. 每天都会写入新数据
  2. 需要使用列存表格

2.1 文件结构

写入数据湖时

  1. 按照每条数据的date进行分区 6.png
  2. 额外使用 metadata 文件记录表信息,如下图所示 7.png

新的需求:把2020-01-01的数据更新了一下,想用新数据和老数据同时跑一下训练

2.2 Time travel

要点:

  1. 每次写入都生成一个新的元数据文件,记录变更
  2. 分区数据在update时,不要删除旧数据,保证新旧共存
  3. 元数据中存储具体的文件路径,而不仅仅是分区文件夹

1.png

  1. 每一次写入操作,创建一个新的json文件,以递增版本号命名,记录本次新增/删除的文件

  2. 每当产生N个json,做一次聚合,记录完整的分区文件信息

  3. 用checkpoint记录上次聚合的版本号

    2.png

两个人一起写,会有冲突吗?一人写一人读,读写冲突?

2.3 Transaction

Transaction:事务

数据湖中的AICD

  • 原子性: 本次写入要么对用户可见,要么不可见(需要设计)
  • 一致性: 输出是什么,落盘的就是什么(由计算引擎保证)
  • 事务隔离: 正确解决读写冲突和写写冲突(需要设计)
  • 持久性: 落完数据后,即便服务器重启结果不改变(由存储引擎保证)

2.3.1 原子性

写入流程:

  1. 写入 parquet 文件
  2. 写入 json 元数据文件

如何确保原子性(从用户可见性入手!)

  1. 用户只会读取以版本号数字命名的json文件,每次都读取到最大的版本号作为数据集的现状
  2. 新的写入写完parquet后开始写json文件,使用hash值对json文件命名,如a2fs4hfg8ee.json
  3. 直到json文件内容写完,利用hdfs的renameIfAbsent能力将a2fs4hfg8ee.json替换为000006.json,到此为止commit完成,新的读取将会以000006.json作为最新版本

读写冲突已被解决,how?

  1. 新的写入除非已经ommit,否则用户读不到
  2. 用户正在读的分区,被另一个写入进行了更新,数据不会进行替换,而是共存

2.3.2 事务隔离

Update写入流程

  1. 从最新的版本中,获取需要update的分区
  2. 乐观锁先把该写入的文件全落盘,然后进入写json阶段
  3. 分几种情况:
    1. 发现版本号和一开始没区别,直接写新的版本
    2. 发现版本号增加了,看看增加的这些版本有没有更新我要更新的 分区?
      • 没有,直接写新的版本
      • 有,两者更新了同一分区,需要重新 update了

若需要删去phone列

2.4 Schema Evolution

Add/Drop/Rename

03.各有所长

3.1 Iceberg 工作重点

3.1.1 Well-designed Metadata Layer

3.png

整体来看,s1相比s0,多的是最右边的data files。

3.1.2 Data File Filter

一些有助于filter的数据被层层记住,如:

  • Manifest file 记录了每个data file 的分区范围
  • Manifest list 记录了每个Manifest file 的分区范围
  • 分区可以被快速定位

3.1.3 Hidden Partition

可以进行分区的转换,如:按date分区 -> 按hour分区

  • 传统方式:新增hour列
  • Iceberg方式:设置partition transform方法

3.2 Hudi

Hadoop Upsert Delete Incremental

3.2.1 Timeline Service & Upsert & Incremental

  • Timeline Service: Hudi管理 transaction的方式,记录的信息类似于metadata
  • Upsert:每一条样本都有一个主键PK,当Upsert一条数据时,如果现有的数据中有这个PK,则update这条数据。否则直接insert这条数据
  • Incremental:某个时间后新增的数据

4.png

3.2.2 Copy on write

  • 数据在写入期间执行同步合并原有数据来更新并重写文件,数据每次提交都会进行隐式压缩,并附带时间戳。
  • 从这个图中,10:10提交了一份最新数据(粉色),那么在这个提交之前(比如10:07),进行数据查询的时候,只能查到10:05分(绿色)的数据,而正在进行的update/insert的数据相当于把10:05的数据copy一份,进行合并和更新,等到10;10commit之后,查询就是最新的粉色数据。

8.png

3.2.3 Merge on Read

增量数据放到append log里,并没有和历史数据进行合并,只有在compact(压缩)操作之后,才会与历史文件的合并和更新。那么10:10在查询数据的时候,实际上是历史数据(10:05)+增量数据(06,07,08,09,10)的合并。

9.png

  • 省去了很多的写入资源

3.3 Delta Lake 工作重点

3.3.1 支持流批一体

5.png