☰
Apache Paimon 湖仓一体架构深度拆解:存储、表格式与计算引擎如何协作
2026/10/22 13:38:11 网站建设 项目流程

Apache Paimon 的湖仓一体架构图,最近在数据工程圈里出现的频率越来越高。很多人把图画得像科技展板一样漂亮,但问到“为什么这个框放在这里,那条线为什么会存在”时,很少有人能讲清楚。我自己过去一年在某数据团队围绕 Paimon 搭架构,踩了不少坑,也想趁这个机会把一张典型的架构图彻底拆开:底层存储、表格式服务、计算引擎、数据管道和下游应用之间到底是怎么协作的。看完你会明白,这张图不是技术栈的简单拼凑,每一层都在解决一个明确问题,也都能对应到具体的参数配置和 SQL 操作。

1. 为什么是 Paimon,而不是其他表格式

1.1 数仓与数据湖的边界痛点

传统数仓做报表和 BI 很好用,但扩展成本高,数据模型僵化,想接半结构化数据、想支持机器学习特征探索,都很痛苦。数据湖反过来,存储成本低、格式开放,可早期的数据湖基本没有事务能力,不支持主键更新,多份任务同时写一张表很容易读到中间状态。更麻烦的是流批分离:实时链路写一套消息或状态存储,离线链路写一套 Hive 分区表,两套数据口径经常对不上。

湖仓一体的核心诉求,就是在类似数据湖的低成本存储上,提供数仓级的一致性、更新能力和查询性能。Apache Paimon 正是奔着这个诉求来的。它把文件格式、元数据管理和快照机制做成一个统一的“表格式服务”,让同一份数据既能支撑流式写入和流式读取,也能支撑批量覆盖和普通 SQL 查询。

实际上 Paimon 并不是一套需要单独部署的存储服务,更像是一个标准化的表格实现。你建一张 Paimon 表,数据最终落在对象存储或 HDFS 上,但表的 schema、分桶、快照、变更日志都由 Paimon 管理。这样一来,Flink、Spark、Hive 都只是这张表的客户端,大家读写同一份文件,却各取所需。

1.2 Paimon 和其他表格式的取舍

很多人会拿 Paimon 和 Iceberg、Hudi 做对比。其实架构选型不需要追求“最强”,只需要在具体场景下最顺手。Paimon 比较有特点的地方有三个:一是底层用 LSM 结构组织主键表,高频更新和删除效率更稳定;二是天生和 Flink 深度绑定,流式读写、CDC 入湖、changelog 流读都做得非常顺;三是文件合并、快照过期、小文件治理这些日常运维机制,内置得比较完整。

能力维度PaimonIcebergHudi
主键表更新内置 LSM 合并需要借助 MOR 或外部处理内置,但配置偏多
流式读取变更日志原生支持不擅长支持,但依赖表类型
Flink 集成深度非常紧密一般可用
小文件自动治理有 compaction 机制需要额外处理有,但运维复杂
学习成本中等中等偏高

所以在我画的架构图里,Paimon 不是“其中一个组件”,而是整个湖仓底座的位置。只要主键更新和流批同源是硬需求,Paimon 比其他格式更适合放在这个位置上。

2. 一张典型架构图的逐层拆解

2.1 存储层:数据落在哪里,文件怎么摆

从下往上看,架构图最底部永远是存储层。Paimon 支持本地文件系统、HDFS 和各类 S3 协议的对象存储,实际生产里大部分团队选对象存储,因为成本低、扩容简单。建一个 catalog 时指定的 warehouse 路径,就是这个湖仓的根目录。

在存储层上,Paimon 表的目录结构大致是:warehouse 下按库名、表名分目录,表目录下再按分区、bucket 分目录。每个 bucket 内部是一系列列式数据文件,比如 Parquet 或 ORC。快照清单文件(manifest)会记录每个 snapshot 对应哪些数据文件,以及文件的一些统计信息,比如分区范围、值域范围。

这个设计的好处是,读任务不需要扫描整个仓库,只要拿到某个 snapshot 的 manifest,就能只读取涉及的文件。架构图画存储层时,通常会在对象存储上方标一个“Paimon 表目录”的框,强调这里不是简单的文件堆放,而是有结构、有索引、有快照语义的。

2.2 表格式核心层:Catalog、快照、Manifest

架构图中间偏下会有一个比较大的框,写着“Paimon 表格式服务”。墙面上的听众往往忽略这个框,但它恰恰是最关键的部分。Paimon 的表能力由三部分支撑:Catalog、Snapshot、Manifest。

Catalog 负责库表元数据,可以是文件系统 Catalog,也可以放到 JDBC 里做统一管理。它会记录库表清单、表结构、表参数位置。Snapshot 是每次写入提交后产生的一个不可变视图,类似数据湖的时间旅行版本。读者只要指定 snapshot-id,就能读到某个历史时刻的数据,不会看到后续未提交的写入。

Manifest 是整个文件组织的地图。每次提交都会生成新的 manifest 列表,记录本次提交涉及的数据文件、变更类型、统计信息。查询引擎通过读取 manifest 来确定扫描范围。这一层是 Paimon 能提供 ACID、流读、增量消费的根基,也是架构图中“湖仓一体”四个字的重要支撑。

2.3 计算引擎层:Flink、Spark、Hive 各司其职

计算引擎层在架构图里通常是中间一整排。最常见的组合是 Flink 负责实时写入和流式读取,Spark 负责批量处理和离线分析,Hive 或 Presto/Trino 负责即席查询和报表。箭头通常分两类:一类是数据写入,一类是数据读取。

Flink 到 Paimon 的写入路径特别顺,因为 Paimon 的 Flink connector 很成熟。上游 CDC 数据、Kafka 消息、业务日志,都可以通过 Flink SQL 直接写入 Paimon 主键表。流读时,Flink 可以像读消息队列一样消费 Paimon 表的新增和变更记录。Spark 则更偏向于跑大规模批任务,比如全量回算历史数据、做复杂 join,或者执行 schema 变更和 compaction。Hive 在一些存量团队里保留着查询入口,Paimon 也支持 Hive 外表,避免一次性迁移所有查询。

理解这个分层的关键是:所有这些引擎都不是常驻服务,而是 Paimon 表的客户端。架构图上画的“三个引擎指向同一张表”,本质是为了说明流批读写可以复用同一份数据文件。

2.4 上游数据接入与下游应用

架构图的左右两侧分别画上游和下游。上游通常是业务库、消息队列和 CDC 工具。以订单场景为例:业务库的变更通过 CDC 接入 Kafka,Flink 再从 Kafka 消费并写入 Paimon。这个路径既支持全量初始化,也支持增量实时同步,数据延迟基本可以做到秒级。

下游的消费方就比较多样。最常见的是 OLAP 加速引擎和指标平台,因为 Paimon 虽然在湖存储上做了一层表格式优化,但要支撑亚秒级前端报表,还是有点吃力。另一种是特征平台,需要把主键表的最新状态变成一个可查询的特征快照,Paimon 的查询能力正好胜任。还有一种是数据服务层,通过统一 SQL 接口对外提供数据接口。

我在画图时还会强调,下游的 OLAP 引擎不是必选组件,而是按查询延迟需求决定的。如果团队只有离线报表,Spark 直接查 Paimon 就够了;要是前端有交互式大屏,加一层 OLAP 缓存会舒服很多。

2.5 横向支撑层:调度、权限、质量和监控

一张成熟的架构图,不能只有上下层,还必须有横向贯穿的支撑体系。调度平台负责周期性任务,比如定时合并小文件、刷新分区、清理过期快照。权限管理负责表级和库级访问控制,在实际生产里通常通过计算引擎的权限体系实现。数据质量模块用来做主键唯一性检查、空值率监控、产出时延告警。

这些在架构图上有时只是底部一条带子,但上线后你会发现,真正花时间维护的不是“写入链路”,而是这些横向服务。Paimon 的小文件数量、snapshot 总量、compaction 作业健康度,都需要纳入监控,否则表会越跑越差。

3. 架构图里不会明说,但你必须懂的核心设计

3.1 建表语句示例与表类型选型

看架构图只能知道模块关系,真正决定效果的是建表参数。我给一个典型的订单主键表做例子:

CREATE TABLE orders ( order_id BIGINT, user_id BIGINT, amount DECIMAL(10, 2), status STRING, ts TIMESTAMP(3), PRIMARY KEY (order_id) NOT ENFORCED ) WITH ( 'bucket' = '16', 'bucket-key' = 'order_id', 'file.format' = 'parquet', 'changelog-producer' = 'lookup', 'sequence.field' = 'ts', 'snapshot.time-retained' = '24 h' );

这里有几个参数非常关键。bucket决定数据分片数,直接影响写入并行度和文件粒度。bucket-key一般和主键保持一致,保证相同主键的更新落在同一个 bucket。file.format用 parquet 或 orc,列式存储适合 OLAP 查询。changelog-producer决定 Paimon 是否能为流读生成完整的变更记录,lookup是比较常见的选择。sequence.field则用来处理乱序数据,让较新的记录覆盖较旧的记录。

Paimon 表类型不只有主键表,还有聚合表和变更日志表。聚合表适合做实时指标预聚合,比如把点击流按维度汇总;变更日志表适合需要完整历史变更记录的场景,比如广播维表。选型时不要一上来就建主键表,先想清楚下游到底需要“最新状态”还是“每一条变更”。

表类型适用场景核心特点
主键表维表、事实表更新、CDC 同步按主键合并,支持更新删除
聚合表实时汇总、指标累计按聚合函数合并,不需要保留明细
变更日志表维表广播、事件回溯保留完整 insert/update/delete 流

3.2 bucket 数量怎么定

bucket 数量是架构设计里最容易被拍脑袋决定的参数。定少了,写入并发上不去,单个文件容易过大;定多了,会产生大量小文件,查询和 compaction 的压力都会增加。我的经验是,bucket 数量建议先按 Flink 写入并行度来配,比如写入并行度 16,bucket 可以设 16 或 32,让每个写入子任务尽量只写少数几个 bucket。

更朴素的判断标准是看单 bucket 的数据量。一个 bucket 数据量控制在 128MB 到 1GB 之间比较合理。如果日增数据量很大,可以适当增加 bucket,但要同步调高写并行度。这里要特别提醒,bucket-key 一旦上线后修改,通常需要重写整张表,不是改个参数就能解决。所以前期建表时,要基于业务 key 的分布和写入流量做保守设计。

3.3 分区、排序与文件裁剪

架构图里很少画出分区细节,但分区策略直接影响查询效果。Paimon 支持常规的分区表,最常用的是按日期或小时分区。分区字段粒度不能太细,比如一张订单表按小时分区,很容易一小时只写几十 MB,文件碎片就会很多。所以通常分区到天就够了,必要时再用小时分区。

Paimon 在处理数据时,manifest 会记录每个文件的统计信息,包括分区字段和主键范围。查询条件能命中分区裁剪时,读取的文件数量会大幅减少。为了让裁剪更精准,写入端尽量保证数据按分区有序进入,不要让同一个分区的数据分散在大量小文件里。排序和分桶的设计,本质都是为了让查询少扫描文件。

3.4 流批一体读写路径怎么实现

Paimon 的流批一体,体现在同一张表可以有完全不同的读取方式。批式查询就是普通 SQL:

SELECT user_id, SUM(amount) FROM orders WHERE dt = '2025-04-10' GROUP BY user_id;

流式读取则更像消费消息队列:

SELECT order_id, status, amount FROM orders /*+ OPTIONS('scan.mode' = 'latest') */;

指定scan.mode='latest'时,Flink 会从当前最新快照开始消费,之后产生的变更会持续输出。如果想从某个历史快照开始消费,可以指定scan.snapshot-id。这个能力让下游流应用、实时大屏和离线报表都能直接用同一张表,不再需要复制一份实时存储。

写入端也一样,Flink 可以连续插入,Spark 也可以批量覆盖。用户不需要关心底层是 LSM 合并还是文件追加,Paimon 会通过快照和 manifest 保证读写隔离。这就是架构图里“流批一体”这条标注的真正含义:一套文件系统,两种读写语义。

3.5 小文件与快照生命周期管理

生产环境里,Paimon 最需要关注的是文件膨胀和快照膨胀。每次写入提交都会产生新的数据文件和 manifest,如果任务频繁提交,小文件会快速堆积。Paimon 提供了 compaction 机制,可以在后台将小文件合并成大文件,也可以设置write-only模式让写入任务不主动做合并,由独立压缩作业来负责。

快照也不能无限保留。每个快照都会占用一部分目录和 manifest,历史快照过多会影响读取性能。一般建议设置snapshot.time-retained为 24 小时或 48 小时,snapshot.num-retained也可以给一个最低限制。不过这里的取舍是:如果下游有长时间运行的流式读取任务,快照过期太激进会导致任务报错读不到数据。所以快照保留时间和流读任务的重启周期要一起考虑。

4. 照着图落地:从零搭建一个可运行的最小架构

4.1 依赖准备与 Catalog 配置

理论讲完,还是要落到实际操作。我以 Flink + Paimon + Spark 这套组合为例,跑通一个最小湖仓。首先版本要选对,我当时用的是 Flink 1.17 和 Paimon 0.8,兼容性比较稳。把对应版本的 paimon-bundle 和 flink 依赖放进 Flink lib 目录后,启动 SQL Client。

在 SQL Client 里创建 Catalog:

CREATE CATALOG my_paimon WITH ( 'type' = 'paimon', 'warehouse' = 's3://my-bucket/warehouse', 'metastore' = 'filesystem' ); USE CATALOG my_paimon;

这段配置应该很好理解,warehouse 就是整个湖仓的根路径。metastore我选了filesystem,也就是把元数据以文件方式管理,适合中小规模快速验证。如果团队有统一元数据中心,也可以换成 JDBC Catalog,把库表元数据放到关系型库里。

然后开始建库建表:

CREATE DATABASE IF NOT EXISTS dwd; USE dwd; CREATE TABLE IF NOT EXISTS orders ( order_id BIGINT, user_id BIGINT, amount DECIMAL(10, 2), status STRING, ts TIMESTAMP(3), PRIMARY KEY (order_id) NOT ENFORCED ) WITH ( 'bucket' = '8', 'bucket-key' = 'order_id', 'file.format' = 'parquet' );

这一步是架构图中“存储层 + 表格式核心层”的实际落地。表建好后,对象存储里就会出现对应的目录结构,你甚至可以直接到文件系统里看数据文件和 manifest 列表,对理解 Paimon 的存储模型非常有帮助。

4.2 用 Flink CDC 持续把业务库数据写入 Paimon

有了表之后,就需要接数据管道。假设业务库有一张订单表,我们用 Flink CDC 把变更实时同步到 Paimon。先定义上游 CDC 表:

CREATE TEMPORARY TABLE orders_cdc ( order_id BIGINT, user_id BIGINT, amount DECIMAL(10, 2), status STRING, ts TIMESTAMP(3), PRIMARY KEY (order_id) NOT ENFORCED ) WITH ( 'connector' = 'mysql-cdc', 'hostname' = '10.0.0.12', 'port' = '3306', 'username' = 'sync_user', 'password' = 'xxxx', 'database-name' = 'app', 'table-name' = 'orders' );

然后一条 SQL 把数据写入 Paimon:

INSERT INTO orders SELECT order_id, user_id, amount, status, ts FROM orders_ccd;

实际执行时需要注意,CDC 表的主键和 Paimon 表的主键要一致。Flink CDC 会输出 insert、update、delete 三类消息,Paimon 主键表能正确处理这些消息,相同主键的记录会按 sequence 字段决定覆盖顺序。首次运行还会自动做全量初始化和增量衔接,对建湖阶段非常友好。

Kafka 路径也是类似的,无非是把上游 CDC 表换成 Kafka 消息表。写 Paimon 时尽量保持按主键分发,否则重分区会带来额外开销。架构图里那条“业务库 -> CDC -> Kafka -> Flink -> Paimon”的链路,到这一步就真实跑通了。

4.3 让下游消费同一张表

数据落湖之后,下游可以按不同方式读。Flink 流读变更,可以这样:

SELECT user_id, status, amount FROM orders /*+ OPTIONS('scan.mode' = 'latest') */;

这个查询会持续输出新增和变更数据,适合实时数仓下游或触发式逻辑。如果要批量查历史数据,直接用 Spark 读 Paimon 更方便。在 Spark 里注册 Catalog:

CREATE DATABASE IF NOT EXISTS my_paimon; USE my_paimon;

看下建表,Spark 的 Catalog 配置方式略有不同,但思路一样。注册完成之后就可以执行:

SELECT status, COUNT(*), SUM(amount) FROM my_paimon.dwd.orders WHERE dt = '2025-04-10' GROUP BY status;

这一条链路下来,就实现了架构图中“Flink 写、Spark 读、同一份数据”的湖仓一体效果。不需要把数据拷贝两份,也不需要做复杂的实时离线对账。

4.4 定时维护任务怎么调度

数据管道跑起来后,一定要把维护任务排上。Paimon 的表不能在长时间运行后放任不管。最基础的是定时执行 compaction,合并小文件:

CALL sys.compact('my_paimon.dwd.orders');

这一步可以通过调度平台,每天在业务低谷跑一次。另一个任务是快照清理,如果建表时设置了合理的snapshot.time-retained,Paimon 会自动过期旧快照,但我们要在监控里关注 snapshot 数量是否持续增长。如果某张表快照数量异常,大概率是 compaction 或过期任务没有正常执行。

另外,还要关注存储量趋势。湖仓空间突然膨胀,通常是单 bucket 小文件过多,需要查写入并行度和 bucket 配置。这个维护流程在架构图里虽然看不到,但它是稳定性的底座。

5. 常见问题排查与避坑指南

5.1 小文件风暴怎么处理

表现是表目录下文件数量非常多,单个文件经常只有几百 KB,查询变慢,compaction 一直追不上。最常见的原因是 bucket 数量设置过大,但写入数据量不大,每个 bucket 分到的数据太少。另一个原因是写入任务提交频率太高,每次 commit 都产生新文件。

我的处理顺序是:先看表目录文件数量,再查写入频率和 bucket 配置。如果 bucket 确实大了,建议重建表,而不是仅仅调大 compaction 频率。如果提交频率高,可以把多条小事务合并成一个大事务,或者用独立 compaction 作业持续清理。记住一个原则,小文件问题不是靠一次性合并能根治的,要从并发和分桶上同时控制。

5.2 snapshot 无限膨胀

snapshot 膨胀的典型现象是 manifest 目录很多,List 文件时非常慢,读取偶尔超时。Paimon 虽然会自动过期快照,但如果你设置的snapshot.time-retained太长,或者长期没有执行过期逻辑,snapshot 就会堆积。

排查时先查看目录下 snapshot 数量,和保留策略对比。如果是流读整个周期很长,不能缩短保留时间,就需要调整下游重启策略,让流读从最新快照开始,而不是从很老的快照开始。如果快照清理已经开始,表目录会周期性出现删除文件操作,属正常现象。

5.3 主键数据重复或乱序覆盖

Paimon 主键表理论上不会重复,但如果你没有配置sequence.field,多个任务写同一张表时,后写入的任务可能用旧数据覆盖新数据。处理办法是给表指定一个严格递增的 sequence 字段,通常用业务时间戳。

另外一个容易踩的问题是 CDC 同步任务重启后,未提交的 reader 可能会导致重复读取,这些重复数据进入 Paimon 后因为有主键可以合并,最终不会出现主键重复,但中间状态可能不符合下游预期。所以在流读下游也要做幂等设计,至少要有唯一标识和时延监控。

5.4 查询慢的定位路径

Paimon 查询慢,先不要怀疑引擎,绝大多数情况下是扫描的数据文件太多。第一步看 SQL 有没有命中分区裁剪,第二步看 bucket 的文件数量,第三步看 manifest 的大小。很多团队忽略了分区字段的设计,导致查询虽然带了过滤条件,但裁剪效果很差。

如果数据总量已经上去了,Paimon 作为湖存储,再怎么优化也难以替代真正的 MPP 数仓。架构图里加一层 OLAP 加速引擎,不是多此一举,而是对不同延迟需求的自然分流。Paimon 更适合做统一源数据和流批底座,亚秒级查询交给专门引擎去扛。

5.5 常见问题速查表

症状可能原因快速处理
文件数量暴增bucket 太多、提交频繁调小 bucket 或合并小文件
快照目录巨大保留时间过长,过期任务没跑调整 snapshot 保留参数
数据覆盖旧值没有配置 sequence.field增加写数据时间戳字段
查询扫描行数大分区字段设计不合理调整分区粒度、用复合分区
流读启动报快照不存在快照被过期清理缩短流读重启延迟
写入反压严重写并行度和 bucket 不匹配调整并发或 bucket 数量

5.6 实操体会收尾

我最想提醒的一点是,Paimon 的架构图看着清晰,但真正理解它,还是要亲手建一张带主键的表,用 Flink 往里写几百万条数据,再观察目录结构、快照和小文件的变化。很多东西在文档里是抽象的,只有亲手处理一次快照膨胀或乱序覆盖,你才知道一个参数配错会带来多大影响。

另外,不要一开始就上复杂的流批链路,先把最简单的“一张表、一个写任务、一个读任务”跑稳,再逐渐增加 CDC、多引擎查询和 OLAP 加速。湖仓一体不是一蹴而就的,先让数据在一张表里流动起来,架构图的价值才会真正体现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询