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 流读都做得非常顺;三是文件合并、快照过期、小文件治理这些日常运维机制,内置得比较完整。
| 能力维度 | Paimon | Iceberg | Hudi |
|---|---|---|---|
| 主键表更新 | 内置 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 加速。湖仓一体不是一蹴而就的,先让数据在一张表里流动起来,架构图的价值才会真正体现。