湖仓进阶优化思路---持续更新

进阶优化

1.写入性能

Paimon的写入性能与检查点密切相关,因此需要更大的写入吞吐量:

Ø 增加检查点间隔,或者仅使用批处理模式。

Ø 增加写入缓冲区大小。

Ø 启用写缓冲区溢出。

Ø 如果您使用固定存储桶模式,请重新调整存储桶数量。

(1) 并行度

建议sink的并行度小于等于bucket的数量,最好相等。

|选项 |必需的 |默认 |类型 |描述| |-|-|-|-|-| |sink.parallelism|No|(none)|Integer|定义sink的并行度。默认情况下,并行度由框架使用上游链式运算符的相同并行度来确定。|

(2) compaction

当Sorted Run数量较少时,Paimon writer 将在单独的线程中异步执行压缩,因此记录可以连续写入表中。然而,为了避免Sorted Runs的无限增长,当Sorted Run的数量达到阈值时,writer将不得不暂停写入。下表属性确定阈值。

|选项 |必需的 |默认 |类型 |描述| |-|-|-|-|-| |num-sorted-run.stop-trigger|No|(none)|Integer|触发停止写入的Sorted Runs次数,默认值为 'num-sorted-run.compaction-trigger' + 1。|

当 num-sorted-run.stop-trigger 变大时,写入停顿将变得不那么频繁,从而提高写入性能。但是,如果该值变得太大,则查询表时将需要更多内存和 CPU 时间。如果您担心内存 OOM,请配置sort-spill-threshold。它的值取决于你的内存大小。

(3) 优先考虑写入吞吐量

如果希望某种模式具有最大写入吞吐量,则可以缓慢而不是匆忙地进行Compaction。可以对表使用以下策略

Plain 复制代码

此配置将在写入高峰期生成更多文件,并在写入低谷期逐渐合并到最佳读取性能。

触发Compaction的Sorted Run数

Paimon使用LSM树,支持大量更新。 LSM 在多次Sorted Runs中组织文件。从 LSM 树查询记录时,必须组合所有Sorted Runs以生成所有记录的完整视图。

过多的Sorted Run会导致查询性能不佳。为了将Sorted Run的数量保持在合理的范围内,Paimon writers 将自动执行Compaction。下表属性确定触发Compaction的最小Sorted Run数。

选项 必需的 默认 类型 描述
num-sorted-run.compaction-trigger No 5 Integer 触发Compaction的Sorted Run数。包括 0 级文件(一个文件一级排序运行)和高级运行(一个一级排序运行)。

(4) 触发Compaction的Sorted Run数

Paimon使用LSM树,支持大量更新。 LSM 在多次Sorted Runs中组织文件。从 LSM 树查询记录时,必须组合所有Sorted Runs以生成所有记录的完整视图。

过多的Sorted Run会导致查询性能不佳。为了将Sorted Run的数量保持在合理的范围内,Paimon writers 将自动执行Compaction。下表属性确定触发Compaction的最小Sorted Run数。

选项 必需的 默认 类型 描述
num-sorted-run.compaction-trigger No 5 Integer 触发Compaction的Sorted Run数。包括 0 级文件(一个文件一级排序运行)和高级运行(一个一级排序运行)。

(5) 写入初始化

在write初始化时,bucket的writer需要读取所有历史文件。如果这里出现瓶颈(例如同时写入大量分区),可以使用write-manifest-cache缓存读取的manifest数据,以加速初始化。

(6) 内存

Paimon writer中主要占用内存的地方有3个:

Ø Writer的内存缓冲区,由单个任务的所有Writer共享和抢占。该内存值可以通过 write-buffer-size 表属性进行调整。

Ø 合并多个Sorted Run以进行Compaction时会消耗内存。可以通过 num-sorted-run.compaction-trigger 选项进行调整,以更改要合并的Sorted Run的数量。

Ø 如果行非常大,在进行Compaction时一次读取太多行数据可能会消耗大量内存。减少 read.batch-size 选项可以减轻这种情况的影响。

Ø 写入列式(ORC、Parquet等)文件所消耗的内存,暂时不可调。--这种只能调大flink的内存了

(7) change log优化

原理 : 当全列数据没有发生变更时(只是ts|seq变更了),可以避免产生变更日志的功能,这样能避免产生无效的增量日志,进而优化下游的计算效率。

比如 : F1-4 字段是我们真实需要检测变更的字段,它变更并不频繁,真正变更频繁的可能是 TS 字段或者 Seq 字段,只要对这些字段配置忽略检测,在真正生成变更日志时,就不会因这些字段的变化而生成新的变更日志了,这对下游无效计算的优化效果是非常明显的。

使用 : 建表时,为某些字段指定changelog-producer.row-deduplicate

(注意:请增加 Flink 配置'execution.checkpointing.max-concurrent-checkpoint,这对性能非常重要)。

2.读取性能

(1) Full Compaction(相当于大合并)

配置"full-compaction.delta-commits"在Flink写入中定期执行full-compaction。并且可以确保在写入结束之前分区被完全Compaction。

注意:Paimon 默认处理小文件并提供良好的读取性能。请不要在没有任何要求的情况下配置此Full Compaction选项,因为它会对性能产生重大影响。

(2) 读主键表的优化

对于主键表来说,这是一种"MergeOnRead"技术。读取数据时,会合并多层LSM数据,并行数会受到桶数的限制。虽然Paimon的merge会高效,但是还是赶不上普通的AppendOnly表。

如果你想在某些场景下查询得足够快,但只能找到较旧的数据,你可以:

Ø 配置full-compaction.delta-commits,写入数据时(目前只有Flink)会定期进行full Compaction。

Ø 配置"scan.mode"为"compacted-full",读取数据时,选择full-compaction的快照。读取性能良好。

这个读取模式会读取最后一次full compaction的数据

(3) 读仅追加表的优化

小文件会降低读取速度并影响 DFS 稳定性。默认情况下,当单个存储桶中的小文件超过"compaction.max.file-num"(默认50个)时,就会触发compaction。

但是当有多个桶时,就会产生很多小文件。

您可以使用full-compaction来减少小文件。full-compaction将消除大多数小文件。

(4) 格式

Paimon 对 parquet 读取进行了一些查询优化,因此 parquet 会比 orc 稍快一些。

3.多Writer并发写入

Paimon的快照管理支持向多个writer写入。

默认情况下,Paimon支持对不同分区的并发写入。推荐的方式是streaming job将记录写入Paimon的最新分区;同时批处理作业(覆盖)将记录写入历史分区。

**注意:**如果需要多个Writer写到同一个分区,事情就会变得有点复杂。例如,不想使用 UNION ALL,那就需要有多个流作业来写入"partial-update"表。参考如下的"Dedicated Compaction Job"。

以前的双流关联,都是流1的数据存入状态,流2的数据存入状态,等待能关联的数据,但这就不得不提到状态的TTL,如果其中一个流中状态的数据过时了,那么就关联不上,会有准确性问题。因此,这个partial-update是现在主流的处理方案,例如下图

假设流1和流2的数据不是同时到达的,那么partial-update,会先将先来的数据插入到表中,然后缺失的字段给null值,等流2的数据到达,会去更新缺失的字段。

如果1,a和1,b同时到达,会产生争抢问题,有可能1,a,null把b给覆盖了;也有可能1,null,b把a覆盖了,那怎么办呢?具体看底层

(1) Dedicated Compaction Job

默认情况下,Paimon writer 在写入记录时会根据需要执行Compaction。这对于大多数用例来说已经足够了,但有两个缺点:

Ø 这可能会导致写入吞吐量不稳定,因为执行压缩时吞吐量可能会暂时下降。

Ø Compaction会将某些数据文件标记为"已删除"(并未真正删除)。如果多个writer标记同一个文件,则在提交更改时会发生冲突。 Paimon 会自动解决冲突,但这可能会导致作业重新启动。

为了避免这些缺点,用户还可以选择在writer中不做compaction,而是运行专门的作业来进行Compaction。由于Compaction仅由专用作业执行,因此writer可以连续写入记录而无需暂停,并且不会发生冲突。

选项 必需的 默认 类型 描述
write-only No false Boolean 如果设置为 true,将跳过Compaction和快照过期。此选项与独立Compaction一起使用。

Flink SQL目前不支持compaction相关的语句,所以我们必须通过flink run来提交compaction作业。

这个作业即使没数据进来,也不会退出,会一直执行

css 复制代码
Plain Text /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。

(2) 冲突

Paimon 支持多个并发写入作业的乐观并发。

每个作业都按照自己的速度写入数据,并通过应用增量 files (删除或添加文件)。

此处可能有两种类型的提交失败:

  1. 快照冲突:快照 ID 已被抢占,表已从另一个作业生成新快照。好,让我们再次提交。

  2. 文件冲突:此作业要删除的文件已被其他作业删除。此时,作业只能失败。(对于流式处理作业,它将失败并重启,有意故障转移一次)

(1)快照冲突

Paimon 的快照 ID 是唯一的,所以只要 Job 将自己的快照文件写入文件系统,就认为是成功的。

Paimon 使用文件系统的重命名机制来提交快照,可以解决这个问题,这对 HDFS 来说是安全的,因为它可以确保 事务重命名和原子重命名。

但对于 OSS 和 S3 等对象存储,它们没有原子语义。我们需要配置 Hive 或 jdbc metastore 和 enable 选项。否则,可能会丢失快照。'RENAME''lock.enabled'

(2)文件冲突

当 Paimon 提交文件删除(只是逻辑删除)时,它会检查与最新快照的冲突。 如果存在冲突(这意味着文件已被逻辑删除),则它不能再在此提交节点上继续。 因此,它只能有意触发故障转移后重新启动,并且 Job 将从文件系统中检索最新状态 希望解决这个冲突。

一句话:如果当前版本的某个文件已经被压缩了,然后我现在想删除这个文件,系统会发现这个文件在压缩的时候就被执行了删除(逻辑上),因此他会发生冲突,他认为已经删除的文件是不可以再次删除的。

Paimon 会确保这里没有数据丢失或重复,但如果两个 streaming job 同时写入并且产生冲突,你会看到它们不断重启,这不是一件好事。

冲突的本质在于删除文件(逻辑上),而删除文件源于压缩,所以只要 我们关闭写入作业的压缩(也就是将 'write-only' 设置为 true)并额外启动一个单独的作业来执行压缩工作。 一切都很好。

4.管理表

(1) 管理快照

<1> 快照过期

Paimon Writer每次提交都会生成一个或两个快照。每个快照可能会添加一些新的数据文件或将一些旧的数据文件标记为已删除。然而,标记的数据文件并没有真正被删除,因为Paimon还支持时间旅行到更早的快照。它们仅在快照过期时被删除。

目前,Paimon Writer在提交新更改时会自动执行过期操作。通过使旧快照过期,可以删除不再使用的旧数据文件和元数据文件,以释放磁盘空间。

设置以下表属性:

选项 必需的 默认 类型 描述
snapshot.time-retained No 1 h Duration 已完成快照的最长时间保留。
snapshot.num-retained.min No 10 Integer 要保留的已完成快照的最小数量。
snapshot.num-retained.max No Integer.MAX_VALUE Integer 要保留的已完成快照的最大数量。

注意,保留时间太短或保留数量太少可能会导致如下问题:

Ø 批量查询找不到该文件。例如,表比较大,批量查询需要10分钟才能读取,但是10分钟前的快照过期了,此时批量查询会读取到已删除的快照。

Ø 表文件上的流式读取作业(没有外部日志系统)无法重新启动。当作业重新启动时,它记录的快照可能已过期。 (可以使用Consumer Id来保护快照过期的小保留时间内的流式读取)。

<2> 回滚快照
css 复制代码
Shell /bin/flink run \ /path/to/paimon-flink-action-0.5-SNAPSHOT.jar \ rollback-to \ --warehouse \ --database \ --table \ --snapshot \ [--catalog-conf [--catalog-conf ...]]

(2) 管理分区

创建分区表时可以设置partition.expiration-time。 Paimon会定期检查分区的状态,并根据时间删除过期的分区。

判断分区是否过期:将分区中提取的时间与当前时间进行比较,看生存时间是否超过partition.expiration-time。比如:

sql 复制代码
Plain Text CREATE TABLE T (...) PARTITIONED BY (dt) WITH ( 'partition.expiration-time' = '7 d', 'partition.expiration-check-interval' = '1 d', 'partition.timestamp-formatter' = 'yyyyMMdd' );
选项 默认 类型 描述
partition.expiration-check-interval 1 h Duration 分区过期的检查间隔。
partition.expiration-time (none) Duration 分区的过期时间间隔。如果分区的生命周期超过此值,则该分区将过期。分区时间是从分区值中提取的。
partition.timestamp-formatter (none) String 用于格式化字符串时间戳的格式化程序。它可以与"partition.timestamp-pattern"一起使用来创建使用指定值的格式化程序。 Ø 默认格式化程序为"yyyy-MM-dd HH:mm:ss"和"yyyy-MM-dd"。 Ø 支持多个分区字段,例如" y e a r − year- year−month- d a y day dayhour:00:00"。 Ø 时间戳格式化程序与 Java 的 DateTimeFormatter 兼容。
partition.timestamp-pattern (none) String 可以指定一种模式来从分区获取时间戳。格式化程序模式由"partition.timestamp-formatter"定义。 Ø 默认情况下,从第一个字段读取。 Ø 如果分区中的时间戳是名为"dt"的单个字段,则可以使用" d t "。Ø如果它分布在年、月、日和小时的多个字段中,则可以使用" dt"。 Ø 如果它分布在年、月、日和小时的多个字段中,则可以使用" dt"。Ø如果它分布在年、月、日和小时的多个字段中,则可以使用"year- m o n t h − month- month−day h o u r : 00 : 00 "。Ø如果时间戳位于 d t 和 h o u r 字段中,则可以使用" hour:00:00"。 Ø 如果时间戳位于 dt 和 hour 字段中,则可以使用" hour:00:00"。Ø如果时间戳位于dt和hour字段中,则可以使用"dt $hour:00:00"。

(3) 管理小文件

小文件可能会导致:

Ø 稳定性问题:HDFS中小文件过多,NameNode会承受过大的压力。

Ø 成本问题:HDFS中的小文件会暂时使用最小1个Block的大小,例如128MB。

Ø 查询效率:小文件过多查询效率会受到影响。

使用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。

5.缩放Bucket

1)说明

由于总桶数对性能影响很大,Paimon 允许用户通过 ALTER TABLE 命令调整桶数,并通过 INSERT OVERWRITE 重新组织数据布局,而无需重新创建表/分区。当执行覆盖作业时,框架会自动扫描旧桶号的数据,并根据当前桶号对记录进行哈希处理。

less 复制代码
SQL -- 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 ,并显示如下类似异常:

sql 复制代码
SQL 对于分区表,不同的分区可以有不同的桶号。例如: ALTER TABLE my*table SET ('bucket' = '4'); INSERT OVERWRITE my*table PARTITION (dt = '2022-01-01') --这样2022-01-01就是4个桶 SELECT * FROM ...;

ALTER TABLE my*table SET ('bucket' = '8'); INSERT OVERWRITE my*table PARTITION (dt = '2022-01-02') --其他分区都是8个桶 SELECT * FROM ...;

在覆盖期间,确保没有其他作业写入同一表/分区。

注意:对于启用日志系统的表(例如Kafka),请重新调整主题的分区以保持一致性。

重新缩放存储桶有助于处理吞吐量的突然峰值。假设有一个每日流式ETL任务来同步交易数据。该表的DDL和管道如下所示。

2)官方示例:

bash 复制代码
SQL 如下是正在跑的一个作业: -- 建表 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';

6.Look up Join的缺点及优化思路

Paimon 支持作为 Lookup 的 source,支持将数据缓存在本地磁盘、按需从表存储中加载数据,还支持将数据缓存在 RocksDB 中。然而,它也有一些不足。

(1) Look up的缓存策略

在不同的 Look Join 场景中有部分缓存和全部缓存两种策略,可以提供开发者更好的性能调优选项。

火山引擎为paimon提供了异步同步cache的方法通过在sql中配置 /*+ OPTIONS('lookup.async'='true') */

<1> 部分缓存

部分缓存模式是在 Join 的过程中按需缓存命中的数据文件,如图所示:

通过建表语句中 WITH 参数 'lookup.cache'='auto' 来开启,必须满足以下两种情况:

  • 关联表为固定分桶模式的主键表

  • 关联表表的主键和 Join Key 一致

此时会自动选择部分缓存(Partial Cache)模式。而不满足这两个条件时会选择全部缓存(Full Cache)模式。

部分缓存能够利用 LSM-Tree 的主键有序性,实现维表缓存数据按需加载,避免全量数据加载,任务初始化更快。

注意:仅支持主键表的主键关联场景,如果关联 Key 不是主键,则无法使用。

<2> 全部缓存

全部缓存会批量将 Paimon 表数据全部 Load 到 RocksDB 中,这样能够在关联 Key 非主键的情况下,能够 Lookup 成功:

通过参数 'lookup.cache'='full' 来开启 全部缓存模式,支持主键表的主键关联和非主键关联两种模式,也支持非主键表的关联。但是劣势是初始化加载时间较长,冷启动现象明显。

(2) Look up 的Bucket Shuffle Join

火山引擎的 Paimon 版本为社区提供了 Bucket Shuffle 功能,极大地提升了 Lookup Join 大规模维表时候的性能。Bucket Shuffle 的原理如下:

没有开启 Bucket Shuffle 功能

开启 Bucket Shuffle 功能

可以看出,在开启 Bucket Shuffle 的 Lookup Join 过程中,主数据会根据 Join Key 进行 Hash 分组处理,这样在每个分组中只要缓存对应 Bucket 数据,大大减少了内存用量,减少了缓存淘汰的概率。可以支持更大规模的维表。开启方法如下,在 hint 中设置 'shuffle' = 'true'

bash 复制代码
SQL -- 使用 hint 选择最新分区 SELECT /*+ LOOKUP('table'='orders', 'shuffle'='true') */ gen.product*id, gen.product*name, gen.product*category*id, gen.product*order*id, gen.product*status, gen.create*date, orders.order*name, orders.order*customer*id, order*status FROM datagen*source AS gen JOIN `paimon_dim`.`dim_test`.`dim_orders` FOR SYSTEM*TIME AS OF gen.create*date AS orders ON gen.product*order*id = orders.order*id;

适用场景:

  • 固定分桶表:主键表和 Append 表(非主键表)都支持

  • Join Key 要包含所有的 Bucket Key

(3) 本地磁盘性能瓶颈

当数据存储在本地磁盘时,每次 Lookup 操作都会触发磁盘的随机读。这对于使用 HDD 磁盘的计算节点来说,Lookup Join 的性能容易受到限制,因为本地磁盘性能可能成为瓶颈。

**解决方案:用更好的远端磁盘去代替本地磁盘,**把这部分磁盘数据存储到专用的远程的 SSD 上来进行优化,SSD 的性能会比 HDD 要好太多,并且把磁盘的负载转移到远程的 SSD 上之后,本地的任务也不容易受节点上其他任务的影响。

(4) 加载全量数据

当数据更新时,尤其是在进行一次全面的 Compaction 之后,节点在启动时需要加载全部数据,导致启动时间过长,这显然不够高效。

解决方案:

<1> 合理设计Bucket

**合理设计bucket:**通过对数据流采用与 Lookup 表相同的 Bucket 策略,可以有效避免在每个并发操作中加载全表数据的问题。优化后的系统能够自动进行分区,这样每个节点只需加载特定 Bucket 的数据,大幅减少了单个 Lookup 节点的数据加载量。

<2> 用Support Lookup Custom Shuffle

针对无法同步bucket策略的情况,这个接口的本质是允许connector 为维表实现指定事实表的数据 shuffle 方式。有了这个接口后 Paimon 表的维表就能够执行一些特定的操作。

  1. **Fixed Bucket :**在作业定义时,而非在 Paimon 表定义时,就已经确定了 Bucket 的数量。本质上这个计算过程是对 Bucket Key 取哈希值,然后再对 Bucket 的总数取模,从而确定数据具体属于哪个 Bucket 。实际上只需要让事实表也按照同样的方式进行 shuffle;例如:在事实表中可以将 K 1 和 K 2 分配到 Lookup 算子上。这个 Lookup 算子知道,它只需要读取 Bucket 1 的数据,并且只需将 Bucket 1 的数据存储在本地即可
  1. **Dynamic Bucket:**Paimon 表对于数据的 Bucket 分配是动态的。也就是说随着 Key 的增多,它可能会增加一些 Bucket 。此时对于一条数据来说,无法像之前那样通过一条简单的公式来计算出它属于哪个 Bucket 。可以通过 Custom Shuffle 接口来指定其 Sort of 的方式。这里的分配方式是指根据 Join Key 取一个哈希值,然后在取模时根据下游 Subtask 的数量,即 Lookup Join 的 Subtask 数量来进行。这时每个 Lookup 算子,或者说每个 Lookup 的并发实例,在读取维表时就会知道它可能会接收到哪些与事实表相关的数据。因此它就可以对其存储的缓存进行一些裁剪。比如说,虽然事实表仍然是按照如 K 1 、 K 3 这样的 Key 发送给上游的并发实例,但这些并发实例在读取数据时还是需要全量读取。但是当数据存储到本地时可以进行过滤,只存储与 K 1 和 K 3 相关的数据。因为他知道事实表的分配算法策略是他指定的,所以他可以只存储与 K 1 、 K 3 相关的维表数据。尽管在读取时仍然需要访问全量的数据,但实际上他只需要在本地保存一部分维表数据。

(5) Hot Key问题

当Lookup Join存储了Hot Key的数据,往往会导致性能瓶颈

解决方案:用Skew Join

bash 复制代码
SQL SELECT /*+ SKEW_JOIN('表别名', '倾斜列', '热点Key列表', 'bucket-num=N') */ ... FROM ... JOIN ...;

当在 SQL 中使用 /*+ SKEW_JOIN('c', 'id') */ 时(不指定热点 Key 列表和 bucket-num), Flink 会自适应识别热点,自动打散: 实时监控数据:在作业运行时动态统计 id 列的 Key 分布频率。 自动识别热点:当某个 Key 的处理量超过阈值(如 skew-join.threshold,默认 10000 行)时,标记为热点。 触发打散策略:对热点 Key 自动执行复制分发,无需手动指定具体 Key。

这个优化策略的本质是,如果作业对 Per Key 的顺序没有特定要求,那么就可以启用它。在这种情况下 Paimon 通过 Lookup 自定义分配,会将同一个 Key 的数据随机分发到 N 个并发实例上,其中 N 是用户可以自行指定的。比如说在这个例子中,设定 N 等于 2 ,事实表中的 K 1 可能会被分发到第一个和第二个并发实例上,从而尽可能地将一个 Hot Key 打散。由于需要进行一个类似于复制的操作,因此第一个和第二个 Lookup 并发算子都需要额外读取一个 Bucket 。这实际上是一个 Trade-off 。如果将 N 设置得很大,那么数据被打散得会更加平均,但每个算子需要读取的数据量也会相应增加。

(6) 支持BloomFilter索引

Lookup 上的另一个优化是支持本地 Lookup file 的 BloomFilter 索引。在 RockDB 查找的过程中,通过二分查找定位到文件后,利用 BloomFilter 来判断对应的key是否在文件中。如果判断存在,才会真正读取。而在 Paimon 中,由于也是多层的 LSM Tree,也可以利用这样的优化加速查找。主要场景有两个:一是 Lookup Join, 另一个就是 Lookup Change Producer。

为什么不直接利用远程文件的 BloomFilter 索引的主要原因有两个:

第一,远程文件的 BloomFilter 是一个对应文件格式,内部 SDK 写入,一般是由 BloomFilter 下推的方式判断,并无直接的判断方式,相对不可控 。

第二,远程文件判断需要不断读取文件的 File footer 的。频繁的 Seek 操作对 HDD 磁盘不友好。在最新的 Paimon 0.8 版本中,也开始在 Append 表上支持 File Index,也支持 File BloomFilter 索引,后续可以基于这些文件索引进一步加速文件查找的过程,以后把文件下载到本地这个过程可能都不需要了。因此在读取远程文件、本地 Build 时构建 BloomFilter 索引,后续二分查找命中此文件时,先进行 BloomFilter 索引判断,命中后再进行查找,从而节约了本地查找文件的过程。

(7) 动态分区的优化,提升lookupjoin的性能

问题背景 :传统数仓中,分区表(如按天分区)的 Lookup Join 通常只需关联最新分区数据。Paimon 通过 max_pt() 特性自动识别最新分区,避免全表扫描。


1. 动态分区表定义
vbnet 复制代码
SQL CREATE TABLE customers ( id INT, name STRING, country STRING, zip STRING, dt STRING, -- 分区字段(如按天分区) PRIMARY KEY (id, dt) NOT ENFORCED ) PARTITIONED BY (dt);
2. Lookup Join 动态分区查询
sql 复制代码
SQL SELECT o.order*id, o.total, c.country, c.zip FROM orders AS o JOIN customers /*+ OPTIONS( 'lookup.dynamic-partition'='max*pt()', -- 自动选择最新分区 'lookup.dynamic-partition.refresh-interval'='1 h' -- 每1小时刷新最新分区 ) */ FOR SYSTEM*TIME AS OF o.proc*time AS c ON o.customer_id = c.id;
  • 工作原理

    • 自动刷新 :每隔 refresh-interval 时间,查询服务会检测最新分区(如 dt='2023-10-01')。

    • 分区剪枝:Lookup Join 仅查询最新分区的数据,减少扫描数据量。

  • 适用场景: 维表按时间分区,且仅需关联最新分区数据(如 T+1 更新的用户画像表)。

(8) 高并发LookupJoin,频繁查paimon表性能瓶颈,热点数据缓存困难

问题背景 :高并发 Lookup Join 场景下,频繁查询 Paimon 表可能导致性能瓶颈。通过启动 常驻 Flink 流作业 作为查询服务,缓存热数据并加速查询。


1. 启动查询服务
arduino 复制代码
SQL -- 启动并行度为4的查询服务 CALL sys.query*service('my*db.customers', 4);
2. Lookup Join 自动优化

当 Query Service 运行时:

  • 优先查询服务:Flink Lookup Join 会优先从 Query Service 的内存缓存中获取数据。

  • 缓存热数据:Query Service 会预加载 Paimon 表数据到内存,并定期更新(类似物化视图)。

3. 性能对比
场景 直接查询 Paimon 表 使用 Query Service
查询延迟 10~100 ms 1~5 ms
吞吐量 1k~10k QPS 10k~100k QPS
适用场景 低并发、冷数据 高并发、热数据

(9) 各种LookupJoin策略的应用场景

1. 高吞吐低延迟场景
  • 异步查询 + Query Service
sql 复制代码
SQL CALL sys.query*service('my*db.customers', 8);

SELECT /*+ LOOKUP('table'='c', 'output-mode'='allow*unordered') */ o. *, c.* FROM orders o JOIN customers /*+ OPTIONS('lookup.async'='true') */ FOR SYSTEM*TIME AS OF o.proc*time AS c ON o.customer*id = c.id;
2. 维表延迟更新场景
  • 异步重试 + Audit Log
sql 复制代码
SQL CREATE VIEW customers*append AS SELECT * FROM customers$audit*log WHERE rowkind = '+I';

SELECT /*+ LOOKUP('retry-strategy'='fixed*delay', 'max-attempts'='300') */ o. *, c.* FROM orders o JOIN customers*append /*+ OPTIONS('lookup.async'='true') */ FOR SYSTEM*TIME AS OF o.proc*time AS c ON o.customer_id = c.id;
3. 动态分区场景
  • 动态分区 + 定期刷新
sql 复制代码
SQL SELECT o.order*id, c.country FROM orders o JOIN customers /*+ OPTIONS( 'lookup.dynamic-partition'='max*pt()', 'lookup.dynamic-partition.refresh-interval'='5 min' ) */ FOR SYSTEM*TIME AS OF o.proc*time AS c ON o.customer_id = c.id; ```

7.starrocks+paimon的优点

(1) 冷热分离

热数据存在OLAP(如starroccks)的物化视图中,冷数据存在远端文件系统(如hdfs、oss),如图 Paimon + StarRocks 冷热分离的例子,如果构建了这样一个冷热分离的 MV 表,当查询到这张表的时候,会自动选择在 StarRocks 上分布的这个热数据和在 Paimon 分布的冷数据。然后对查询结果合并,并返回给用户。

(2) 存算分离

  • 可以采用paimon存数据,然后starrocks建立外部表去访问paimon中的数据,进行计算
  • 可以指定paimon的catalog为starrcoks,在对接 Paimon 外表时,只需要在 StarRocks 上执行下面这条 Create External Catalog 语句,对 Type 指定为 Paimon,填写上对应的路径之后就可以直接查询 Paimon 中的数据了。---可以实现联邦查询

(3) 读取PK表性能提升--Deletion Vector

采用Deletion Vector,减少读取时候merge的文件,Deletion Vector则是用 Bitmap 来标记,我数据文件中的哪些列是被 Upsert 掉了需要删除的,也就不需要合并,只要跳过就可以了。

(4) 物化视图改写执行计划(透明加速)

我创建了一个 t1 t2 join 的物化视图,之后我执行了一个t1 t2 t3三个表 join 的查询,查的是原表。这种情况下,StarRocks 发现,这个查询中有一个 join 命中了物化视图,然后这一部分的查询就会自动被改写到物化视图上,这个过程用户是不感知的,也就是所谓透明加速。(简单来说:t1和t2的join可以从物化视图拿取,然后在这些拿取的数据上,再去paimon中查询t3的数据进行join)

(5) 统一catalog

有人可能会觉得,我所有湖格式的外表都在一个统一的 Metastore 里,我要给每种湖格式都建一个 catalog,我觉得这还是太麻烦了,怎么办?StarRocks提供了 Unified Catalog 这个功能,专门就针对这种情况,这是一个特殊的外表 catalog,里面什么表都有,只要元数据一样,就都能查。Paimon也是支持了 unified catalog

相关推荐
ServBay1 小时前
Grok Bot 还是 OpenClaw?聊聊真正能替人打杂的 AI 智能体
后端·aigc·grok
掘金挖土1 小时前
前端手摸手跑路之 AI 应用开发(五)
前端·后端
弈栈录1 小时前
LangGraph 入门:用状态机设计可靠的 Agent 工作流
后端·程序员
LEE3 小时前
前端转型全栈 00:AI 时代该学哪些,不该学哪些
前端·后端
Go_error3 小时前
上下文取消链:摧毁我们支付系统的 bug
后端·go
无限压榨切图仔3 小时前
一个优惠券需求改了三端,我才明白全栈不是多学一门语言
前端·后端
CRZZX3 小时前
阶段 0.1:为 AI Agent 项目建立敏感配置治理与安全基线
后端
Zane19943 小时前
用了Optional,为什么NPE还是防不住?
java·后端
yunwei373 小时前
eBPF 示例教程:实现 `scx_nest` 调度器
linux·后端·性能优化
薛定谔的算法3 小时前
从 NestJS 到 Spring Boot:一次完整的 Node.js → Java 后端迁移实战
后端