[湖仓架构] Apache Paimon核心原理
这是一篇较为完整的介绍Apache Paimon和Flink进阶应用的文章。
0 序
Apache Paimon 毕业(2024.04.16)
-
北京时间 2024 年 4 月 16日,开源软件基金会
Apache Software Foundation
(以下简称ASF
)正式宣布 Apache Paimon 毕业成为 Apache 顶 级项目(TLP, Top Level Project)。经过社区的共同努力和持续创新,Apache Paimon 在构建实时数据湖与流批处理技术领域取得了重大突破,数据湖步入实时新篇章! -
Apache 官方博客发布了 Apache Paimon 毕业的消息:
项目背景:大数据的不可能三角(成本、查询延时、新鲜度)
- 大数据系统都是有着不同侧重的考虑,甚至还要加上开发人员的精力,构建了一系列的权衡 (Trade-off),这个世界上目前并没有 Silver Bullet (银弹、具有极端有效性的解决方法),没有大一统的系统,每个场景有对应的解决方案。
流计算+存储 场景 1:实时预处理 | Queue + Flink(SQL + Stateful Streaming) + HBase/Redis/MySQL + Online Query
- 大概 10 年前,Storm 开源,8000 行 Clojure 代码组成一个完整的流计算系统,惊艳的设计,巧妙的 Ack 机制解决 At-least-once 的问题,解决流计算的基本诉求:
- 数据流起来。
- 数据不丢。但是随着业务的发展,流计算越来越需要 Exactly-once 的保证和 SQL API。
- 在还没有分布式流计算时,人们往往想通过批的调度来达到实时计算的效果。
- 最早使用 Hive 进行分钟级的调度,但是调度和进程启停的成本比较高,所以调度时延并不能太低。
- Spark 凭借 RDD 优秀的设计,在其之上提供了 Spark Streaming,进程常驻,数据每批次调度过去进行一次 mini-batch 的计算,完全复用了批计算的能力,达到了流计算的能力,以此来提供了 Exactly-once 的一致性保障,但是它并不是常驻进程的流计算,由于调度的开销,最低延时只能到分钟级。
-
Apache Flink
在 2014 年横空出世,通过内置状态存储和全局一致性快照设计,很好的解决了纯流(early-fire) 模式下保证 Exactly-once 的问题,实现了数据一致性和低延迟的兼顾。 -
同年笔者加入阿里 Galaxy 团队支撑阿里内部的流计算业务(Galaxy 系统的架构跟 Flink 有很多相似之处,限于篇幅不做展开),并且负责流计算入 Tair (阿里自研的一种 KV 存储,类似 Redis + HBase)的工作。当时的流计算可以说等同于 :流计算 + 在线 KV 服务:
本质上是一种将数据进行多维预处理的技术,预处理后的数据写入模式简单的 KV 系统(HBase 或 Redis)中,或者写入数据存储量较小的关系型数据库中。流计算在此中的作用是 计算流动的数据、并维护状态、把结果输出到 KV 系统中。
这个架构的优缺点分别是:
- 1、查询速度超快:业务需要查询到的数据都是已经准备好的,KV 系统和关系型数据库作为在线服务,提供毫秒级查询延时。
- 2、灵活性低、开发成本高:数据集有限(关系型数据库),或者数据模式受限(KV 系统),新增业务需要新增开发一条完整的链路。
它利用了KV系统或关系型数据库的可更新性,能够以幂等更新的方式在达到秒级实时的同时实现最终一致(类Exactly-once)的效果。
当然这个架构也是目前实时数仓比较主流的架构,对于重要业务来说,这是不二之选。
那有没有系统可以更灵活一点,不用为了业务的新增定制一条链路?
流计算+存储 场景 2:实时数仓 | Queue + Flink + OLAP + Online Query
- ClickHouse 由 Yandex 开发,在 2016 年开源,ClickHouse 看来只是一个单机的 OLAP 引擎,需要一些功夫也能拼出分布式的 OLAP,它的核心亮点是通过向量化的计算带来了超快的查询性能。
这带来了一个可能性,可以将业务数据放到
Clickhouse
中,能提供比较灵活的业务查询,而不仅仅只能预定义计算。
-
随着 ClickHouse 的逐渐流行,国内也涌现出很多优秀的 OLAP 项目,包括 Doris、StarRocks,阿里云的 Hologres,同时阿里内部 JStorm、Galaxy、Blink 合并成一个团队,大力发展 Blink,Blink 基于开源的 Flink,有着更新的架构、优秀的设计、高质量的代码和活跃的开源社区。
-
Flink + OLAP 系统(如: Clickhouse/Doris/Hologres) 形成了一个新的场景:通过 Flink 流计算做一些预处理,但是不用形成直接的业务数据,输出到 Hologres,这样 Hologres 保存相对更加完整的数据,提供给业务方高性能的查询,解决了部分 KV 系统不灵活导致无法灵活分析的问题**。
- 这个架构的好处是:
1、查询速度很快:OLAP 系统使用向量化计算,结合固态 SSD 磁盘,高性能机型,提供毫秒级的计算。
2、灵活性较高:OLAP 系统保存 Schema 化的数据,可以根据业务场景使用不同的查询模式。 (OLAP 系统有个通病是 分布式 Join 远不如 聚合,所以一般更多使用大宽表来满足查询性能。)
- 但这个架构也不是完全通用的,有个主要问题是:OLAP 的存储很贵。
OLAP 也做了一些工作来避免过高的成本,比如存算分离,但是它需要保证查询的实时性,所以从架构上仍然是高成本的设计。
1、其中 Flink 不仅仅只是做一些数据的打宽,也需要做一些数据ETL来确保最后落到 OLAP 系统的数据不会太多,这会带来额外的成本。
2、另外 OLAP 系统也不能保存太久的历史数据。比如,一般情况下,保存最近7天的数据,7天前的数据通过 TTL 被淘汰掉。
-
所以这里就有一个
Trade-Off
,是预处理(流计算)多一些,还是把更多的数据放入 OLAP 系统,不同的业务有不同的选择。 -
那有没有办法可以再灵活一点,以比较低的成本保存所有数据?
流计算+存储 场景 3:实时湖仓
流计算+存储 场景 3.1:Hive 实时化 | ... + Flink + Hive
-
既然 OLAP 存储比较贵,那我们就将流计算的结果写入到一个不那么贵的存储里,通过存储所有数据,提供灵活的 Ad-hoc 查询 (虽然没 OLAP 那么快)。
-
另外笔者加入 Blink 团队后,最开始的主要工作是 Blink 的批计算上,随着后续的发展,也在思考流批一体不仅仅只是计算的一体,计算的一体本质上并不能带来足够的业务价值。
-
开启 Flink + Hive 存储 (流批融合) 的一些工作:
Flink Hive Sink
支持写入 Parquet、ORC、CSV 等格式,提供 Exactly-once 的写入一致性保证,也支持配置分区提交的能力。这解决日志数据流入 Hive 离线数仓的能力,它的优缺点有:
- 1、离线数仓近实时化,为了一致性保证,延时是 Checkpoint 级别的,一般在分钟级。
- 2、存储成本低,存储大部分原始数据,非常灵活。
- 3、查询性能差,数据只是列存,存储到便宜的机器和便宜的磁盘上,读取较慢。
流计算+存储 场景 3.2:Iceberg 实时化
-
随着国外 Snowflake 和 Databricks 的崛起,湖存储成为了取代传统 Hive 数仓的新兴力量。
-
Apache Iceberg
是比较典型的湖存储,它作为替代 Hive 的方案有着以下好处:
- 1、提供 ACID 一致性保证:
>> a. 数据更安全了,这意味着你可以做一些小数据的操作:比如 INSERT INTO 一些数据,DELTE \ UPDATE \ MERGE_INTO 有着更好的支持。
>> b. 而不是像 Hive 一样,要安全的动数据只能 INSERT OVERWRITE 整个分区。- 2、易扩展的元数据管理:
>> a. 对象存储的 list 文件非常慢,使用 Iceberg 的元数据管理,可以避免 list 文件带来的性能瓶颈。(为什么要上对象存储,因为存储可分级————便宜啊)
>> b. 这意味着你可以摆脱 Hive Metastore 里面保存 Partition 信息的各种瓶颈问题 (HMS 的 Mysql 又挂了)。
>> c. 基于元数据的 Data Skipping,可以在离线数仓近实时化场景下,除了提升数据新鲜度,还能(基于排序字段的过滤)降低查询的延迟,也给进一步的索引加速带来了可能。
- 3、流读流写更容易和自然。
- 数据可以实时入湖,甚至
Iceberg
的数据也可以被实时流读。在Hive 离线数仓
的基础上增强了可用性。
但是,该解决方案缺乏对 Upsert 场景的支持,存在以下问题:
- 1、离线数仓最头疼的场景是 CDC 入仓,传统的全量表 + 增量表的方案,不仅存储成本高,计算成本也高,操作还繁琐。
- 2、流计算过程产生的 CDC 数据无法很好的处理。
CDC 入仓:全量与增量
- 传统的 Hive 数仓在解决
CDC
入仓时使用了 : 全量表 + 增量表的技术。
具体如图:
- 具体步骤如下:
- 1、等到 24 点后,同步当天的 Binlog 数据产出一份增量数据,形成增量表的一个分区。
- 2、将当天的增量表分区与前一天的全量表分区进行合并,产出全量表的当天分区。
- 3、全量表分区面向全量查询,增量表分区面向增量查询。
- 可以看出,这个过程的成本非常高,特别对于大全量、少增量的表:
- 1、存储成本非常高,每天一个全量,每天一个增量,完全没有复用。
- 2、计算成本非常高,每天都需要全量的读写合并。
- 3、延时 T + 1,只能用作离线查询。
- 4、离线表的准备时间也很长,需要等待全增量合并。
- 那假设有一个能够更好支持 Upsert 更新的数据湖系统,数据直接 Upsert 写进去怎么样?那此时数据湖里面的这张表就是业务数据库那张表的镜像,流 + 湖存储 需要一个支持按主键更新的数据湖!
流计算+存储 场景 3.3:Upsert 的探索 | Iceberg、Hudi、Delta、Hive-ACID
- 2020 年,笔者所在阿里云团队调研了三大数据湖:Iceberg、Hudi、Delta,当时阿里云的胡争 (后面的 Iceberg PMC 成员) 写了一篇非常好的文章:
- 深度对比 Delta、Iceberg 和 Hudi 三大开源数据湖方案 - INFOQ 里面的总结非常有趣:
- 如果用一个比喻来说明 Delta、Iceberg、Hudi、Hive-ACID 四者差异的话,可以把四个项目比做建房子。
- 1、 Delta 的房子底座相对结实,功能楼层也建得相对高,但这个房子其实可以说是 Databricks 的 。
(笔者补充:今天 Delta 依然是 Databricks 的,也对接了其它引擎,但成熟度不高,开源版本距离 Databricks 商业版也有一定差距)- 2、Iceberg 的建筑基础非常扎实,扩展到新的计算引擎或者文件系统都非常的方便,但是现在功能楼层相对低一点
(笔者补充:今天 Iceberg 建筑扎实,北美数仓 SAAS 强者众多,各个批数仓对接 Iceberg,在北美风生水起)- 3、Hudi 的情况有所不同,它的建筑基础设计不如 iceberg 结实,举个例子,如果要接入 Flink 作为 Sink 的话,需要把整个房子从底向上翻一遍,同时还要考虑不影响其他功能;但是 Hudi 的楼层是比较高的,功能是比较完善的 。
(笔者补充:Hudi 功能多,各种功能都有,目前在中国流行起来,Upsert 写入在中国打开了局面)- 4、Hive-ACID 的房子,看起来是一栋豪宅,绝大部分功能都有,但是细看这个豪宅的墙面是其实是有一些问题的 。
(笔者补充:Hive-ACID 国内应用案例较少)
扎实的房子对我们来说更具有诱惑力,我们坚信能基于 Iceberg 打造出属于 Streaming 的 Lakehouse,所以选取了 Iceberg 作为后续主要的方向来突破,胡争和笔者在 Iceberg 社区突破两个方向:
- 1、Flink 集成:如何基于 Flink 和 Iceberg 构建云原生数据湖?
- 2、CDC 入湖:Flink 如何实时分析 Iceberg 数据湖的 CDC 数据
-
一个阶段性产出是 Flink + Iceberg 进入生产可用,Flink 入湖成为主流应用,CDC 入湖基本可用。
-
但是,CDC 只是基本可用,离我们的想象还差的较远,大规模更新与近实时延时都有距离,更别说流读的增强了,主要的原因有如下:
- 1、Iceberg 社区基本盘还是在离线处理,它在国外的应用场景主要是离线取代
Hive
,它也有强力的竞争对手 Delta,很难调整架构去适配 CDC 流更新。
- 2、Iceberg 扩展性强,对其它计算引擎也暴露的比较多的优化空间,但是这也导致后续的发展难以迅速,涉及到众多已经对接好的引擎。
-
这并没有什么错,后面也证明了 Iceberg 主打离线数据湖和扩展性是有很大的优势,得到了众多国外厂商的支持。
-
Upsert 探索的另一个方向是 Flink + Hudi,当时阿里云的陈玉兆 (后面的 Hudi PMC 成员) 加入 Hudi 社区一起推出 Flink + Hudi Connector。
-
2021 年 12 月 FFA 的分享:使用 Flink Hudi 构建流式数据湖平台 - Aliyun
- Hudi 默认使用 Flink State 来保存 Key 到 FileGroup 的 Index,这是一套和 Spark 完全不同的玩法:
- 1、好处是全自动,想 Scale Up 只用调整并发就行了
- 2、坏处是性能差,直接让湖存储变成了实时点查,超过5亿条数据性能更是急剧下降。
- 3、存储成本也高,RocksDB State 保存所有索引。
- 4、数据非常容易不一致,甚至再也不能有别的引擎来读写,因为一旦读写就破坏了 State 里面的 Index。
- 针对 Flink State Index 诸多问题,字节跳动的工程师们在 Hudi 社区提出了 Bucket Index 的方案 :
- 此方案非常简单,但又解决问题:“给定 n 个桶, 用 Hash 函数决定某个记录属于哪个桶。最终所有分区被分成 N 个桶,每个桶对应一个 File Group。”
1、好处是去除了 Index 带来了诸多性能问题。
2、坏处是需要手动选取非常合适的 Bucket Number
,多了小文件操作很多,少了性能不行。
-
这套方案也是目前 Hudi 体量较大的用户的主流方案。
-
Hudi 在阿里云上也有不少的用户,但是随着用户的增多,问题浮现出来了:
- 1、Hudi 众多的模式让用户难以选择。
>> a. 使用 Flink State 还是 Bucket Index?一个易用性好、但是性能不行,一个难以使用。
>> b. 使用 CopyOnWrite 还是 Merge On Read?一个写入吞吐很差,一个查询性能很差。
- 2、更新效率低,1-3 分钟 Checkpoint 容易反压,默认 5 次 Checkpoint 合并,一般业务可接受的查询是查询合并后的数据;全增量一体割裂,难以统一。
- 3、系统设计复杂,Bugs 难以收敛,工单层出不穷 (笔者也当过半年 Hudi 负责人);各引擎之间的兼容性也非常差;参数众多。
-
Hudi
天然面向 Spark 批处理模式设计而诞生,不断在面向批处理的架构上进行细节改造,无法彻底适配流处理更新场景,在批处理架构上不断强行完善流处理更新能力,导致架构越来越复杂,可维护性越来越差。 -
Hudi 的稳定性随着近几个版本已经好很多了,我觉得这可以归功于来自中国的开发者和使用者们,他们解决 Hudi 各种各样的稳定性和正确性的漏洞,一步一步踩坑探索实时数据湖。
-
但是,看看最新的 Hudi Roadmap:hudi.apache.org/roadmap
-
你会发现 Flink 相关,流相关依然少的可怜,往往 Hudi 社区做了一个新 Feature,只有 Spark 支持,Flink 支持不了,如果 Flink 要支持,需要天翻地覆的重构,大重构又会诞生很多的 Bugs,还不一定能支持的很好。Hudi 是个好系统,但Hudi不是为实时数据湖而生的。
-
那我们需要什么?
- 1、一个湖存储有着类似 Iceberg 良好的建筑基础,功能满足湖存储的基本诉求。
- 2、一个具有很强
Upsert
能力的存储,需要 OLAP 系统 & 流计算 State & KV 系统 都使用的 LSM 结构。- 3、一个
Streaming First
,面向 Flink 有最好集成的存储,这座房子的地基应该直接考虑 Streaming & Flink 的场景,而不是在一个复杂的系统上修修补补,越走到后面,越吃力。- 4、一个更多面向中国开发者以及使用者的社区,而且社区的主方向应该是长期投入 Streaming + Lake。
- Streaming Lakehouse 的路还很长,那就造一个吧。
Apache Paimon 的诞生 //TODO
1 项目简介
简介
- Flink 社区希望能够将 Flink 的 Streaming 实时计算能力和 Lakehouse 新架构优势进一步结合,推出新一代的 Streaming Lakehouse 技术,促进数据在数据湖上真正实时流动起来,并为用户提供实时离线一体化的开发体验。为此,Flink 社区内部孵化了 Flink Table Store (简称
FTS
)子项目,一个真正面向 Streaming 以及 Realtime 的数据湖存储项目。
2023年3月12日,FTS 进入 Apache 软件基金会 (ASF) 的孵化器,改名为 Apache Paimon (incubating)。
Apache Paimon
是一个流数据湖平台,具有高速数据摄取、变更日志跟踪和高效的实时分析的能力。
发展历程
Apache Paimon
原名Flink Table Store
,2022年1月在 Apache Flink 社区从零开始研发,Flink 社区希望能够将 Flink 的 Streaming 实时计算能力和 Lakehouse 新架构优势进一步结合,促进数据在数据湖上真正实时流动起来,并为用户提供实时离线一体化的开发体验。
-
2023 年 3 月 12 日,
Flink Table Store
项目顺利通过投票,正式进入 Apache 软件基金会 (ASF) 的孵化器,改名为Apache Paimon
,一个真正面向Streaming
以及Realtime
的数据湖存储项目。之后在导师 Yu Li、Becket Qin、Stephan Ewen、 Robert Metzger 的指导下,由Apache孵化器管理委员会成员进行辅导和孵化。 -
2024 年 3 月 20 日,
Apache
董事会通过Apache Paimon
毕业决议,结束了为期一年的孵化,正式确定 Apache Paimon 成为 Apache 顶 级项目。 -
孵化的一年间,Paimon 社区的贡献者和关注者都获得了非常大的提升。
- Paimon 在2023-2024这一年里发布了四个大版本,并在大量企业生产实践中使用,包括 阿里巴巴、字节跳动、同程旅行、蚂蚁集团、中国联通、网易、中原银行、汽车之家、平安证券、喜马拉雅等企业。
广泛应用于实时数据湖的构建,帮助数据库更好的 CDC 入湖,帮助构建近实时流式湖仓,帮助企业提升数据时效性价值,获取业务实时化效果。
读/写:Paimon 支持多种读/写数据和执行 OLAP 查询的方式
-
(1)对于读取,它支持以下方式消费数据
从历史快照(批处理模式)、从最新的偏移量(在流模式下),或以混合方式读取增量快照。 -
(2)对于写入,它支持来自数据库变更日志(CDC)的流式同步或来自离线数据的批量插入/覆盖。
生态系统
- 除了Apache Flink之外,Paimon还支持Apache Hive、Apache Spark、Trino等其他计算引擎的读取。
内部
- 在底层,Paimon 将列式文件存储在文件系统/对象存储上,并使用 LSM 树结构来支持大量数据更新和高性能查询。
统一存储
- 对于 Apache Flink 这样的流引擎,通常有三种类型的连接器:
- 消息队列:例如 Apache Kafka,在源阶段和中间阶段都使用它,以保证延迟保持在秒级
- OLAP系统:例如Clickhouse,它以流方式接收处理后的数据并为用户的即席查询提供服务
- 批量存储:例如Apache Hive,它支持传统批处理的各种操作,包括INSERT OVERWRITE
- Paimon 提供表抽象。它的使用方式与传统数据库没有什么区别:
- 在批处理执行模式下,它就像一个Hive表,支持Batch SQL的各种操作。查询它以查看最新的快照。
- 在流执行模式下,它的作用就像一个消息队列。查询它的行为就像从历史数据永不过期的消息队列中查询流更改日志。
2 核心特性
-
Apache Paimon 是一个湖格式,结合 Flink 及 Spark 构建流批处理的实时湖仓一体架构。Paimon 创新的结合湖格式与 LSM 技术,给数据湖带来了实时流更新以及完整的流处理能力。
-
在过去的孵化期间,Paimon 通过技术创新不断克服挑战,展现出了以下关键特性:
● 实时入湖能力增强:Paimon 提供了一系列的入湖工具,自动同步 Schema 变更,允许快速将包括 MySQL 在内的多种数据库系统的实时变化同步至数据湖,即便在千万级数据规模下也能保持高效率与低延迟。
● 湖上批流一体处理:Paimon 结合 Flink 提供完整的流处理能力,结合 Spark 提供完整的批处理能力。基于统一的数据湖存储,提供数据口径一致的批流一体处理,提高易用性并降低成本。
● 全面生态集成拓展:Paimon 已经与众多开源工具和技术栈紧密集成,支持大数据典型计算引擎,包括 Flink、Spark、Hive、Trino、Presto、StarRocks、Doris 等等,统一存储,计算无边界。
● 湖仓存储格式革新:Paimon 持续创新,引入新功能,在流批技术处理的基础上,提出 Deletion Vectors 和索引来增强查询性能,在分钟级时效性基础上满足流、批、OLAP 等场景的全方位支持。
- Apache Paimon 的毕业意味着该项目已经在社区治理、代码质量、文档完善度以及用户采用度等方面达到了 Apache 社区严格的标准要求,得到了广泛认可。这将进一步加速项目的普及与应用,推动实时数据湖技术在全球范围内的广泛应用。
1)统一批处理和流处理
批量写入和读取、流式更新、变更日志生成,全部支持。
2)数据湖能力
低成本、高可靠性、可扩展的元数据。Apache Paimon 具有作为数据湖存储的所有优势。
3)各种合并引擎
按照您喜欢的方式更新记录。保留最后一条记录、进行部分更新或将记录聚合在一起,由您决定。
4)变更日志生成
Apache Paimon 可以从任何数据源生成正确且完整的变更日志,从而简化您的流分析。
5)丰富的表类型
除了主键表之外,Apache Paimon还支持append-only表,提供有序的流式读取来替代消息队列。
6)模式演化
Apache Paimon 支持完整的模式演化。您可以重命名列并重新排序。
3 基本概念
Snapshot
快照捕获表在某个时间点的状态。用户可以通过最新的快照来访问表的最新数据。通过时间旅行,用户还可以通过较早的快照访问表的先前状态。
Partition
-
Paimon 采用与 Apache Hive 相同的分区概念来分离数据。
-
分区是一种可选方法,可根据日期、城市和部门等特定列的值将表划分为相关部分。每个表可以有一个或多个分区键来标识特定分区。
-
通过分区,用户可以高效地操作表中的一片记录。
-
如果定义了主键,则分区键必须是主键的子集。
Bucket
-
未分区表或分区表中的分区被细分为存储桶,以便为可用于更有效查询的数据提供额外的结构。
-
桶的范围由记录中的一列或多列的哈希值确定。用户可以通过提供bucket-key选项来指定分桶列。如果未指定bucket-key选项,则主键(如果已定义)或完整记录将用作存储桶键。
-
桶是读写的最小存储单元,因此桶的数量限制了最大处理并行度。不过这个数字不应该太大,因为它会导致大量小文件和低读取性能。一般来说,建议每个桶的数据大小为1GB左右。
Consistency Guarantees一致性保证
-
Paimon writer使用两阶段提交协议以原子方式将一批记录提交到表中。每次提交在提交时最多生成两个快照。
-
对于任意两个同时修改表的writer,只要他们不修改同一个存储桶,他们的提交都是可序列化的。如果他们修改同一个存储桶,则仅保证快照隔离。也就是说,最终表状态可能是两次提交的混合,但不会丢失任何更改。
4 文件布局
- 一张表的所有文件都存储在一个基本目录下。Paimon 文件以分层方式组织。下图说明了文件布局。从快照文件开始,Paimon 读者可以递归地访问表中的所有记录。
下面简单介绍文件布局。
Snapshot Files
- 所有快照文件都存储在快照目录中。
- 快照文件是一个 JSON 文件,包含有关此快照的信息,包括:
- 正在使用的Schema文件
- 包含此快照的所有更改的清单列表(manifest list)
Manifest Files
- 所有清单列表(manifest list)和清单文件(manifest file)都存储在清单(manifest)目录中。
- 清单列表(manifest list)是清单文件名(manifest file)的列表。
- 清单文件(manifest file)是包含有关 LSM 数据文件和更改日志文件的文件信息。例如对应快照中创建了哪个LSM数据文件、删除了哪个文件。
Data Files
- 数据文件按分区和存储桶分组。每个存储桶目录都包含一个 LSM 树及其变更日志文件。
- 目前,Paimon 支持使用 orc(默认)、parquet 和 avro 作为数据文件格式。
LSM Trees
- Paimon 采用 LSM 树(日志结构合并树)作为文件存储的数据结构。
Sorted Runs
-
LSM 树将文件组织成多个Sorted Run。Sorted Run由一个或多个数据文件组成,并且每个数据文件恰好属于一个Sorted Run。
-
数据文件中的记录按其主键排序。在Sorted Run中,数据文件的主键范围永远不会重叠。
-
正如您所看到的,不同的Sorted Run可能具有重叠的主键范围,甚至可能包含相同的主键。查询LSM树时,必须合并所有Sorted Run,并且必须根据用户指定的合并引擎和每条记录的时间戳来合并具有相同主键的所有记录。
-
写入LSM树的新记录:将首先缓存在内存中。当内存缓冲区满时,内存中的所有记录将被排序并刷新到磁盘。
Compaction
-
当越来越多的记录写入LSM树时,Sorted Run的数量将会增加。由于查询LSM树需要将所有Sorted Run合并起来,太多Sorted Run将导致查询性能较差,甚至内存不足。
-
为了限制Sorted Run的数量,我们必须偶尔将多个Sorted Run合并为一个大的Sorted Run。这个过程称为Compaction。
-
然而,Compaction是一个资源密集型过程,会消耗一定的CPU时间和磁盘IO,因此过于频繁的Compaction可能会导致写入速度变慢。这是查询和写入性能之间的权衡。Paimon 目前采用了类似于 Rocksdb 通用压缩的Compaction策略。
-
默认情况下,当Paimon将记录追加到LSM树时,它也会根据需要执行Compaction。用户还可以选择在“专用Compaction作业”中独立执行所有Compaction。
Y 进阶: 集成Flink
写入性能
- Paimon的写入性能与检查点密切相关。若需要更大的写入吞吐量,则:
增加检查点间隔,或者仅使用批处理模式。
增加写入缓冲区大小。
启用写缓冲区溢出。
如果您使用固定存储桶模式,请重新调整存储桶数量。
- 并行度
建议sink的并行度小于等于bucket的数量,最好相等。
- Compaction
当Sorted Run数量较少时,Paimon writer 将在单独的线程中异步执行压缩,因此记录可以连续写入表中。然而,为了避免Sorted Runs的无限增长,当Sorted Run的数量达到阈值时,writer将不得不暂停写入。下表属性确定阈值。
当 num-sorted-run.stop-trigger 变大时,写入停顿将变得不那么频繁,从而提高写入性能。但是,如果该值变得太大,则查询表时将需要更多内存和 CPU 时间。如果您担心内存 OOM,请配置sort-spill-threshold。它的值取决于你的内存大小。
- 优先考虑写入吞吐量
如果希望某种模式具有最大写入吞吐量,则可以缓慢而不是匆忙地进行Compaction。可以对表使用以下策略
num-sorted-run.stop-trigger = 2147483647
sort-spill-threshold = 10
此配置将在写入高峰期生成更多文件,并在写入低谷期逐渐合并到最佳读取性能。
- 触发Compaction的Sorted Run数
- Paimon使用LSM树,支持大量更新。LSM 在多次Sorted Runs中组织文件。从 LSM 树查询记录时,必须组合所有Sorted Runs以生成所有记录的完整视图。
- 过多的Sorted Run会导致查询性能不佳。为了将Sorted Run的数量保持在合理的范围内,Paimon writers 将自动执行Compaction。下表属性确定触发Compaction的最小Sorted Run数。
- 写入初始化
- 在write初始化时,bucket的writer需要读取所有历史文件。如果这里出现瓶颈(例如同时写入大量分区),可以使用write-manifest-cache缓存读取的manifest数据,以加速初始化。
- 内存
Paimon writer中主要占用内存的地方有:
- Writer的内存缓冲区,由单个任务的所有Writer共享和抢占。该内存值可以通过 write-buffer-size 表属性进行调整。
- 合并多个Sorted Run以进行Compaction时会消耗内存。可以通过 num-sorted-run.compaction-trigger 选项进行调整,以更改要合并的Sorted Run的数量。
- 如果行非常大,在进行Compaction时一次读取太多行数据可能会消耗大量内存。减少 read.batch-size 选项可以减轻这种情况的影响。
- 写入列式(ORC、Parquet等)文件所消耗的内存,不可调。
读取性能
Full Compaction
- 配置full-compaction.delta-commits在Flink写入中定期执行full-compaction。并且可以确保在写入结束之前分区被完全Compaction。
注意:Paimon 默认处理小文件并提供良好的读取性能。请不要在没有任何要求的情况下配置此Full Compaction选项,因为它会对性能产生重大影响。
主键表
-
对于主键表来说,这是一种
MergeOnRead
技术。读取数据时,会合并多层LSM数据,并行数会受到桶数的限制。虽然Paimon的merge会高效,但是还是赶不上普通的AppendOnly表。 -
如果你想在某些场景下查询得足够快,但只能找到较旧的数据,你可以:
- 配置full-compaction.delta-commits,写入数据时(目前只有Flink)会定期进行full Compaction。
- 配置
scan.mode
为compacted-full
,读取数据时,选择full-compaction的快照。读取性能良好。
仅追加表
-
小文件会降低读取速度并影响 DFS 稳定性。默认情况下,当单个存储桶中的小文件超过“compaction.max.file-num”(默认50个)时,就会触发compaction。但是当有多个桶时,就会产生很多小文件。
-
您可以使用full-compaction来减少小文件。full-compaction将消除大多数小文件。
格式
- Paimon 对 parquet 读取进行了一些查询优化,因此 parquet 会比 orc 稍快一些。
多Writer并发写入
-
Paimon的快照管理支持向多个writer写入。
-
默认情况下,Paimon支持对不同分区的并发写入。推荐方式是
streaming job
将记录写入Paimon的最新分区;同时批处理作业
(覆盖)将记录写入历史分区。
- 如果需要多个Writer写到同一个分区,事情就会变得有点复杂。例如,不想使用 UNION ALL,那就需要有多个流作业来写入"partial-update"表。参考如下的"Dedicated Compaction Job"。
Dedicated Compaction Job
- 默认情况下,Paimon writer 在写入记录时会根据需要执行Compaction。这对于大多数用例来说已经足够了,但有两个缺点:
- 这可能会导致写入吞吐量不稳定,因为执行压缩时吞吐量可能会暂时下降。
- Compaction会将某些数据文件标记为“已删除”(并未真正删除)。如果多个writer标记同一个文件,则在提交更改时会发生冲突。Paimon 会自动解决冲突,但这可能会导致作业重新启动。
- 为了避免这些缺点,用户还可以选择在writer中跳过Compaction,并仅运行专门的作业来进行Compaction。由于Compaction仅由专用作业执行,因此writer可以连续写入记录而无需暂停,并且不会发生冲突。
Flink SQL
目前不支持compaction相关的语句,所以我们必须通过flink run
来提交compaction
作业。
<FLINK_HOME>/bin/flink run \
/path/to/paimon-flink-action-0.5-SNAPSHOT.jar \
compact \
–warehouse \
–database \
–table \
[–partition ] \
[–catalog-conf [–catalog-conf …]] \
- 如果提交一个批处理作业(execution.runtime-mode:batch),当前所有的表文件都会被Compaction。如果您提交一个流作业(execution.runtime-mode: Streaming),该作业将持续监视表的新更改并根据需要执行Compaction。
表管理
管理快照
1)快照过期
-
Paimon Writer每次提交都会生成一个或两个快照。每个快照可能会添加一些新的数据文件或将一些旧的数据文件标记为已删除。然而,标记的数据文件并没有真正被删除,因为Paimon还支持时间旅行到更早的快照。它们仅在快照过期时被删除。
-
目前,Paimon Writer在提交新更改时会自动执行过期操作。通过使旧快照过期,可以删除不再使用的旧数据文件和元数据文件,以释放磁盘空间。
-
设置以下表属性:
-
注意,保留时间太短或保留数量太少可能会导致如下问题:
- 批量查询找不到该文件。例如,表比较大,批量查询需要10分钟才能读取,但是10分钟前的快照过期了,此时批量查询会读取到已删除的快照。
- 表文件上的流式读取作业(没有外部日志系统)无法重新启动。当作业重新启动时,它记录的快照可能已过期。(可以使用Consumer Id来保护快照过期的小保留时间内的流式读取)。
2)回滚快照
<FLINK_HOME>/bin/flink run \
/path/to/paimon-flink-action-0.5-SNAPSHOT.jar \
rollback-to \
–warehouse \
–database \
–table \
–snapshot \
[–catalog-conf [–catalog-conf …]]
管理分区
-
创建分区表时可以设置partition.expiration-time。Paimon会定期检查分区的状态,并根据时间删除过期的分区。
-
判断分区是否过期:将分区中提取的时间与当前时间进行比较,看生存时间是否超过partition.expiration-time。比如:
CREATE TABLE T (…) PARTITIONED BY (dt) WITH (
'partition.expiration-time' = '7 d',
'partition.expiration-check-interval' = '1 d',
'partition.timestamp-formatter' = 'yyyyMMdd'
);
管理小文件
小文件可能会导致:
- 稳定性问题:HDFS中小文件过多,NameNode会承受过大的压力。
- 成本问题:HDFS中的小文件会暂时使用最小1个Block的大小,例如128MB。
- 查询效率:小文件过多查询效率会受到影响。
1)Flink Checkpoint的影响
-
使用Flink Writer,每个checkpoint会生成 1-2 个快照,并且checkpoint会强制在 DFS 上生成文件,因此checkpoint间隔越小,会生成越多的小文件。
-
默认情况下,不仅checkpoint会导致文件生成,writer的内存(write-buffer-size)耗尽也会将数据flush到DFS并生成相应的文件。可以启用 write-buffer-spillable 在 writer 中生成溢出文件,从而在 DFS 中生成更大的文件。
所以,可以设置如下:
增大checkpoint间隔
增加 write-buffer-size 或启用 write-buffer-spillable
2)快照的影响
-
Paimon维护文件的多个版本,文件的Compaction和删除是逻辑上的,并没有真正删除文件。文件只有在 Snapshot 过期后才会被真正删除,因此减少文件的第一个方法就是减少 Snapshot 过期的时间。Flink writer 会自动使快照过期。
-
分区和分桶的影响
表数据会被物理分片到不同的分区,里面有不同的桶,所以如果整体数据量太小,单个桶中至少有一个文件,建议你配置较少的桶数,否则会出现也有很多小文件。
3)主键表LSM的影响
- LSM 树将文件组织成Sorted Runs的运行。Sorted Runs由一个或多个数据文件组成,并且每个数据文件恰好属于一个Sorted Runs。
- 默认情况下,Sorted Runs数取决于 num-sorted-run.compaction-trigger,这意味着一个桶中至少有 5 个文件。如果要减少此数量,可以保留更少的文件,但写入性能可能会受到影响。
4)仅追加表的文件的影响
默认情况下,Append-Only 还会进行自动Compaction以减少小文件的数量
对于分桶的 Append-only 表,为了排序会对bucket内的文件行Compaction,可能会保留更多的小文件。
5)Full-Compaction的影响
主键表是5个文件,但是Append-Only表(桶)可能单个桶里有50个小文件,这是很难接受的。更糟糕的是,不再活动的分区还保留了如此多的小文件。
建议配置Full-Compaction,在Flink写入时配置‘full-compaction.delta-commits’定期进行full-compaction。并且可以确保在写入结束之前分区被full-compaction。
G 进阶: 缩放Bucket
1)说明
- 由于总桶数对性能影响很大,Paimon 允许用户通过 ALTER TABLE 命令调整桶数,并通过 INSERT OVERWRITE 重新组织数据布局,而无需重新创建表/分区。当执行覆盖作业时,框架会自动扫描旧桶号的数据,并根据当前桶号对记录进行哈希处理。
– rescale number of total buckets
ALTER TABLE table_identifier SET (‘bucket’ = ‘…’)
– reorganize data layout of table/partition
INSERT OVERWRITE table_identifier [PARTITION (part_spec)]
SELECT …
FROM table_identifier
[WHERE part_spec]
注意:
ALTER TABLE 仅修改表的元数据,不会重新组织或重新格式化现有数据。重新组织现有数据必须通过INSERT OVERWRITE
来实现。
重新缩放桶数不会影响读取和正在运行的写入作业。
一旦存储桶编号更改,任何新安排的 INSERT INTO
作业写入未重新组织的现有表/分区将抛出 TableException
,并显示如下类似异常:
Try to write table/partition … with a new bucket num …,
but the previous bucket num is … Please switch to batch mode,
and perform INSERT OVERWRITE to rescale current data layout first.
- 对于分区表,不同的分区可以有不同的桶号。例如:
ALTER TABLE my_table SET ('bucket' = '4');
INSERT OVERWRITE my_table PARTITION (dt = '2022-01-01')
SELECT * FROM …;
ALTER TABLE my_table SET ('bucket' = '8');
INSERT OVERWRITE my_table PARTITION (dt = '2022-01-02')
SELECT * FROM …;
在覆盖期间,确保没有其他作业写入同一表/分区。
注意:对于启用日志系统的表(例如Kafka),请重新调整主题的分区以保持一致性。
重新缩放存储桶有助于处理吞吐量的突然峰值。假设有一个每日流式ETL任务来同步交易数据。该表的DDL和管道如下所示。
2)官方示例
如下是正在跑的一个作业:
建表
CREATE TABLE verified_orders (
trade_order_id BIGINT,
item_id BIGINT,
item_price DOUBLE,
dt STRING,
PRIMARY KEY (dt, trade_order_id, item_id) NOT ENFORCED
) PARTITIONED BY (dt)
WITH (
'bucket' = '16'
);
kafka表
CREATE temporary TABLE raw_orders(
trade_order_id BIGINT,
item_id BIGINT,
item_price BIGINT,
gmt_create STRING,
order_status STRING
) WITH (
'connector' = 'kafka',
'topic' = '…',
'properties.bootstrap.servers' = '…',
'format' = 'csv'
…
);
流式插入16个分桶
INSERT INTO verified_orders
SELECT trade_order_id,
item_id,
item_price,
DATE_FORMAT(gmt_create, 'yyyy-MM-dd') AS dt
FROM raw_orders
WHERE order_status = 'verified';
过去几周运行良好。然而,最近数据量增长很快,作业的延迟不断增加。为了提高数据新鲜度,用户可以执行如下操作缩放分桶:
(1)使用保存点暂停流作业
$ ./bin/flink stop \
–savepointPath /tmp/flink-savepoints \
$JOB_ID
(2)增加桶数
ALTER TABLE verified_orders SET ('bucket' = '32');
(3)切换到批处理模式并覆盖流作业正在写入的当前分区
SET 'execution.runtime-mode' = 'batch';
– 假设今天是2022-06-22
- 情况1:没有更新历史分区的延迟事件,因此覆盖今天的分区就足够了
INSERT OVERWRITE verified_orders PARTITION (dt = '2022-06-22')
SELECT trade_order_id,
item_id,
item_price
FROM verified_orders
WHERE dt = '2022-06-22';
- 情况2:有更新历史分区的延迟事件,但范围不超过3天
INSERT OVERWRITE verified_orders
SELECT trade_order_id,
item_id,
item_price,
dt
FROM verified_orders
WHERE dt IN ('2022-06-20', '2022-06-21', '2022-06-22');
(4)覆盖作业完成后,切换回流模式,从保存点恢复(可以增加并行度=新bucket数量)。
SET 'execution.runtime-mode' = 'streaming';
SET 'execution.savepoint.path' = ;
INSERT INTO verified_orders
SELECT trade_order_id,
item_id,
item_price,
DATE_FORMAT(gmt_create, 'yyyy-MM-dd') AS dt
FROM raw_orders
WHERE order_status = 'verified';
P 经典项目案例
自如基于 Apache StreamPark™ + Paimon 实现数据一键入湖最佳实践
X 参考文献
- Apache Paimon
- Apache Sea Tunnel
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!