一张图看懂Apache Paimon湖仓一体架构:从存储到计算 从一张图说起。做数据平台这几年被问得最多的不是要不要上湖仓一体而是湖仓一体现有架构下到底长什么样。直到我把基于 Apache Paimon 的湖仓架构图画在白板上很多同事才真正看懂这条路怎么走底层一套存储上层接流批计算中间靠 Paimon 这张表格式把数据湖的灵活性和数仓的一致性捏在一起。这篇文章就围绕我画的这张典型的 Apache Paimon 湖仓一体架构图把每个框、每条线背后的设计逻辑讲透。同时会附上落地时真正能用到的配置、步骤和排坑经验适合正在选型或者已经决定用 Paimon 搭平台的数据工程师和架构师参考。1. 先看懂这张架构图到底在讲什么1.1 湖仓一体的核心矛盾与 Paimon 的解法湖仓一体的本质是要同时拿到数据湖的两样东西和数据仓的一样东西数据湖的廉价海量存储与开放格式数据仓的 ACID 事务和强一致查询。过去几年大家被这个矛盾折腾得不轻。纯 Hive 表加 Spark 批处理写入时没有事务保护读的时候可能看到半个文件Hive 本身对 update/delete 的支持近乎没有业务想修正一条数据只能整表重写代价高到让人无语。Paimon 切入的角度是表格式层。它不重新发明存储而是把数据文件组织方式、元数据管理逻辑、变更日志生成机制放到一张表内部来做。数据文件还是 Parquet/ORC 放在对象存储或 HDFS 上但文件之间有了清晰的层级关系提交时通过快照管理读的时候可以拿到一致性视图。我在实际项目里最直观的感受是写端终于不用先删整张表再灌全量这种粗暴路子Paimon 表可以直接以 Merge-On-Read 的方式处理更新业务上修正一条记录变成了一件小时级甚至分钟级的事。这套做法的聪明之处在于它把复杂度收敛在表格式内部对上层计算引擎暴露的是标准接口。Flink 读它、Spark 读它、StarRocks 读它各自用自己的方言但底层都遵循同一套快照和文件组织规则。这也是我画架构图时坚持把 Paimon 画在存储与计算之间那层的原因——它不是引擎而是连接引擎与文件系统的枢纽。1.2 一张图上的分层逻辑我画的这张典型架构图自上而下分成四层。最上层是计算引擎层包括 Flink、Spark、StarRocks或者 Presto/Trino根据场景取舍。Flink 主要负责流式写入和流读Spark 负责批量处理和复杂 ETLStarRocks 这类 OLAP 引擎负责对 Paimon 表做高性能查询加速。中间一层是 Paimon 本身包括 Catalog、表定义、快照管理、文件清单Manifest、LSM 树结构管理。这一层是整张图的核心决定了数据怎么组织、怎么提交、怎么被多引擎读取。再往下是文件系统层包括 HDFS 或者 S3/OSS/COS 这类对象存储。Paimon 不强制你用什么存储但不同的存储对应的配置和性能表现差异很大。最底层是计算资源与调度层常见的是 YARN 或 Kubernetes负责给上面这些引擎分配资源。我画这张图的时候特意把数据链路也标了出来业务日志通过 Kafka 或者直接 Flink Source 接入Flink 作业将流式数据写入 Paimon 表与此同时 Spark 批任务扫描同一张表做指标汇总StarRocks 通过外部目录直接查询 Paimon 表支撑报表。三条链路同时跑在一张表上互不阻塞这就是湖仓一体最直观的价值。有一点必须提醒这张图看起来所有组件平铺但实际部署时引擎和文件系统之间的网络拓扑非常关键。如果 Flink 集群在机房 A对象存储在云上区域 B跨地域拉数据会让写入吞吐直接砍半。我踩过这个坑后面会专门讲。2. 架构图里的每个角色都不是摆设2.1 存储层对象存储与 Paimon 的文件组织Paimon 的表数据在文件系统里不是简单平铺的它采用了一套类似 LSM-Tree 的目录组织方式表目录下按 Bucket 分类每一个 Bucket 内部维护若干层级的 Sorted Run。每次写入任务产生的数据文件、删除文件如果启用 deletion vector都注册到 Manifest 文件里而 Manifest 本身也是小文件再由 Manifest List 统一引用。这套结构带来的好处是加粗的写入吞吐因为数据先写内存缓冲memtable达到阈值后刷成不可变文件然后在后台做 compaction 合并写入路径上是顺序追加为主的没有随机写带来的锁竞争。我实测过 Flink 单作业写 Paimon 表在 50 个并行度下写入吞吐稳定在几十万条每秒比 Hive 表配合 Hive Streaming 的方式稳定得多。对象存储和 HDFS 的取舍我给出一个经验建议存储类型优势典型问题适用场景HDFS吞吐高、文件语义成熟、rename 原子性好运维成本高、弹性差已有机房、大规模批处理对象存储S3/OSS/COS弹性、成本低、按量付费List 操作延迟高、rename 不是原子操作、流控云原生、弹性扩缩容如果你选对象存储Paimon 很多版本已经针对它的限制做了适配比如用 copy 代替 rename 提交优化 list 的调用频率但底层存储的物理特性改不掉。设计分区和 Bucket 数量时要避免单分区内文件数量过多触发大量 List 请求这个要注意。一般来说一个 Bucket 对应 1-2 个并发写就够了Bucket 数量定下来之后尽量在生产环境不要改改动等同于一次全表重分布代价极高。2.2 计算层Flink、Spark、StarRocks 各司其职计算引擎这块我最常被问的问题是Paimon 是不是必须用 Flink。答案是写入强烈建议用 Flink但不是必须读取完全随意。为什么写入建议 Flink因为 Paimon 和 Flink 同属 Apache 基金会天然在流写入、CDC 同步、分桶策略上做了深度集成Flink 写 Paimon 的 Connector 支持流式攒批、两阶段提交、动态分桶这些能力这些在其他引擎上要么没有、要么得自己造轮子。Spark 写 Paimon 也是支持的适合离线全量初始化场景。比如一张历史大表需要从 Hive 迁移到 Paimon你可以用 Spark 批量读旧表、写 Paimon 表并行度拉大一次性完成。但需要明白的是Spark 写入时对 LSM 的 memtable 刷写和 compaction 触发的控制没有 Flink 那么精细局部性能可能差一些。StarRocks 这类 OLAP 引擎则是典型的读多写少场景。它在 Paimon 架构图里的定位是查询加速器Paimon 表本身支持在 Trino/Spark 上做查询但面对高并发、低延迟的报表查询还是交给 StarRocks 更靠谱。StarRocks 通过 Catalog 方式直连 Paimon 元数据和文件查询时把谓词条件下推到文件扫描层只读取命中的列和行组响应时间能压到秒级以内。我的建议是核心链路用 Flink 写入 Paimon 存储报表加速用 StarRocks 或 PrestoSpark 用来跑复杂 ETL 和全量初始化谁擅长什么就干什么别让一个引擎硬扛所有事。2.3 元数据与 Catalog容易被忽视的中枢架构图上如果不画 Catalog这张图是不完整的。Paimon 依赖 Catalog 来统一管理库表元数据常见的实现有 Hive Catalog、JDBC Catalog 和 Paimon 自带的文件系统 Catalog。选型上我的经验是Hive Catalog 最通用如果你们公司已经有了 Hive Metastore直接用 Hive Catalog 可以把 Paimon 表暴露给 Hive/Spark 生态里的所有组件兼容性最省心。JDBC Catalog 适合想把元数据集中放到 MySQL 或 PostgreSQL 的场景查询元数据更快不依赖 Hive 那一套。文件系统 Catalog 最简单元数据直接落到指定目录下单机测试方便但多引擎并发访问时容易出问题生产环境慎选。有一点需要注意的是Catalog 本身不保存表数据它只保存表结构、分区信息、快照引用这些元数据。你删掉 Catalog 里的表定义文件系统上的数据文件还在只是没有入口去读了。所以运维上要有严格的元数据即资产意识Catalog 的备份和容灾和底层数据一样重要。我自己就碰到过 Catalog 元数据丢失导致整表数据还在却查不出来的事故最后是靠快照目录手动恢复的耗时巨大。3. 核心机制拆解为什么这张图能通3.1 LSM-Tree 与主键表写入性能的关键Paimon 主键表的核心存储结构是 LSM-Tree。这个概念听起来复杂其实可以类比成先记账再整理账本。每次写入新数据先在内存里记一笔写 memtable记到一定量就形成一个不可变的小账本SSTable 文件这些小账本在后台定期合并成大账本compaction。查询的时候要同时翻阅多个小账本把相同的 key 合并取最新值。这个设计的核心收益是写放大和读放大的权衡。写入时不用去找老文件在哪里、不用锁表直接追加新账本即可所以写吞吐很高。代价是读的时候可能要读多个文件而 compaction 就是为了减少文件数、让读的时候翻的账本尽量少。实际配置中控制 compaction 频率最关键的参数是write-only属性和快照过期时间。如果业务对查询实时性要求不高、主要是批量写入可以开启 write-only 模式把 compaction 推迟到查询或空闲期再做写入性能还能再提升一截。反过来如果业务要求分钟级内能看到最新数据、做分析查询就把 compaction 设置得积极一些减少读路径上的文件数量。主键表的分桶Bucket决定了数据在存储层的物理分布。Paimon 默认按主键哈希分桶目的是让相同 key 的数据落到同一个桶里这样 compaction 和查询都能局部化。分桶数选的不好会出现严重的数据倾斜某些桶文件巨大某些桶几乎没数据。我一般建议 Bucket 数控制在并发写并行度的 1-2 倍并且要结合数据量级预估而不是随便填个数字。3.2 快照机制读写不冲突的基础架构图上表格式和引擎之间最核心的交互就是快照。Paimon 的每次提交都会生成一个新快照快照本质上是指向某个版本文件清单的指针包含当时表内全部有效文件的元数据。读任务只需要拿一个快照顺着快照指向的文件列表去读就能拿到一致性的数据视图。这个机制的一个直接好处是读写分离。写任务在生成并提交新快照的过程中读任务还停留在旧快照上两者之间没有锁竞争。你可以同时跑一个流式写作业和一个大型分析查询写作业不断提交新快照分析查询读它的快照互不干扰。快照的生命周期管理是用snapshot.time-retained参数控制的。默认情况下 Paimon 会保留一段时间的快照比如 24 小时超过时间的快照会被清理。这样设计是为了兼顾时间旅行time travel能力和存储成本。我见过有团队把快照保留时间设成 30 天结果存储成本直接翻倍因为每个快照都引用了大量 manifest 文件和数据文件的指针信息。所以建议生产环境把快照保留时间设为 1-7 天既能满足排查问题、回溯数据的需求又不会让元数据膨胀到失控。如果业务上有更长周期的回溯需求建议把历史数据单独导出归档而不是无限拉长快照保留时间。3.3 Changelog流批一体的秘密武器Paimon 和传统批式表最大的不同是它可以在入库时同步生成 Changelog也就是变更日志。Flink 从 Kafka 消费数据写入 Paimon 时Paimon 会记录每条记录的 insert、update、delete 标记形成类似数据库 binlog 的流。下游 Flink 作业可以直接消费这张 Paimon 表产生的 Changelog实现表到表的增量流计算。这个能力在架构图上是流批一体的直接体现一份存储既可以被批任务全量扫描又能被流任务增量消费。这在传统架构里极其难做到——Hive 表不支持流读Kafka 数据需要单独再存一份供流计算使用现在 Paimon 一张表全搞定了。Changelog 的生成需要开启changelog-producer参数有 input、lookup、full-compaction 等几种模式。input 模式最简单直接消费写入时的输入流来生成 Changelog适合输入本身就已经包含完整前镜像后镜像的 CDC 数据lookup 模式通过查询当前表状态来生成 Changelog适用于输入只有 update-after 没有 update-before 的场景full-compaction 模式则是通过强制合并来产出完整的 Changelog延迟高但最准确。我在项目上的配置经验是如果数据源是 Debezium 或 Canal 同步的 MySQL CDC直接用 input 模式效率和一致性最好如果数据源是业务自个儿拼的更新流优先考虑 lookup对准确性要求极致的场景可以牺牲一些延迟换 full-compaction。4. 照着这张图落地实操步骤与参数建议4.1 环境准备与表模型选择搭环境这一步我的建议是先小后大。先用本地或者单机 Docker 把 Paimon 跑起来确认 Flink 连接器版本和 Paimon 版本兼容再上生产。版本兼容矩阵是 Paimon 官方文档一直在维护的关键信息常见组合是 Flink 1.17/1.18 搭配 Paimon 0.7/0.8Spark 3.4/3.5 搭配对应版本。版本不匹配最常见的问题就是 SQL 客户端提交作业时报 ClassNotFoundException排查起来又慢又痛苦。表模型选择上也别一上来就乱定。Paimon 有主键表Primary Key Table和附加表Append Table两种。主键表适合有更新语义的数据比如业务维表、订单状态表、用户画像表附加表适合只有新增没有更新的日志明细数据比如埋点日志、行为日志。选错模型会带来灾难比如你把日志表建成了主键表每一行都走 LSM 合并流程compaction 压力剧增写入性能和查询效率都会受影响。反过来你把状态表建成附加表数据重复后无法去重下游统计立刻失真。建表的核心 DDL 我一般这么写CREATE CATALOG paimon_catalog WITH ( type paimon, warehouse oss://bucket-name/warehouse/ ); CREATE TABLE IF NOT EXISTS paimon_catalog.db.orders ( order_id BIGINT, user_id BIGINT, status STRING, amount DECIMAL(10, 2), ts TIMESTAMP_LTZ(3), PRIMARY KEY (order_id) NOT ENFORCED ) PARTITIONED BY (dt) WITH ( bucket 16, changelog-producer input, snapshot.time-retained 24 h, merge-engine deduplicate );注意这里的NOT ENFORCEDPaimon 不会在写入时强制校验主键唯一性主键更多是用于数据组织。merge-engine有 deduplicate、partial-update、aggregation 几种默认是 deduplicate。如果需要实时更新某个用户的最新状态可以考虑 partial-update做指标累加用 aggregation。表模型和 merge-engine 的选择直接决定下游读到的数据长什么样这块值得在上线前反复测。4.2 写入链路从 Kafka 到 Paimon典型架构图里的写入链路是业务日志 → Kafka → Flink SQL / Flink CDC → Paimon 表。Flink SQL 写 Paimon 的代码非常简洁本质上是把 Kafka 表源和 Paimon 表目标通过一条 INSERT INTO 语句串起来。CREATE TEMPORARY TABLE kafka_orders ( order_id BIGINT, user_id BIGINT, status STRING, amount DECIMAL(10, 2), ts TIMESTAMP_LTZ(3) ) WITH ( connector kafka, topic orders_topic, properties.bootstrap.servers localhost:9092, properties.group.id paimon_group, scan.startup.mode earliest-offset, format debezium-json ); INSERT INTO paimon_catalog.db.orders SELECT order_id, user_id, status, amount, ts FROM kafka_orders;这里最关键的是设置并行度和攒批参数。Flink 写 Paimon 时每个并行子任务会维护自己的 memtable攒够一定大小或者超过一定时间后触发刷写。参数 sink 在 connector 里是sink.parallelism、write-buffer-size和write-buffer-spillable。如果并行度设的太高每个子任务都有一份内存 buffer整体内存压力会非常大设太低写入吞吐又会受限。通常我会把 sink 并行度设置成 bucket 总数的 1-2 倍这样每个 bucket 至少有一个稳定的写入流又不会堆积太多空闲子任务。第一次跑通这套链路时我建议从scan.startup.mode earliest-offset开始Kafka Topic 里有多少消费多少先确认全量数据能正确写入 Paimon。第二次再做增量提交测试比如只从最新 offset 开始消费保证流式任务重启后不重复写、不丢数据。Kafka 数据进入 Paimon 后过一段时间要做一次数据验证用 Spark 或 Trino 在 Paimon 表上跑几个 count、sum 查询和源系统的统计指标做对比。这个步骤虽然基础但能及早发现字段类型映射错误比如 STRING 和 DECIMAL 混用导致的数据截断和主键冲突等数据质量问题。4.3 查询链路与 OLAP 加速查询侧架构图上常见的是三条路Flink 流读消费 Changelog、Spark 批读、StarRocks 外部表查询。Spark 批读适合跑 T1 报表或全量分析代码大致是val df spark.read.format(paimon) .load(oss://bucket-name/warehouse/db/orders) .filter(dt 2025-01-01) df.createOrReplaceTempView(orders) spark.sql(select status, sum(amount) from orders group by status).show()如果你希望兼顾查询性能和数据新鲜度StarRocks 或 Trino 会是更顺手的读引擎。以 StarRocks 为例创建 External Catalog 指向 PaimonSQL 就能直接查CREATE EXTERNAL CATALOG paimon_catalog PROPERTIES ( type paimon, paimon.catalog.type filesystem, paimon.catalog.warehouse oss://bucket-name/warehouse/ ); SELECT status, sum(amount) FROM paimon_catalog.db.orders WHERE dt 2025-01-01 GROUP BY status;查询性能优化的重点一是分区裁剪二是谓词下推。Paimon 的元数据中记录了每个文件列的统计信息查询引擎可以只扫描满足条件的数据文件。为了让这种下推真正生效建表时列的排序很重要。比如你经常按dt和status过滤那么在表设计时把这两个字段放在靠前的列位置或者在文件内设置合理的排序键能显著减少扫描量。这一点经常被忽略但实测中查询性能差距可以有数倍。另外不要忽略列存压缩的收益。对象存储上的 Parquet 文件已经做了列式压缩如果你用 StarRocks 建物化视图或者聚合表来承接高频报表可以把 Paimon 表的明细数据进一步聚合成宽表报表查询延迟从秒级压到毫秒级。这也是为什么架构图上 OLAP 引擎要单画一层——它承载的是快的诉求而 Paimon 承载的是准、全、流批一体的诉求。5. 常见问题与排查技巧实录5.1 小文件失控用 Paimon 最典型的问题是小文件太多。流式作业长期运行每次 checkpoint 都可能刷一批小文件如果 bucket 数量又很大存储上会出现成千上万个几十 KB 的文件。这会导致两个后果文件系统元数据膨胀、查询扫描文件数量巨大、性能急剧下降。排查方法是先看每个 bucket 下的数据文件数量和大小用 Spark 扫描 Paimon 表目录把文件大小分布统计出来。正常情况下单个数据文件应该在 64MB-256MB 之间如果大量文件小于 10MB说明刷写策略或 compaciton 策略需要调整。解决办法有几个方向一是调大write-buffer-size让 memtable 攒得更久再刷写单文件更大二是开启自动 compaction让后台在写入空闲期合并小文件三是给 Flink 作业配置合理的 checkpoint 间隔避免频繁 checkpoint 触发频繁刷写。还有一个隐藏参数write-buffer-spillable打开后允许 memtable 在内存不足时溢写到磁盘防止小文件因内存压力而过早刷出。5.2 快照过期和存储成本失控我见过不少团队接入 Paimon 后存储成本突然涨了一截排查下来十有八九是快照保留时间设太长。Paimon 每次提交都生成新快照快照引用的 manifest 文件会重复记录文件列表信息快照越多元数据冗余越多。而且 Paimon 的snapshot.time-retained默认并不算短如果不主动设置它会按默认时间去保留历史对象存储上的容量账单就会悄悄变大。我建议上线前就把快照保留策略定好。数据回溯需求严格评估能留 1 天就不留 7 天。清理低频旧快照可以用CALL sys.remove_orphan_files或者直接依赖过期机制前提是把snapshot.expiration相关参数调好。此外对象存储的生命周期规则也可以和 Paimon 的快照清理配合使用比如对超过 N 天的 Manifest 文件做归档或转冷进一步压低成本。5.3 数据乱序与主键冲突流式写入过程中最隐蔽的问题是乱序数据。Kafka 的分区机制一般能保证分区内有序但如果有多个 Flink 子任务并行消费多个分区同一个主键的数据可能被打散到不同子任务再加上网络延迟和重试写入顺序就乱了。Paimon 对乱序数据的处理方式是依赖快照的水位线和合并策略但如果你用的是 deduplicate 模式后到的旧数据可能覆盖新数据造成最终结果错误。解决办法是在写入链路里显式带一个事件时间字段然后在 Flink 作业里做去重或取最大版。比如同一订单存在多次状态变更下游只需要最新状态可以在写入 Paimon 前用ROW_NUMBER()按ts降序取第一条或者在 Paimon 表上用 aggregation merge-engine把status字段配置为last_non_null_value这样合并时天然取最新值。第一种方案适合数据量可控的场景第二种适合希望把逻辑沉淀在表里的场景。另外一个常见乌龙是主键冲突导致写入失败。Paimon 分桶出现数据倾斜时某个桶的写入压力过大compaction 跟不上作业会出现反压最终表现为 checkpoint 超时。遇到这种情况先检查是否数据主键分布不均匀比如订单表的order_id本身随机性好但分区键user_id有热点用户。如果热点严重建议调整桶数或者改用更离散的主键组织方式别让单个桶成为瓶颈。5.4 多引擎并发读写需要避的坑架构图上多引擎并发实际踩坑最多的是 Catalog 权限和文件可见性。Flink、Spark、StarRocks 同时连接同一个 Paimon 表时各个引擎通过 Catalog 获取最新的快照信息理论上没有问题但是如果你混用了不同版本的连接器就可能出现我改了表结构另一个引擎看到的还是旧结构这类问题。我的经验是所有引擎的 Paimon 连接器版本保持一致Catalog 配置尽量统一。比如统一走 Hive Catalog所有引擎都通过 Metastore 拿元数据这样结构变更能第一时间同步。如果某一个引擎查不到新分区优先确认它的 Catalog 缓存刷新策略必要时重启该引擎的元数据缓存。还要提醒一点对象存储的并发读写场景下尽量避免多个作业同时写同一张 Paimon 表。Paimon 支持并发写但并发协作的协商成本是客观存在的尤其在 Bucket 数量有限时多作业同时写可能触发锁冲突和提交失败。能合并到一个 Flink 作业里通过多 Sink 写就不要拆多个作业。6. 从架构图到真实生产力的最后一公里架构图画得再漂亮最终的检验标准还是业务能不能稳定跑起来。我个人在多次落地中总结出一套从图到生产的最小验证路径先用 Flink SQL 跑通一条 Kafka 到 Paimon 的写入链路再用 Spark 跑一个分组聚合查询最后接上 StarRocks 出报表。三步走完架构图里的每条连线都验证过了再逐步加复杂场景比如 CDC 同步、部分字段更新、二级索引。过程中最花时间的一定不是建表和写 SQL而是资源规划和参数调优。Paimon 的默认参数对单机测试友好生产环境必须按数据规模重调内存、并行度和 compaction 策略。没有哪个参数组合能一劳永逸关键是理解每个参数背后对应的机制比如 buffered 内存、文件刷写频率、快照清理时效当业务增长时你能敏锐地判断该动哪个旋钮。最后分享一个我自己很受益的小习惯周期性巡检表的状态和健康度。用下面的 SQL 就能看到每个表的快照数、文件数、总大小SELECT table_name, snapshot_count, total_size_mb, file_count FROM paimon_catalog.sys.all_table_stats;每张表的snapshot_count如果异常增长说明写入频率远大于 compaction 频率file_count如果持续上扬说明小文件问题正在恶化。巡检数据不用天天看每周扫一眼就能把问题消灭在萌芽期。这套架构真正跑稳之后你会发现开发和运维的负担都比传统 HiveFlink 两条链要小得多因为湖和仓的边界终于真的消失了。