1.Paimon相关架构
所有相关的Paimon的架构建议去看Paimon基本概念
2.Paimon读取流程
首先,需要知道计算层和存储层之间是怎么连接的
- 计算层:就是Flink、Spark、Hive这些计算引擎
- 存储层:在上面文章已经说过,就是snapshot、Manifest、Sorted Run这些存在hdfs/s3的文件
- 写入的连接器:Paimon Sink,具体流程看:Flink-To-Paimon 2pc写入机制
- 读取的连接器:Paimon Source
接下来,就可以根据读取流程展开了
(1) 读取初始化 -- 从存储层就开始减少读取量
- 确定读取模式与快照 :
- 批量读取 (Batch Scan) :通常会读取表的最新快照 (Latest Snapshot) 。用户也可以通过
scan.snapshot-id或scan.timestamp-millis指定一个历史快照,实现时间旅行。 - 流式读取 (Streaming Scan) :会从一个起始点(如最新快照)开始,并持续监控新的快照生成,以消费增量数据。Paimon 通过
Consumer-ID来跟踪每个消费者的进度。
- 批量读取 (Batch Scan) :通常会读取表的最新快照 (Latest Snapshot) 。用户也可以通过
- 下推优化 (Pushdown) :这是至关重要的性能优化步骤。Paimon 的
Source实现了 Flink/Spark 的SupportsPushDown接口。计算引擎会将查询计划中的Filter(过滤条件下推) 和Projection(列裁剪下推) 信息传递给Source。- Filter Pushdown :将
WHERE中的条件尽可能地下推到 Paimon 的存储层。这意味着 Paimon 在扫描阶段就可以利用这些条件进行数据跳过,而不是将所有数据读到引擎层再进行过滤。 - Projection Pushdown:只读取查询真正需要的列。对于列式存储(Parquet/ORC),这可以极大地减少 I/O。
- Filter Pushdown :将
- 生成读取切分 (Splits) :Paimon 会根据表的分区和桶 信息,将读取任务划分成多个独立的切分 (Split) 。每个 Split 通常对应一个或多个桶的数据。这些 Splits 会被分发给下游并行的
SourceReader任务来执行,从而实现并行读取。
(2) 文件扫码和策略选择
每个并行的 SourceReader 任务拿到分配给它的 Split 后,就开始了真正的文件扫描和选择过程。这个过程的核心是解析元数据并应用各种过滤策略。
- 解析快照与清单 (Snapshot & Manifest) :Reader 首先根据
Split中的快照信息,找到对应的Manifest List文件,然后根据清单列表找到所有的Manifest文件。 - 应用过滤规则 :Reader 会遍历
Manifest文件中记录的所有数据文件条目,并应用一系列过滤规则来决定哪些文件需要被读取:- 分区与桶过滤 :
Split本身就已经限定了分区和桶的范围,这是第一层过滤。 - 统计信息过滤 (Min/Max Stats) :利用下推的
Filter条件,与Manifest中记录的每个数据文件的列统计信息进行比较。任何不满足WHERE条件范围的文件都会被直接跳过。 - 文件索引过滤 (File Index) :如果为查询涉及的列配置了文件索引,Reader 会读取索引文件(或从 Manifest 中获取内嵌的小索引),并使用布隆过滤器或位图来进一步过滤文件。一个文件只有在通过了所有索引检查后,才会被加入到最终的"待读列表"中。
- 分区与桶过滤 :
(3) 数据合并读取
经过层层筛选,Reader 终于得到了一个需要物理读取的数据文件列表。对于追加表 ,流程相对简单,只需逐个读取文件并返回记录即可。但对于主键表 ,则进入了其最核心也最复杂的阶段------K-路归并与记录合并。
- K-路归并 (K-Way Merge) :
由于主键表的 LSM 结构,一个Split(通常是一个桶)中的数据分散在多个 Sorted Run(即多个有序文件)中。Reader 需要为每个文件创建一个迭代器,然后将这K个迭代器的数据进行归并排序,以确保输出的记录流是严格按照主键有序的。 为了高效地执行 K-路归并,Paimon 采用了先进的算法:
败者树 (Loser Tree) :相比于传统的最小堆(Min-Heap)算法,败者树在
K值很大时能显著减少比较次数,从而提升归并效率。Paimon 社区通过PIP-2引入了基于败者树的SortMergeReaderWithLoserTree,这已成为高性能读取的关键组件。
-
合并引擎 (Merge Engine) :
归并后的数据流中,同一个主键可能会有多条记录(来自不同的 LSM 层级或更新操作)。此时,合并引擎,它负责根据预设的策略处理这些多版本记录,只保留一条最终的正确记录。
- deduplicate :这是最常见的策略。它会比较具有相同主键记录的
sequence number(一个单调递增的编号) 或时间戳字段,只保留最新的那一 条。 - partial-update:支持部分列更新。它会合并多条记录的非空字段,生成一个完整的记录。这对于更新宽表的部分字段非常有用。
- aggregation :允许在记录合并时进行聚合操作,如
SUM,MAX等。 - first-row:保留遇到的第一条记录,忽略后续的更新。
- deduplicate :这是最常见的策略。它会比较具有相同主键记录的
-
Changelog 生成 :在流式读取场景下,Paimon 还可以在合并过程中生成
Changelog流(包含+I,-U,+U,-D等行类型),这对于下游需要精确捕获数据变更的流计算任务至关重要。changelog-producer配置项控制了Changelog的生成方式,如full-compaction可以在全量合并时产出完整的变更日志。
(4) 数据的序列化与反序列化
最后一步,从数据文件中读取的二进制数据需要被反序列化成计算引擎能够理解的内存对象(如 Flink 的 RowData)。由于 Projection Pushdown 的存在,Paimon 的 Reader 只会反序列化查询所需的列,避免了不必要的 CPU 开销。Paimon 的类型系统确保了从存储格式到引擎内存对象的精确转换。
3.Paimon针对读取查询的优化
(1)文件格式
Paimon 支持多种主流的数据文件格式,允许用户根据读写负载和存储成本进行权衡。
- Parquet:(1.0之后)默认的列式存储格式。它具有出色的压缩率和查询性能,查询时只需读取所需的列,减少了I/O。Paimon 利用 Parquet 的字典编码、行程长度编码(RLE)和位打包等技术来优化存储和读取效率。
- ORC (Optimized Row Columnar) :(0.8默认是ORC)另一种高效的列式存储格式,与 Hive 生态兼容性良好。在某些场景下,ORC 也能提供与 Parquet 相媲美的性能。
- Avro:一种行式存储格式。与列式存储相比,Avro 在写入密集型或需要频繁读取整行数据的场景中可能更具优势。它的 Schema 演进能力也做得非常出色。
- CSV / JSON:主要用于调试和实验目的,不建议在生产环境中使用,因为它们的存储效率和查询性能远低于列式格式。
如何选择?
- 读多写少场景,推荐使用 Parquet。
- 如果业务需要频繁地进行全行更新或读取,可以考虑使用 Avro。
类型映射与精度问题:Paimon 维护了一套自身的逻辑数据类型,并负责将其精确地映射到所选文件格式的物理类型。这个过程并非总是无损的,需要注意一些边界情况:
- 高精度时间戳 :在 Parquet 中,超出微秒精度的时间戳(
TIMESTAMP(p)with p > 6)可能会被存储为INT96类型,这在跨平台读取时可能存在兼容性问题。 - 复杂类型 :对于
MAP类型,Parquet 不支持key为可空(nullable)。 - 时区处理 :
TIMESTAMP_LOCAL_ZONE类型在不同格式(如 ORC)中可能存在时区转换的细微差异,读取时需确保时区上下文的一致性。
(2) Compaction控制读放大
Compaction 是 LSM-Tree 的"新陈代谢"过程。Paimon 的后台任务会定期检查各层的文件数量和大小,当满足一定条件时(如 L0 文件数超过 num-sorted-run.compaction-trigger),就会触发合并。合并过程会将多个较小的、可能重叠的 Sorted Run 读入内存,进行归并排序,并写成一个或多个更大的、有序的、主键范围更连续的新文件,通常会放入下一层级。
这个过程的主要目的在于:
- 减少文件数量 :降低读取时需要扫描和归并的文件总数,即控制读放大。
- 消除数据冗余:合并同一主键的多个版本,保留最终状态。
- 优化数据布局:使数据在物理上更加有序,提升范围查询性能。
对读取的影响:
LSM-Tree 结构决定了主键表的读取操作不是简单地读取一个文件,而是需要同时读取来自多个 Level、多个 Sorted Run 的数据,并对它们进行 K-路归并 。一个主键的最新数据可能在 L0,较老的数据在 L1,更老的数据在 L2。因此,读取器必须能够高效地合并这些数据流,并根据 sequence number 或时间戳来确定哪个版本是最新的,这个过程被称为 Merge-on-Read。
同时,compacti的时候可配置CALL sys.compact('my_db.my_table', 'Z-ORDER', 'col1,col2'),Z-Order 排序能优化多维数据的空间局部性,极大提升多列范围查询的过滤效果。
(3) 文件索引与分区分桶
为了避免全表扫描,Paimon 提供了一套丰富的索引机制,它们像书的目录一样,帮助读取器快速定位到可能包含目标数据的区域,从而实现"数据跳过"(Data Skipping)。
<1> 分区分桶
- 分区 :最基础也是最有效的数据裁剪方式。通过在DDL时指定分区键(如
PARTITIONED BY (dt, hour)),数据在物理上会被组织在不同的目录中。查询时如果WHERE条件中包含对分区键的过滤,Paimon 会只扫描相关的分区目录,跳过大量无关数据。 - 桶 (Bucket) :在分区内部,Paimon 通过桶对数据进行进一步的物理组织。数据根据主键或指定分桶键的哈希值被分配到固定的桶中 。这保证了具有相同主键的记录总是位于同一个桶内 。这个特性对于读取和写入都至关重要:
- 读取:当进行点查询或 Lookup Join 时,Paimon 可以根据主键的哈希值直接定位到唯一的桶,极大地缩小了扫描范围。
- 写入:不同主键桶也不同,并发写入无影响;同一主键,compaction可以解决冲突问题。
<2> 文件元数据统计(Min/Max Stats)
在生成每个数据文件(Parquet/ORC)时,Paimon 会计算文件中每一列的最大值和最小值,并将这些统计信息存储在清单(Manifest)文件中。当读取器接收到一个带 WHERE 条件的查询时(如 WHERE value > 100),它会首先检查清单文件。如果一个文件的 value 列的 max 值都小于 100,那么这个文件就可以被安全地整个跳过,无需任何 I/O。这种机制对于范围查询尤其有效。
通过设置 'table.metadata.stats-mode' = 'full',让 Paimon 为每个文件收集完整的列统计信息。这使得基于 Min/Max 的文件过滤效果最大化。
<3> 索引
- 布隆过滤器(Bloom Filter) :
file-index.bloom-filter.columns,一种空间效率极高的数据结构,适用于高基数列的等值查询 (如user_id = 'xxx'),用于快速判断一个元素是否可能存在于一个集合中。Paimon 可以为指定列创建布隆过滤器,在查询时,如果查询条件中的值在布隆过滤器中不存在,就可以直接跳过整个数据文件,避免无效的 I/O - 位图索引(Bitmap Index) :
file-index.bitmap.columns,适用于基数(Cardinality)较低的列。它为每个唯一的列值创建一个位图,记录该值出现在哪些行。位图索引在处理等值查询和IN子句时非常高效。 - 范围位图索引(Range Bitmap Index) :对位图索引的扩展,适用于数值范围查询 (如
>、<)。 - 位切片索引 (Bit-Sliced Index, BSI) :适用于数值范围查询,是对位图索引的扩展,能高效处理范围过滤(它是范围查询的专用索引)。
<4> 结论
索引的协同工作 :Paimon 的读取优化是一个层层过滤的过程。首先通过分区 过滤掉大量目录,然后在分区内通过桶 定位到更小的文件集。接着,利用清单中的 Min/Max 统计信息 跳过不符合范围的数据文件。最后,对于剩下的文件,再利用文件索引(Bloom/Bitmap) 进行最后一轮的精细过滤。只有通过了所有这些筛选的数据文件,才会被真正地打开和读取。
(4) 内存管理与缓存策略
Paimon 在读写路径上都设计了精细的内存管理和缓存机制,以减少对磁盘 I/O 的依赖。
- 写路径 :核心是
WriteBuffer的管理。通过write-buffer-size和write-buffer-spillable等参数,可以平衡内存使用和写入性能。 - 读路径 :Paimon 实现了多级缓存机制来加速数据读取。
- 块缓存(Block Cache) :将从数据文件中读取的数据块(Block)缓存在内存中。Paimon 的
CacheManager负责管理这些缓存页,并采用 LRU(Least Recently Used)等淘汰策略。 - 内存切片(MemorySlice) :为了避免不必要的内存拷贝,Paimon 广泛使用
MemorySlice技术,它提供了对底层内存(如byte[]或ByteBuffer)的零拷贝视图。这在数据序列化/反序列化和比较操作中大大提升了效率。 - 文件索引缓存:如布隆过滤器(Bloom Filter)等文件索引也会被缓存在内存中,以加速文件过滤的过程。
- 块缓存(Block Cache) :将从数据文件中读取的数据块(Block)缓存在内存中。Paimon 的
4.总结
(1) 批式查询的优化
- 合理涉及分区:按天\小时分区,分区剪裁过滤大部分数据
- 启用元数据统计 :配置
'table.metadata.stats-mode' = 'full',这使得基于Min/Max的文件过滤效果最大化。 - 针对仅追加表,采用compact的时候排序 :
CALL sys.compact('my_db.my_table', 'Z-ORDER', 'col1,col2'),Z-Order排序优化多维数据的空间,加速查询 - 开启文件索引 :=用
bloom-filter;in用bitmap;范围用BSI - 调整并行度和读批大小 :
scan.parallelism调并行度,read.batch-size调批读大小
(2) 流式查询的优化
- 合理设置Checkpoint :
execution.checkpointing.interval设置cp间隔,短了,开销大;大了,延迟高execution.checkpointing.max-concurrent-checkpoints:并发检查点数量,通常都是1,高了,消耗多,但可以适当提升cp能力
- 针对需要关联维度的Lookup Join :Paimon优化