Apache Paimon 官方文档
改自官方文档
https://paimon.apache.org/docs/1.0/
Apache Paimon 是一种支持流批一体处理的实时湖存储格式,旨在构建现代化的实时湖仓架构;它 创新性地融合湖存储格式与LSM树结构,既保持了数据湖的低成本、可扩展优势,又提供了数据库般的高效更新和查询能力,将数据湖的开放性与实时流处理能力相结合,真正实现了"湖仓一体"的实时数据处理架构。
核心能力总结
1. 实时更新处理
- 主键表大规模更新:支持通过Flink Streaming进行高性能、大规模的数据更新操作
- 灵活合并引擎:提供多种数据更新策略:
- 保留末行去重
- 部分列更新
- 数据聚合
- 保留首行记录
- 完整的变更日志:支持生成准确的changlog,简化流式分析流程
2. 海量数据批流处理
- 追加表处理:支持无主键表的大规模批处理和流处理
- 自动优化:
- 自动小文件合并
- Z-Order数据压缩优化文件布局
- 高效查询:基于min-max等索引实现数据跳过,提升查询性能
3. 完整数据湖特性
- 可扩展元数据:支持PB级数据量和海量分区分桶管理
- 企业级特性:
- ACID事务保证
- 时间旅行(数据版本回溯)
- Schema演化(结构演进)
4. 统一存储架构
- 表抽象层:提供与传统数据库一致的使用体验
- 多模式支持:
- 批处理模式:类似Hive表,支持完整批处理SQL操作
- 流处理模式:类似消息队列,支持永不丢失历史的流式变更日志查询
5. 生态兼容性
- 多引擎支持:兼容Apache Flink、Spark、Hive、Trino等主流计算引擎
- 多样化数据接入:
- 支持CDC流式同步
- 支持离线数据批量导入
Paimon 如何体现“湖仓一体”(Lakehouse)
传统的架构中,数据湖(Data Lake)和数据仓库(Data Warehouse)是分离的:
- 数据湖:存储在廉价对象存储(如S3、OSS)上的原始数据,格式开放(如Parquet),成本低、扩展性强,但缺乏事务支持、更新能力弱,查询性能不佳。
- 数据仓库:存储在专用系统中的高度结构化数据,支持事务、高速更新和复杂查询,性能极高,但成本昂贵、数据格式封闭、扩展性有上限。
湖仓一体旨在打破这种割裂,用一个统一的、开放的数据平台,同时具备两者的优势。
1. 数据湖的低成本与可扩展优势 (The "Lake" Side)
Paimon 完美继承了数据湖的根基:
- 开放文件格式:文档中提到
Paimon stores the columnar files on the filesystem/object-store
。它将数据以列式文件(通常是Apache ORC或Parquet)的形式存储在廉价的文件系统或对象存储(如HDFS、S3、OSS)上。这与传统数据湖的存储方式完全一致,带来了极低的存储成本和近乎无限的扩展能力。 - 开放的生态系统:
supports read by other computation engines like Apache Hive, Apache Spark and Trino
。它不像封闭的数据仓库(如Snowflake、早期ClickHouse),数据被锁定在专有系统中。任何支持读取这些开放格式的计算引擎都可以直接访问Paimon的数据,实现了最大的灵活性和开放性。
2. 数据仓库的高效能力 (The "House" Side)
这是Paimon的创新之处,它在数据湖的基础上,赋予了传统数据仓库才有的核心能力:
-
【事务支持 ACID】:文档明确说明
Supports ACID Transactions
。这是数据仓库的基石能力。它确保了在并发读写时数据的完整性和一致性。例如,流式写入任务和批式查询任务可以同时进行,而不会读到损坏的中间状态数据。这是与传统数据湖(通常只能追加、覆写)最根本的区别之一。 -
【高效更新与删除】:这是LSM树结构带来的核心优势。
Primary key table supports writing of large-scale updates
:支持基于主键的大规模、高性能更新和删除。这不再是数据湖的“覆写整个分区”的笨重模式,而是像数据库一样精准、高效地更新记录。Support defining Merge Engines
:提供了灵活的冲突解决策略(保留最后一行、聚合等),这极大地丰富了数据处理的语义。
-
【高性能查询】:
provides fast queries based on data skipping using indexes such as minmax
:通过索引(minmax、bitmap等)实现数据跳过(Data Skipping),在扫描文件前就能快速过滤掉大量不相关的数据,极大提升了查询性能,这是数据仓库的典型优化手段。Supports Data Compaction with z-order sorting
:通过Z-Order排序优化数据布局,将相关性强的数据排列在一起,进一步提升过滤效率。Automatic Small File Merge
:自动合并小文件,解决流式写入带来的小文件问题,避免查询性能因文件数过多而下降。
-
【企业级管理功能】:
Time Travel
:时间旅行功能允许用户查询历史任意时间点的数据快照(例如,SELECT * FROM table VERSION AS OF '2023-01-01'
)。这既是数据审计的需求,也是高级分析(如回溯分析、故障排查)的利器。Schema Evolution
:Schema演化支持表结构随着业务需求的变化而平滑变更(如增加列、修改类型),而无需重写整个数据集,保证了业务的连续性。
总结:Paimon如何统一“湖”与“仓”
Paimon的“湖仓一体”体现在它用一个统一的、开放的数据存储,同时服务于流处理和批处理两种范式,满足了BI报表、实时分析、数据科学等多种负载。
- 对流处理引擎(如Flink)而言:Paimon
acts like a message queue
。它不再需要先将数据写入Kafka这样的消息队列,再写入ClickHouse这样的OLAP库,最后为了批处理再同步一份到Hive。Paimon一个系统就扮演了流水线中的多个角色,简化了架构,降低了运维成本,并保证了端到端的 Exactly-Once 语义。 - 对批处理引擎(如Spark、Hive)而言:Paimon
acts like a Hive table
。批处理作业可以像查询普通Hive表一样,直接读取Paimon中最新、最全的数据快照进行计算,无需复杂的数据同步和转换。
结论:
Apache Paimon 的湖仓一体,不是简单的“湖”+“仓”的拼凑。它是在数据湖的开放、低成本存储基石上,通过LSM树、索引、ACID事务等创新设计,原生地“长”出了数据仓库的高效更新、高性能查询和管理功能。它用一个技术栈统一了原本需要多个系统协同才能完成的复杂数据架构,实现了流批的统一、存储与计算的解耦,是湖仓一体理念的杰出工程实践。
Paimon 核心概念与文件布局解析
一、文件组织结构
Paimon采用分层式文件结构,所有表文件统一存储在基础目录下,其核心层级包括:
-
快照文件(Snapshot)
- 存储位置:
snapshot
目录 - 格式:JSON文件
- 内容:
- 当前使用的schema文件信息
- 包含本次快照所有变更的清单列表(Manifest List)
- 功能:捕获表在特定时间点的状态,支持通过最新快照访问实时数据,并通过时间旅行功能回溯历史状态。
- 存储位置:
-
清单文件(Manifest Files)
- 存储位置:
manifest
目录 - 组成:
- 清单列表(Manifest List):记录所有清单文件名称的索引
- 清单文件(Manifest File):记录LSM数据文件和变更日志文件的变更详情(如文件的创建与删除)
- 作用:建立快照与数据文件之间的映射关系,实现高效的数据追溯。
- 存储位置:
-
数据文件(Data Files)
- 存储格式:支持Parquet(默认)、ORC、Avro
- 组织方式:按分区(Partition)分组存储,优化数据管理与查询效率。
二、分区机制
- 设计理念:与Apache Hive分区方案兼容,通过特定列(如日期、城市)的值将表划分为逻辑相关部分。
- 优势:
- 支持单或多个分区键,快速定位数据分区
- 高效操作分区内的数据子集,提升查询性能
三、一致性保障
Paimon通过两阶段提交协议(Two-phase Commit Protocol) 保证数据原子性写入:
提交机制:
- 单次提交最多生成两个快照(取决于写入策略):
- 仅增量写入时 → 生成增量快照
- 触发压缩操作时 → 生成增量快照 + 压缩后快照(两次快照提交)
见:Paimon LSM Tree写入 和 Compaction 如何不冲突
并发控制:
- 跨分区写入:支持并行提交,无冲突
- 同分区写入:保证快照隔离(Snapshot Isolation)
- 最终状态为两次提交的混合结果
- 确保所有变更不丢失,但可能存在中间状态交错
见:Paimon原子提交机制解析
同分区并发写入的"混合结果"本质
假设两个写入任务(Writer A和Writer B)同时修改表的同一个分区:
时间 | Writer A操作 | Writer B操作 |
---|---|---|
T1 | 插入记录(id=1, value=100) | 插入记录(id=2, value=200) |
T2 | 更新记录(id=1, value=150) | 删除记录(id=2) |
在快照隔离下,最终表可能呈现两种合法状态之一:
- 状态组合1
- 包含
(id=1, value=150)
- 不包含
id=2
(Writer B的删除生效)
- 包含
- 状态组合2
- 包含
(id=1, value=100)
(Writer A的更新未生效) - 包含
(id=2, value=200)
(Writer B的删除未生效)
- 包含
👉 这就是文档所说的"混合结果":最终状态可能是两个写入任务的部分操作组合,但绝不会丢失任何操作(如不会出现id=1和id=2都消失的情况)。
如果业务要求严格保证:
- "Writer B必须基于Writer A更新后的数据操作"
- "两个写入必须作为一个原子单元生效"
Paimon的应对方案
- 分区设计:将存在逻辑依赖的数据划分到不同分区
- 外部协调:通过Flink的Checkpoint或分布式锁控制写入顺序
- 合并引擎:使用
deduplicate
或aggregation
、Partial Update 合并策略消除冲突
这种设计非常适合数据湖场景下的大规模并行ETL作业,但需要用户在业务逻辑层处理可能的中间状态(如通过幂等操作或补偿机制)。
对于不同列的partial update 的合并自然是幂等的。
见:Paimon 是否能够多个任务同时写同一个桶
Paimon 并发控制
Paimon 采用乐观并发控制(Optimistic Concurrency Control, OCC) 策略来处理多任务并发写入,其核心思想是“先执行,后提交,遇到冲突再处理”。这与传统数据库的悲观锁(如行锁、表锁)机制截然不同。
一、两种提交冲突及其处理方式
Paimon 的提交过程可能会遇到两种类型的失败:
1. 快照冲突 (Snapshot Conflict)
- 根源:快照ID被抢占。当Job A正在基于快照S1准备提交时,Job B抢先提交并成功生成了新的快照S2。
- 机制:
- 快照ID是全局唯一的。
- 提交本质上是将临时快照文件重命名(Rename) 为最终快照文件。
- 处理方式:自动重试。
- 作业检测到当前最新快照已不是自己基于的那个版本,会简单地重新基于最新快照(S2)再次尝试提交。这是一个轻量级的、通常能成功的操作。
- 存储差异:
- HDFS:利用文件系统原生的、原子性的重命名操作,无需额外组件即可保证提交安全。
- 对象存储(OSS/S3):其“重命名”非原子操作。必须启用分布式锁(通过配置Hive/JDBC Metastore并设置
lock.enabled=true
) 来模拟原子性,否则可能导致快照丢失。
2. 文件冲突 (File Conflict)
- 根源:逻辑删除冲突。Job A准备删除文件F,但在提交时发现F已经被Job B在更新的快照中逻辑删除了。
- 处理方式:失败重启(Failover)。
- 作业无法自动解决此冲突,只能故意使自身失败并重启。
- 重启后,作业从文件系统重新加载最新状态,希望基于新状态继续工作能避开冲突。
- 影响:
- 保证数据正确性:无数据丢失或重复。
- 可能影响可用性:如果两个流作业持续写入并频繁发生文件冲突,会导致作业陷入“重启-冲突-再重启”的循环,影响稳定性。
为什么Compaction冲突需要重启
既然读取时遇到被Compaction删除的文件可以通过OutOfRangeException
触发reopen
恢复,为什么文件冲突的写入作业必须重启(而不仅仅是reopen)?
Paimon Snapshot 快照过期机制 分析了lookup时SST过期会怎么做。
关键区分:
- 读取场景:消费者(Reader)遇到文件缺失时,只需
reopen
(重新打开最新快照)即可继续消费,这是一个无状态的轻量级操作。 - 写入场景:生产者(Writer)遇到文件冲突时,必须重启整个作业,因为写入作业的内部状态机可能已不一致。
Paimon的流式写入作业(如Flink Job)具有连续状态:
- Checkpoint依赖:冲突发生时,作业可能已经记录了部分状态的Checkpoint,继续运行会导致后续状态基于错误的前提。
- Exactly-Once语义:重启后从最后一个完整Checkpoint恢复,是保证端到端精确一次语义的唯一可靠方式。
这里可以补充具体分析,实际上也存在reopen 解决冲突的可能。不过两个compaction同时运作本身浪费资源。
二、冲突的本质与最佳实践
-
冲突根源:文件冲突本质上源于压缩(Compaction) 操作。压缩是为了合并小文件、优化查询,但这个过程会产生文件删除动作,从而成为并发写入的主要冲突点。
-
最佳实践:解耦写入与压缩。
- 方案:为写入作业设置
'write-only' = 'true'
关闭其压缩功能。然后启动一个独立的、专用的压缩作业来统一为表执行压缩。 - 优势:
- 减少冲突:写入作业只追加数据(不会产生删除文件的动作),从根本上避免了文件冲突。
- 提升稳定性:写入作业不再因压缩而重启,变得非常稳定。
- 资源控制:压缩是资源密集型操作,独立作业可以更好地控制其资源分配和执行周期。
- 方案:为写入作业设置
Paimon Catalog
Paimon 提供 Catalog 抽象层 来统一管理表的元数据和目录结构。它是访问 Paimon 表的推荐方式,旨在无缝集成各种计算引擎(如 Flink, Spark, Hive)。
三种元数据存储模式(Metastore)
Paimon Catalog 支持三种模式,适应不同技术栈和环境需求。
模式 | 核心特点 | 元数据存储位置 | 表数据存储位置 | 适用场景 |
---|---|---|---|---|
Filesystem (默认) | 轻量级,无外部依赖 | 文件系统(与数据文件在一起) | 文件系统(warehouse 路径) | 测试、简单生产环境,无Hive生态需求 |
Hive | 与Hive元数据无缝集成 | Hive Metastore + 文件系统 | 文件系统(warehouse 路径) | 需要被Hive直接读取,或与现有Hive生态工具(如Atlas, Ranger)集成 |
JDBC | 元数据高可用,共享性强 | 关系型数据库(MySQL, PostgreSQL等) | 文件系统(warehouse 路径) | 生产环境,需要多引擎共享且高可用的元数据服务 |
Hive/JDBC Catalog存储了什么?
Paimon的元数据实际上分为两个层级,它们的存储位置和用途完全不同:
1. 表结构元数据(Catalog Metadata)
- 内容:表名、列名、数据类型、分区字段、表属性(如
metastore.partitioned-table
)等结构定义信息。 - 存储位置:由Catalog类型决定:
- Hive Catalog:存储在Hive Metastore(通常是MySQL或Derby)
- JDBC Catalog:存储在配置的关系数据库(如MySQL)
- Filesystem Catalog:存储在文件系统的
_metadata
目录下(如hdfs://path/to/warehouse/db.db/table/_metadata/schema-0
)
2. 数据快照元数据(Snapshot/Manifest)
- 内容:
- Snapshot:记录每次提交的快照信息(版本号、schema版本、包含的Manifest列表)
- Manifest:记录数据文件(Data Files)的变更记录(哪些文件被添加/删除)
- 存储位置:始终存储在文件系统(与数据文件在一起),无论使用哪种Catalog类型。路径示例:
/warehouse/db.db/table/├── _metadata/ │ ├── snapshot-1 # 快照文件│ ├── manifest-1-0 # 清单文件│ └── schema-0 # 结构元数据├── partition=2023-01-01/│ └── data-file-1.parquet
当使用Hive Catalog或JDBC Catalog时:
- 额外存储的仅是表结构元数据(即Hive标准的
TBLS
表信息),不包括Snapshot/Manifest。 - 数据文件和Snapshot/Manifest依然存储在文件系统(如HDFS/S3)
为什么这样设计?
1. 解耦与性能
- Snapshot/Manifest是高频更新的数据(每次写入都会生成),适合存储在可扩展的文件系统。
- 表结构是低频变更的数据(如修改列类型),适合用事务型数据库保证一致性。
2. 生态兼容性
- 将表结构存入Hive Metastore后,Hive/Spark等工具无需理解Paimon的Snapshot机制就能识别表结构(即使它们读不到最新数据)。
Hive 和 MySQL 存储元数据的好处
将元数据存储在 Hive Metastore 或关系型数据库(如 MySQL)中,相比纯粹存储在文件系统上,主要有以下几个核心优势:
-
集中化与共享性 (Centralization & Sharing)
- 好处:Hive Metastore 或 MySQL 数据库作为一个独立的、中心化的服务运行。多个用户、多个计算引擎(Flink, Spark, Hive, Trino)可以同时连接到这个服务,获取统一的、一致的元数据视图。
- 对比:Filesystem Catalog 的元数据分散在每个表的目录里,没有中心化的协调点,难以保证多引擎并发访问时的一致性。
-
元数据的高可用与持久化 (High Availability & Durability)
- 好处:像 MySQL 这样的关系数据库本身具备高可用(主从复制)和强一致性(ACID 事务)能力。这意味着元数据服务非常可靠,不会因为单点故障而丢失或损坏。
- 对比:存储在文件系统上的元数据文件本身没有高可用机制(除非底层文件系统如 HDFS 提供多副本),且缺乏事务保证。
-
更强的生态集成能力 (Ecosystem Integration)
- 好处:这是 Hive Catalog 最主要的价值。将表结构注册到 Hive Metastore 后,整个 Hadoop 生态系统中任何能与 Hive 集成的工具(如 Spark、Impala、Presto/Trino,以及数据治理工具 Ranger、Atlas 等)都能自动发现这张 Paimon 表。用户可以直接用
Hive
或Beeline
命令行来查看和查询(尽管可能读不到最新数据),极大地降低了使用和运维的门槛。
- 好处:这是 Hive Catalog 最主要的价值。将表结构注册到 Hive Metastore 后,整个 Hadoop 生态系统中任何能与 Hive 集成的工具(如 Spark、Impala、Presto/Trino,以及数据治理工具 Ranger、Atlas 等)都能自动发现这张 Paimon 表。用户可以直接用
-
性能与锁机制 (Performance & Locking)
- 好处:对于高频的元数据操作(如并发创建大量表),基于数据库的 JDBC Catalog 通常比在文件系统上大量创建小文件性能更好。此外,JDBC Catalog 是实现跨系统原子重命名(Rename)和分布式锁的关键,这对于在对象存储(S3、OSS)上保证并发安全至关重要。
Catalog 包含的信息及产生的表
一个 Paimon Catalog 实例主要管理以下信息:
- 数据库 (Database) 列表:例如
default_db
,my_database
。 - 表 (Table) 列表:每个数据库下包含哪些表。
- 表的元数据 (Schema):这是核心信息,包括:
- 表名
- 字段名、字段数据类型
- 分区字段定义
- 表的主要属性(Options),如
'metastore.partitioned-table' = 'true'
,主键信息等。 - 注意:表的详细快照(Snapshot)、清单(Manifest)等动态元数据依然存储在对象存储(
warehouse
路径下),不在这里。
当在 Paimon 中使用 CREATE TABLE ...
创建一张表时,Hive Catalog 会在 Hive Metastore 中创建对应的元数据记录。
- 在 Hive Metastore 的
DBS
表中:注册或关联一个数据库。 - 在 Hive Metastore 的
TBLS
表中:注册一张新表。这张表的TBL_TYPE
通常是MANAGED_TABLE
或EXTERNAL_TABLE
。 - 在 Hive Metastore 的
TABLE_PARAMS
表中:存储这张表的一些参数。 - 在 Hive Metastore 的
SDS
、COLUMNS
等表中:存储表的字段信息、存储位置(指向 Paimon 的warehouse
路径)等。
关键点:在 Hive 中产生的这张表,本质上是一个指向 Paimon 数据文件的“指针”或“代理”。它让 Hive 知道:“这里有一张表,它的结构是这样的,数据文件在哪个位置”。
metastore.partitioned-table
选项的作用
这个选项决定了 Hive 中这张“代理表”的表现形式:
-
false
(默认):在 Hive 中显示为一张非分区表。即使 Paimon 表有分区,Hive 也认为它没有分区。分区过滤由 Paimon 自己在读取时完成。- 优点:简单,避免向 Hive 同步大量分区元数据的开销。
- 缺点:Hive 原生优化器无法基于分区进行优化。
-
true
:在 Hive 中创建为原生分区表。Paimon 会将其分区信息同步到 Hive Metastore 的PARTITIONS
等相关表中。- 优点:与 Hive 生态工具兼容性最好,Hive 可以直接识别和管理分区。
- 缺点:同步分区信息有额外开销。
详细对比与使用指南
1. Filesystem Catalog
- 配置:仅需指定仓库路径(
warehouse
)。CREATE CATALOG my_catalog WITH ('type' = 'paimon','warehouse' = 'hdfs:///path/to/warehouse' -- 或 oss://, s3:// 等 );
- 特点:部署最简单,但元数据无法被其他系统(如Hive)直接感知。
2. Hive Catalog
- 配置:指定
metastore
类型为hive
。warehouse
参数通常可省略,默认使用Hive配置中的仓库路径。CREATE CATALOG my_hive WITH ('type' = 'paimon','metastore' = 'hive' -- 关键:元数据存到 Hive Metastore-- 不指定 warehouse 时,默认继承 hive-site.xml 中的 hive.metastore.warehouse.dir );
- 核心价值:双向同步。在Paimon中创建的表,可以在Hive中直接查询。
- 分区表高级配置:
- 默认行为 (
metastore.partitioned-table = false
):在Hive中显示为非分区表。分区过滤由Paimon的谓词下推功能完成,对用户透明。 - 显式同步 (
metastore.partitioned-table = true
):在Hive中创建标准分区表,并同步新分区到Hive Metastore。适用于需要Hive原生分区管理或通过Hive外部工具写入分区的场景。
- 默认行为 (
3. JDBC Catalog
- 配置:需指定JDBC连接参数和仓库路径。
CREATE CATALOG my_jdbc WITH ('type' = 'paimon','metastore' = 'jdbc','uri' = 'jdbc:mysql://host:port/db','jdbc.user' = 'user','jdbc.password' = 'pass','warehouse' = 'hdfs:///path/to/warehouse' );
- 核心价值:
- 元数据高可用:利用MySQL/PostgreSQL等数据库的集群能力,避免单点故障。
- 元数据共享:多个Paimon Catalog实例(甚至不同引擎)可以连接到同一个JDBC数据库,共享和集中管理元数据。
核心架构与设计哲学
-
分离式架构:Paimon遵循元数据(Meta) 与数据(Data) 分离的设计。
- 数据:始终以开放格式(Parquet/ORC)存储在廉价对象存储(如S3、OSS、HDFS)上。
- 元数据:提供了灵活的选择,可以存放在文件系统、HMS或关系数据库中,以满足不同场景下的可访问性和可靠性要求。
-
生态集成优先:Hive Catalog的设计深刻体现了这一点。它不是简单地将元数据写入HMS,而是通过
metastore.partitioned-table
选项,在性能(谓词下推)和兼容性(原生Hive分区表)之间提供了灵活的选择。 -
生产环境推荐:
- 如果需要与Hive生态交互,选择Hive Catalog。
- 如果需要构建高可用、多引擎共享的湖仓平台,选择JDBC Catalog。
- Filesystem Catalog仅建议用于开发测试或简单场景。
总结:Paimon的Catalog体系是其成为开放、多引擎共享的湖仓格式的关键组件。它通过支持多种元存储后端,实现了与大数据生态系统的深度集成,为用户提供了根据自身技术栈和运维能力进行灵活选择的可能。
Paimon 表类型
Paimon 提供了丰富多样的表类型,以支持从传统的结构化数据到现代的非结构化数据、从实时更新到物化视图等多种数据管理和处理场景。其核心设计思想是一栈多能,用一个统一的存储框架满足多样化的需求。
一、核心表类型对比
表类型 | 核心特征 | 主要能力 | 适用场景 |
---|---|---|---|
有主键表 (with PK) | LSM结构,支持主键约束 | 流式更新、删除、变更日志读取、批处理 | 实时数仓核心表,需要CDC、维表关联、实时更新的场景 |
无主键表 (w/o PK) | 仅追加,无主键约束 | 批量DELETE /UPDATE ,但不支持流式更新 | 日志流水、事件流、事实表,数据天然仅追加的场景 |
视图 (View) | 虚拟表,不存储数据 | 保存查询逻辑,简化复杂查询,跨引擎兼容 | 数据权限控制、逻辑数据抽象、复用复杂查询逻辑 |
格式表 (Format Table) | 映射外部数据 | 直接读写Hive等外部表的数据文件,生态兼容 | 查询现有Hive表,作为数据接入或输出的桥梁 |
对象表 (Object Table) | 管理非结构化数据 | 为OSS/S3上的文件(图片、PDF等)建立元数据索引 | AI场景(处理图片、视频)、文档分析、多模态数据管理 |
物化表 (Materialized Table) | 预计算聚合结果 | 自动增量刷新,查询加速 | 实时指标看板,简化流批一体聚合管道 |
二、详细解析与设计哲学
1. 有主键表 vs. 无主键表:更新模式的抉择
- 有主键表:是Paimon的核心创新。通过LSM树结构,在数据湖上实现了数据库般的高效更新和删除能力,这是构建实时湖仓的基石。
- 无主键表:更接近传统数据湖的追加模式,虽然支持批量更新,但旨在提供更好的兼容性和处理纯追加数据的性能。
2. 视图与格式表:生态集成的双翼
- 视图:在逻辑层集成,通过统一SQL语法屏蔽底层差异。
- 格式表:在物理层集成,直接操作Hive等系统的数据文件,实现了元数据与数据双层面的兼容。
'format-table.enabled'
选项体现了其可插拔的设计。
3. 对象表与物化表:面向未来的扩展
- 对象表:将湖仓能力从结构化数据拓展到非结构化数据,瞄准AI和多模态应用,是前瞻性的设计。
- 物化表:旨在简化流处理开发。通过声明式的
FRESHNESS
策略自动维护中间聚合结果,用户无需再手动编写复杂的流式聚合作业,极大降低了开发门槛。
三、关键技术实现要点
- 有主键表:
PRIMARY KEY ... NOT ENFORCED
与'bucket'='8'
是关键配置。主键决定了数据排序和更新方式,分桶数影响并行度和文件大小。 - 格式表:通过
'file.format'='csv'
等参数指定外部文件格式,其行为与Hive表高度一致,分区发现等机制也得以保留。 - 物化表:
FRESHNESS = INTERVAL '30' SECOND
是核心,它定义了结果更新的频率,实现了从“手动维护”到“自动维护”的转变。 - 对象表:
'object-location'
指向数据存储位置,CALL sys.refresh_object_table()
用于手动刷新元数据索引,支持时间旅行(scan.snapshot-id
)体现了其高级功能。
四、生产应用建议
- 实时业务核心表:优先选择有主键表,利用其流式更新能力。
- ETL中间表或日志表:可选择无主键表,节省存储和计算开销。
- 集成现有Hive数仓:使用格式表进行平滑迁移和混合查询。
- 构建实时数仓指标层:使用物化表自动聚合明细数据,简化架构。
- 管理AI训练数据:使用对象表对存储在OSS上的图片、文档等进行编目和查询。
总结:Paimon的表类型体系展现了其一体化和可扩展的设计哲学。它并非一个单一的数据湖格式,而是一个统一的数据管理平台,既能高效处理传统的结构化数据批流处理,也为未来的非结构化数据分析和AI工程化提供了强大的原生支持。
Paimon 系统表
Paimon 的系统表功能是其作为可观测数据湖的核心体现,提供了从微观(单表文件)到宏观(整个 Catalog)的全方位监控和诊断能力。其设计哲学是透明化、可调试、可管理。
一、系统表分类与价值
Paimon 的系统表分为两大层次:
- 数据系统表 (
table_name$system_table
):附着于每张业务表,用于监控和调试该表本身。 - 全局系统表 (
sys.system_table
):存在于sys
系统库中,用于监控和管理整个 Catalog 或集群。
核心价值:
- 运维可视化:无需登录底层存储(如 HDFS),直接通过 SQL 即可洞察数据湖的内部状态。
- 问题诊断:快速定位数据倾斜、小文件、快照膨胀等问题。
- 元数据管理:统一管理标签(Tags)、分支(Branches)、消费者偏移量等。
- 跨引擎兼容:Flink、Spark、Trino 等引擎均可查询,提供了统一的运维视角。
二、核心系统表功能解析
表名 | 核心功能 | 生产应用场景 |
---|---|---|
$snapshots | 快照历史追踪:查看每次提交的详细信息(时间、记录数、水印)。 | 审计数据变更历史,定位数据延迟,进行时间旅行。 |
$schemas | Schema 演变历史:记录表结构(字段、分区、主键)的变更过程。 | 数据血缘分析,排查因 Schema 变更导致的数据兼容性问题。 |
$files | 文件级洞察:查看每个数据文件的详细统计信息(大小、记录数、最大值、最小值)。 | 诊断性能问题:发现小文件、数据倾斜(通过 min_key /max_key )、评估压缩效果。 |
$manifests | 元数据文件洞察:查看清单文件的内容,了解新增/删除的文件列表。 | 深入分析快照之间的差异,理解 LSM 树的压缩合并行为。 |
$audit_log & $binlog | 变更数据捕获 (CDC):以不同的格式查看行级的增量变更。 | 实现审计、同步到外部系统(如 ES)、构建物化视图。 |
$tags & $branches | 数据版本管理:管理基于快照的标签和分支。 | 数据版本化:为重要版本(如 ML 训练集)创建标签,或在分支上进行数据隔离开发。 |
$partitions & $buckets | 存储结构洞察:查看分区和分桶级别的聚合信息(记录数、文件数、大小)。 | 快速定位热点:找出数据量异常大的分区或分桶,优化存储布局。 |
$ro (Read-Optimized) | 查询加速:仅读取已压缩的顶层文件,牺牲一定 freshness 换取性能。 | 为对延迟不敏感的交互式查询提供加速服务。 |
sys.all_table_options | 全局配置管理:一键查看所有表的配置信息。 | 统一审计和治理表的相关参数设置。 |
三、设计哲学与关键技术
-
SQL 作为统一管理接口:
- 所有运维操作都可以通过熟悉的 SQL 完成,极大降低了数据湖的管理门槛。
- 示例:
SELECT * FROM my_table$files WHERE file_size_in_bytes < 1024 * 1024;
快速找出所有小文件。
-
深度可观测性集成:
- 系统表暴露了 LSM 树、快照、清单等核心内部机制的运行状态,使底层存储不再是一个“黑盒”。
- 像
$files
表中的min_value_stats
/max_value_stats
等信息,直接用于查询时的数据跳过(Data Skipping),关联了元数据与查询性能。
-
面向多引擎的生态位:
- 系统表的存在,使得任何能够连接 Paimon 的引擎(Flink/Spark/Trino)都同时获得了强大的运维能力,避免了依赖特定工具或 CLI。
-
功能强大的增量处理基础:
$audit_log
和$binlog
表提供了强大的 CDC 能力,这是构建实时数仓和同步链路的基础。$consumers
表记录了消费进度,是实现多消费者协同和断点续传的关键。
四、生产环境最佳实践
-
监控告警:
- 定期查询
$snapshots
监控提交频率和延迟。 - 定期查询
$partitions
监控分区数据量,防止产生过热分区。 - 定期查询
$files
监控小文件数量,触发自动压缩作业。
- 定期查询
-
故障排查:
- 数据不一致时,用
$audit_log
追溯变更记录。 - 查询性能下降时,用
$files
和$manifests
分析文件层级和统计信息。
- 数据不一致时,用
-
数据治理:
- 使用
$tags
为重要数据节点(如日报数据)打标签,便于管理和回溯。 - 使用
sys.all_table_options
审计所有表的配置是否符合规范。
- 使用
-
优化建议:
- 为高频使用的系统表查询(如查找小文件)创建物化视图,提升运维效率。
- 将重要的系统表查询(如每日分区增长量)集成到现有的监控平台(如 Grafana)中。
总结:Paimon 的系统表远不止于“查看元数据”,它是一个功能完备的运维和管理平台。它将数据湖的底层复杂性通过 SQL 接口抽象出来,让用户能够像管理传统数据库一样,轻松地管理大规模、多引擎的数据湖环境,真正实现了湖仓一体的可观测性和可管理性。
Paimon 数据类型
Paimon 的数据类型系统设计遵循了 ANSI SQL 标准,并与 Apache Flink 和主流计算引擎(如 Spark)保持高度兼容。其核心目标是提供一套统一、精确的类型系统,用于定义表结构并确保在批处理和流处理中的数据类型安全。
一、数据类型分类
Paimon 支持的数据类型可分为以下几大类:
类别 | 包含的数据类型 | 说明 |
---|---|---|
数值类型 | TINYINT , SMALLINT , INT , BIGINT , FLOAT , DOUBLE , DECIMAL(p, s) | 涵盖整数、浮点数和精确小数 |
字符/二进制类型 | CHAR(n) , VARCHAR(n) , STRING , BINARY(n) , VARBINARY(n) , BYTES | 存储文本和二进制数据 |
日期时间类型 | DATE , TIME(p) , TIMESTAMP(p) , TIMESTAMP(p) WITH TIME ZONE | 处理时间相关的数据,支持时区 |
布尔类型 | BOOLEAN | 逻辑值(TRUE, FALSE, UNKNOWN) |
复杂/嵌套类型 | ARRAY<t> , MAP<kt, vt> , MULTISET<t> , ROW<...> | 支持结构化数据的高级类型 |
二、核心特性与最佳实践
1. 精度与长度控制
- 字符类型:
CHAR(n)
和VARCHAR(n)
允许指定长度,这对于优化存储和确保数据质量至关重要。最佳实践是始终根据业务需求指定长度,避免使用无长度的默认定义。 - 时间类型:
TIME(p)
,TIMESTAMP(p)
支持指定小数秒的精度(0-9),默认为TIME(0)
和TIMESTAMP(6)
。根据实际精度需求设置,避免不必要的存储开销。 - 高精度计算:
DECIMAL(p, s)
用于金融等需要精确计算的场景,必须仔细定义精度(p)和标度(s)。
2. 语义化别名
STRING
是VARCHAR(2147483647)
的同义词,BYTES
是VARBINARY(2147483647)
的同义词。推荐使用STRING
和BYTES
以提高代码的可读性。
3. 复杂的嵌套数据类型
-
ARRAY<t>
:用于存储同质元素的列表。适用于标签、访问历史等场景。 -
MAP<kt, vt>
:用于存储键值对。适用于动态属性、配置参数等。 -
ROW<...>
:是定义嵌套结构的主力。强烈推荐为每个字段命名和注释,这能极大提升Schema的可读性和可维护性。-- 好的实践:字段有名字和注释 ROW<user_id BIGINT '用户唯一标识',address ROW<city VARCHAR,street VARCHAR> '用户住址' >
4. 时区处理
TIMESTAMP WITH TIME ZONE
类型用于处理跨时区的时间数据。它会将时间值转换为 UTC 存储,并在查询时根据会话时区显示。对于国际化应用,应优先选择此类型以避免时间歧义。
三、与湖仓一体架构的关联
- Schema Evolution(模式演进):Paimon 强大的数据类型系统是其支持无损 Schema 演进的基础。例如,可以安全地将
INT
类型字段扩展到BIGINT
。 - 跨引擎兼容性:这些类型定义是跨 Flink、Spark、Hive 和 Trino 的通用抽象层。在 Paimon 中定义的表结构可以被这些引擎正确理解,这是实现湖仓一体的关键。
- 数据质量:严格的数据类型有助于在数据入湖时进行初步的验证和清洗,保障数据质量。
四、生产环境设计建议
- 谨慎使用复杂类型:虽然
ARRAY
,MAP
,ROW
功能强大,但并非所有查询引擎都能高效处理。需评估查询模式再决定是否使用。 - 为时间字段选择正确的类型:
- 仅需日期 ->
DATE
- 时间戳,无需时区 ->
TIMESTAMP
- 时间戳,需跨时区 ->
TIMESTAMP WITH TIME ZONE
- 仅需日期 ->
- 避免过度使用最大长度:除非确实需要,否则不应将
VARCHAR
或VARBINARY
的长度定义为最大值 (2147483647
),这会影响优化器对内存使用的预估。 - 保持一致性:在整个数仓中,对同一含义的字段(如
user_id
)应使用相同的数据类型(如BIGINT
),以减少关联操作时的类型转换开销。
总结:Paimon 的数据类型系统是其作为企业级湖仓格式的基石。它既提供了标准化的基础类型以确保兼容性,又提供了强大的复杂类型以满足现代数据分析的灵活性和丰富性。在设计表结构时,充分利用这些类型的特性,可以为数据湖的可靠性、性能和可维护性奠定坚实基础。
Specification(规范)
在软件工程,特别是数据系统领域,Specification(规范,常简写为 Spec) 是一份权威性的技术文档,它明确定义了一个系统或组件的:
- 设计目标与范围:要解决什么问题,边界在哪里。
- 核心概念与术语:统一的语言,避免歧义(如 Paimon 定义的 Snapshot, Manifest 等)。
- 文件结构与布局:数据、元数据在存储介质(如 HDFS/S3)上如何组织。您提供的目录树就是其具体体现。
- 数据存储格式:数据文件(如 ORC)内部的编码方式。
- 读写协议:如何正确地读取和写入数据,包括并发控制、原子性保证等。
- 兼容性承诺:保证不同版本间的读写兼容性,确保系统可持续演进。
简单来说,Paimon 的 Specification 就是其作为一款表格式的“宪法”和“设计蓝图”。它确保了不同版本的 Paimon 以及各种计算引擎(Flink, Spark, Trino)能够依据同一套规则来理解和操作数据,从而实现互操作性和稳定性。
核心术语与磁盘布局的对应关系
官网提供的目录树完美地展示了 Paimon Spec 的核心术语如何映射到物理存储上。我们来逐一分解:
warehouse/ # Catalog 的仓库根目录
└── default.db/ # Database└── my_table/ # Table├── bucket-0/ # 分桶目录 (Bucket)│ └── data-xxx-0.orc # 数据文件 (Data File)├── index/ # 全局索引目录 (Global Index)│ └── index-xxx-0 # 索引文件├── manifest/ # 清单目录│ ├── index-manifest-xxx-0 # 清单索引│ ├── manifest-xxx-0 # 清单文件 (Manifest)│ └── manifest-list-xxx-0 # 清单列表 (Manifest List)├── schema/ # 表结构历史目录│ └── schema-0 # 表结构 (Schema)└── snapshot/ # 快照目录├── EARLIEST -> snapshot-1 # 最早快照符号链接├── LATEST -> snapshot-1 # 最新快照符号链接└── snapshot-1 # 快照文件 (Snapshot)
1. Schema (schema/schema-0
)
-
是什么:表的“结构定义”,包括字段名、类型、主键、分区键和表属性(options)。
-
磁盘体现:
schema-0
文件。如果表结构发生变更(Schema Evolution),会产生schema-1
,schema-2
等。
2. Snapshot (snapshot/snapshot-1
)
-
是什么:表的“时间点视图”。这是查询的入口点。每个成功的
INSERT
/UPDATE
都会生成一个新快照。 -
磁盘体现:JSON 文件,记录了该快照对应的
schema-id
、manifest-list
以及元数据(如记录数、水印)。 -
设计巧思:
LATEST
和EARLIEST
是符号链接,指向具体的快照文件。这使“查询最新数据”和“时间旅行”操作变得非常高效,无需扫描所有快照文件。
3. Manifest List (manifest/manifest-list-xxx-0
)
-
是什么:一个快照所引用的所有 Manifest 文件的列表。它是快照文件(Snapshot)直接指向的对象。
-
作用:将快照与大量的数据文件解耦。
4. Manifest (manifest/manifest-xxx-0
)
-
是什么:数据文件(Data File)的变更日志。它记录了在本次提交中,哪些文件被添加和哪些文件被删除。
-
类比:类似于 Git 中的一次
commit
,它记录了文件的增删。
5. Data File (bucket-0/data-xxx-0.orc
)
-
是什么:实际存储数据的文件。默认采用列式格式(ORC)。这些文件是不可变的(Immutable)。
-
组织方式:存储在分桶目录(
bucket-0/
)下,这是实现分布式处理和高效更新的关键。
6. Global Index (index/index-xxx-0
)
- 是什么:基于主键的索引,用于在包含多个分桶的表中快速定位一条记录存储在哪个桶里。这是实现高性能
UPDATE
和DELETE
的关键。
总结
-
原子性提交 (Atomic Commit):
- 通过 Snapshot -> Manifest List -> Manifest -> Data File 的链式引用,配合文件系统的原子重命名操作,保证了提交的原子性。读者永远只能看到一个完整的快照。
-
数据布局 (Data Layout):
- 分区 + 分桶:数据首先被分区,然后在分区内分桶。这既是数据分布策略,也是数据聚类(Clustering)策略,为查询优化(如裁剪、排序)奠定了基础。
-
增量处理优化:
- Manifest 机制 使得系统可以轻松地知道“哪些数据是新增的”,从而高效地支持流式读取和增量计算。
-
多引擎兼容的基石:
- 这份公开的 Spec 是 Flink、Spark、Trino 等引擎能够正确读写 Paimon 表的根本原因。任何引擎只要按照 Spec 实现,就能融入 Paimon 的生态。
Paimon 作为一款开源、开放的表格式,它不仅定义了数据如何存储,更定义了一套多引擎协作的“游戏规则”,这正是其能构建“湖仓一体”架构的底层根基。
Paimon Schema
Paimon 的 Schema 管理是其模式演进(Schema Evolution) 能力的核心,它通过一套精密的版本化、元数据分离和字段 ID 系统,实现了在数据湖场景下安全、灵活地表结构变更。
核心设计理念
- Schema 与数据解耦:Schema 信息被独立存储在
schema/
目录下的 JSON 文件中,与真实的数据文件(Data Files)分离。这种解耦是模式演进的基础。 - 版本化(Versioning):每次 Schema 变更都会生成一个新版本的文件(
schema-0
,schema-1
,schema-2
, ...),所有历史版本均被保留。 - 字段 ID 为核心:每个字段都有一个唯一且永久的数字 ID。这是实现向后兼容和安全演化的关键,它使系统能准确匹配新旧 Schema 中的字段,而无须依赖易变的字段名。
Schema 文件详解
1. 核心字段解析
字段名 | 类型 | 说明 |
---|---|---|
"id" | int | Schema 版本 ID。从 0 开始递增。 |
"fields" | Array<DataField> | 表的所有列定义。每个字段包含 id , name , type 。 |
"highestFieldId" | int | 已分配的最大字段 ID。确保新增字段的 ID 唯一性。 |
"partitionKeys" | Array<String> | 分区字段名列表。不可变更(需重写数据)。 |
"primaryKeys" | Array<String> | 主键字段名列表。不可变更(需重写数据)。 |
"options" | Map<String, String> | 表的属性配置(如 'bucket'='5' )。 |
"timeMillis" | long | Schema 创建的时间戳(用于审计)。 |
2. 字段 ID (id
) 的核心作用
- 唯一标识符:即使字段名被修改(
RENAME COLUMN
),其 ID 保持不变。系统通过 ID 而非名称来识别字段。 - 安全新增字段:新增字段会获得一个新的、更大的 ID(
highestFieldId + 1
),绝不会与现有字段 ID 冲突。 - 读取兼容性:旧数据文件中的字段通过其 ID 映射到新 Schema 的对应字段,从而实现无缝读取。
模式演进(Schema Evolution)工作流
1. 变更触发
用户执行 ALTER TABLE
语句(如新增列、重命名列、修改列类型)。
2. 新 Schema 生成
Paimon 会:
- 基于当前最新的 Schema 版本(如
schema-2
)创建一个副本。 - 应用用户请求的变更。
- 为新增字段分配新的字段 ID(更新
highestFieldId
)。 - 将新 Schema 保存为下一个版本(如
schema-3
)。
3. 快照关联
新产生的快照(Snapshot)会记录其对应的 Schema 版本 ID(如 "schema_id": 3
)。
4. 数据读取(演化读)
当查询某个快照时:
- 系统根据快照中的
schema_id
找到对应的 Schema 文件(如schema-3
)。 - 读取数据文件时,数据文件中的字段通过字段 ID 与 Schema 文件中的字段定义进行匹配。
- 对于新旧 Schema 的差异(如新增列),系统会自动处理:
- 新增列:新列在读取旧数据时自动填充
NULL
(或默认值)。 - 重命名:由于 ID 不变,数据自动映射到新列名。
- 类型变更:在支持的类型转换范围内自动完成转换。
- 新增列:新列在读取旧数据时自动填充
磁盘结构直观地反映了这一过程:
my_table/
└── schema/├── schema-0 # v0: 初始 Schema├── schema-1 # v1: 新增了一个列└── schema-2 # v2: 重命名了一个列
- 快照
snapshot-5
可能引用schema-0
。 - 快照
snapshot-10
可能引用schema-1
。 - 快照
snapshot-20
可能引用schema-2
。
不可变约束与最佳实践
-
不可变更的要素:
- 分区键 (
partitionKeys
):变更分区键需要重写整个表的数据。 - 主键 (
primaryKeys
):变更主键会影响所有的更新和查询行为,需要重写数据。
- 分区键 (
-
历史 Schema 文件的重要性:
- 绝不能随意删除旧 Schema 文件。因为旧的数据文件(
data-*.orc
)在写入时是参照其当时的 Schema 版本进行编码的。删除旧 Schema 文件会导致这些历史数据无法被正确读取。 - 旧 Schema 文件是实现时间旅行(Time Travel) 功能的基础。要查询历史快照的数据,必须使用其创建时的 Schema。
- 绝不能随意删除旧 Schema 文件。因为旧的数据文件(
-
兼容性提示:
- 文档中提到的
version 1
和version 2
的兼容性处理,体现了 Paimon 在早期版本迭代中对用户的无感升级,确保了新旧版本的平滑过渡。
- 文档中提到的
总结
Paimon 的 Schema 管理机制是其作为一款企业级湖仓格式的稳健性和灵活性的体现。它通过:
- 字段 ID 系统:实现了真正的、安全的 Schema 演进。
- 版本化与快照关联:将表结构变更像数据变更一样纳入版本管理,支持完整的审计和回溯。
- 元数据与数据分离:解耦了结构定义和存储内容,为各种演化操作提供了可能。
这套机制使得用户能够像在传统数据库中一样,随时根据业务需求调整表结构,而无需担心破坏现有数据或应用,真正做到了“模式演进,数据无忧”。
Paimon Snapshot(快照)机制
Snapshot 是 Paimon 表格式的核心原子,它定义了表的时间点状态(Point-in-Time View),是所有读写操作的统一入口和事务性保证的基石。它完美融合了数据湖的存储规模与数据库的事务特性。核心设计理念
- 原子性视图(Atomic View):每个 Snapshot 代表表在某个提交成功后的完整、一致的状态。查询某个 Snapshot,看到的就是该时间点下表的全量数据。
- 增量链式结构(Incremental Chaining):Snapshot 通过引用 Manifest List 来组织数据文件,这种设计使得增量读取和时间旅行变得非常高效。
- 多版本并发控制(MVCC):多个 Snapshot 并存,使得读写可以分离。写入器创建新 Snapshot 时,读取器可以继续访问旧的 Snapshot,实现了无锁的并发控制。
Snapshot 文件详解
1. 磁盘布局与 Hint 文件
my_table/snapshot/├── EARLIEST -> snapshot-1 # 符号链接,指向最早的有效快照(可能不准确)├── LATEST -> snapshot-3 # 符号链接,指向最新的成功快照(可能不准确)├── snapshot-1 # 快照版本 1├── snapshot-2 # 快照版本 2└── snapshot-3 # 快照版本 3
-
EARLIEST
/LATEST
:是符号链接(Symbolic Links),作为性能优化。它们提供了快速访问路径,但可能在并发读写时短暂失效(不准确)。系统有回退机制,会扫描所有 Snapshot 文件来确定真正的起止点。 - 连续 ID:Snapshot ID 从 1 开始严格连续递增。这简化了版本管理和增量扫描的逻辑。
2. 核心字段解析(JSON 内容)
字段名 | 说明 |
---|---|
id , schemaId | Snapshot 自身 ID 和其对应的 Schema 版本。建立了数据与结构的版本映射。 |
baseManifestList | 基础清单列表,记录了此前所有 Snapshot 累积的数据文件变更。这是实现增量读取的关键。 |
deltaManifestList | 增量清单列表,记录了本次 Snapshot 提交中新产生的数据文件变更。 |
changelogManifestList | 变更日志清单列表,记录了由 changelog-producer 生成的 CDC 数据。用于流式增量消费。 |
commitUser , commitIdentifier | 流式写入的恢复元数据。commitUser 标识写入作业,commitIdentifier 是 Flink 的 Checkpoint ID。作业重启后依靠它们实现精确一次(Exactly-Once) 语义。 |
commitKind | 提交类型(APPEND, COMPACT, OVERWRITE)。解释了本次 Snapshot 产生的原因。 |
watermark | 事件时间水印。从流式写入(如 Flink)中继承而来,用于支持基于事件时间的查询优化。 |
totalRecordCount , deltaRecordCount | 记录数统计。用于监控和数据质量校验。 |
工作流与核心功能实现
1. 提交过程(Commit)
- 抢占 ID:写入器预先抢占下一个连续的 Snapshot ID(如
snapshot-4
)。 - 构建清单:生成本次写入的
deltaManifestList
(新增了哪些文件,删除了哪些文件)。 - 原子提交:将新 Snapshot 文件(
snapshot-4
)原子性地写入snapshot/
目录。 - 更新指针:最后更新
LATEST
符号链接,使其指向snapshot-4
。- 关键:这个顺序确保了在任何时刻,读者要么看到旧状态,要么看到完整的新状态,绝不会看到中间状态。
2. 核心功能实现
- 时间旅行(Time Travel):
- 用户指定一个时间戳或 Snapshot ID。
- 系统找到小于等于该时间点的最大 Snapshot ID。
- 读取该 Snapshot 所引用的所有数据文件,即可看到历史状态。
- 增量读取(Incremental Read):
- 用户指定起始和结束 Snapshot ID(如
from snapshot-2 to snapshot-4
)。 - 系统计算两个 Snapshot 的
deltaManifestList
差异,仅读取在期间新增的数据文件。
- 用户指定起始和结束 Snapshot ID(如
- 流式消费(Streaming Consumption):
- 消费者记录已消费的 Snapshot ID。
- 下次启动时,从
next_snapshot_id
开始读取,通过changelogManifestList
获取精确的行级变更(+I
,-U
,+U
,-D
)。
生产环境启示
-
Snapshot 管理:
- Snapshot 会累积,需要配置过期策略(如
'snapshot.time-retained' = '72 h'
)自动清理旧快照,释放存储空间。 -
COMPACT
类型的 Snapshot 由压缩作业产生,旨在合并小文件,优化查询性能。
- Snapshot 会累积,需要配置过期策略(如
-
流式作业恢复:
commitUser
和commitIdentifier
是实现故障恢复和精确一次处理的核心。切勿手动修改或删除这些信息。
-
监控:
- 监控 Snapshot 的增长频率和大小,可以洞察数据摄入的健康状态。
- 关注
deltaRecordCount
可以了解每次提交的数据量。
-
性能提示:
- 虽然
EARLIEST
/LATEST
hint 文件能加速访问,但在极高并发场景下,可能会遇到 hint 未及时更新的情况,这是正常现象,系统有兼容逻辑。
- 虽然
总结
Paimon 的 Snapshot 机制是其流批一体和湖仓一体能力的技术心脏。它通过一个简单的原子文件,巧妙地实现了:
- ACID 事务:保证多并发读写的一致性。
- 多版本管理:支持时间旅行和历史回溯。
- 增量处理:为流计算提供高效的增量数据源。
- 流式恢复:保障流作业故障后的精确一次语义。
这种设计使得 Paimon 表不仅是一个静态的数据存储,更是一个动态的、带有时空版本信息的数据实体,为现代数据架构提供了强大的基石。
Paimon Manifest
Manifest(清单)系统是 Paimon 表格式的索引引擎和变更日志,它在Snapshot(快照) 和Data Files(数据文件) 之间构建了一座桥梁。其核心设计目标是:高效地将快照的宏观版本映射到海量微观数据文件的精确变更集上,同时为查询优化提供丰富的统计信息。
核心架构:分层索引
Paimon 通过两层清单结构来组织元数据,这是一种经典的“索引的索引”设计,极大地提升了可扩展性。
-
Manifest List(清单列表)
- 定位:
manifest-list-{UUID}-N
- 角色:快照的目录。一个 Snapshot 文件直接指向一个 Manifest List 文件。
- 内容:记录了一批 Manifest 文件的元信息(路径、大小、统计信息等)。
- 价值:使得快照可以轻量地引用大量文件变更。查询时,可先读取 Manifest List,利用其中的分区统计信息(
_PARTITION_STATS
)进行快速过滤,跳过完全不相关的 Manifest 文件。
- 定位:
-
Manifest File(清单文件)
- 定位:
manifest-{UUID}-N
或index-manifest-{UUID}-N
- 角色:数据文件的目录。一个 Manifest 文件包含一批数据文件或索引文件的详细变更记录。
- 价值:记录了文件级别的增删(
_KIND
)和详细统计信息,为后续的数据跳过(Data Skipping)提供依据。
- 定位:
这种分层结构将全局扫描的复杂度从 O(N)(N 为数据文件数)降低到接近 O(1),因为只需要先扫描少量的 Manifest List 和 Manifest 文件即可定位到所需的数据文件。
Manifest 的类型与功能
1. Data Manifest(数据清单)
- 内容:记录数据文件(
data-*.orc
)和变更日志文件的增删操作。 - 核心字段:
_KIND
:操作类型(ADD / DELETE),这是实现 LSM 树 compaction 和增量处理的基础。_PARTITION
,_BUCKET
:文件的位置信息,用于快速定位。_FILE
:文件的丰富元数据,这是数据跳过(Data Skipping) 的性能关键。_MIN_KEY
/_MAX_KEY
,_KEY_STATS
:基于主键的范围,用于快速定位文件。_VALUE_STATS
:非主键列的统计信息(min, max, null counts),用于过滤。_LEVEL
:LSM 树的层级,用于控制 Compaction 策略。
- 工作流:一次提交(Commit)会生成一个 Data Manifest,记录本次新增和删除的文件。Compaction 作业会生成 DELETE 条目来逻辑删除旧文件,并 ADD 新合并后的文件。
2. Index Manifest(索引清单)
- 内容:专门记录全局索引文件(
index-*
)的增删操作。 - 核心字段:
_INDEX_TYPE
:标识索引类型(如HASH
或DELETION_VECTORS
)。_DELETION_VECTORS_RANGES
:这是 Deletion Vectors 技术的核心元数据。它不再需要重写整个数据文件来标记删除,而是在独立索引文件中记录删除位图,极大提升了删除操作的效率。
- 价值:将索引文件的生命周期管理也纳入到 Snapshot 体系中,保证了索引与数据的原子一致性。
核心特性与设计哲学
1. 不可变性与追加写
-
Manifest 和 Data Files 都是不可变的。任何变更(增删文件)都通过追加新的 Manifest 条目来实现。
-
优势:简化了并发控制,读者无需加锁即可获得一致的视图。
2. 高效的统计信息驱动
-
从 Manifest List 的
_PARTITION_STATS
到 Data Manifest 中每个文件的_KEY_STATS
/_VALUE_STATS
,Paimon 在每一层都嵌入了丰富的统计信息。 -
价值:在真正读取数据文件之前,就可以在元数据层面进行多层过滤,大幅减少 I/O。这是数据湖查询性能优化的关键。
3. 支持高级特性
-
Deletion Vectors:通过 Index Manifest 管理,实现了高效的随机删除(Random Delete),避免了重写整个文件,是湖仓一体走向实时化的关键技术。
-
Embedded File Index:
_EMBEDDED_FILE_INDEX
字段允许将小索引直接内联在 Manifest 中,避免了小文件问题,优化了读取性能。
4. 二进制优化
- 使用 BinaryRow 格式存储统计信息(如
_MIN_VALUES
)。这是一种紧凑的、基于字节的格式,序列化/反序列化效率极高,避免了 Java 对象的开销,非常适合存储和传输大量的统计元数据。
生产环境启示
-
Manifest 的压缩:随着频繁的 Commit 和 Compaction,Manifest 文件也会增长。Paimon 有后台机制会合并多个小的 Manifest 文件,以优化元数据的读取性能。
-
监控:关注 Manifest 文件的数量和大小,可以间接反映表的更新频率和 Compaction 的健康状态。
-
性能调优:查询性能极大依赖于统计信息的准确性。确保 Compaction 作业正常运行,可以使统计信息(min/max values)保持紧凑,从而提升数据跳过的效率。
总结:Paimon 的 Manifest 系统远不止是一个“文件列表”,它是一个高度优化、信息丰富、分层管理的分布式元数据索引引擎。它通过精巧的设计,将快照的原子性与海量数据文件的管理能力结合起来,同时为查询优化提供了强大的统计支持,是 Paimon 实现高性能湖仓一体架构的幕后功臣。
Paimon DataFile、分区分桶
Paimon 的数据组织架构是其实现高性能、可扩展性和丰富语义的核心。它通过分区(Partitioning)、分桶(Bucketing) 和数据文件(Data File)内部编码的三层设计,来满足从批量ETL到高并发点查的各种场景需求。
数据布局:分区与分桶
Paimon 采用与 Hive 兼容的分区概念,并在此基础上引入了更灵活的分桶策略。
1. 分区(Partitioning)
- 定义:按照指定的列(如日期
dt
、地区region
)将数据划分到不同的物理目录。 - 磁盘体现:
part_t/dt=20240514/
- 价值:
- 查询加速:通过分区裁剪(Partition Pruning),查询可以跳过大量不相关的数据目录。
- 数据管理:方便地对特定分区进行生命周期管理(如删除过期数据)。
- 约束:分区字段是不可变更的,定义后需谨慎选择。
2. 分桶(Bucketing)
- 定义:在分区内(或无分区表),根据分桶键(Bucket Key) 的哈希值将数据分散到固定数量的桶中。
- 磁盘体现:
part_t/dt=20240514/bucket-0/
- 配置:通过
'bucket' = 'N'
表属性设置。 - 工作模式:
表类型 | bucket = -1 (动态分桶) | bucket = N (固定分桶) |
---|---|---|
主键表 | 默认模式。使用全局索引来定位记录所在的桶。适合高基数主键和频繁更新的场景。 | 根据分桶键(默认为主键)的哈希值分桶。适合大规模批量写入和有序扫描。 |
无主键表 | 默认模式。忽略分桶,所有数据写入 bucket-0 ,但读写并行度不受限。 | 必须指定分桶键。根据分桶键哈希值分桶。适合大规模、仅追加的数据。 |
总结:分区用于粗粒度的数据划分,分桶用于细粒度的数据分布和并行度控制。
数据文件(Data File)的内部编码
数据文件是最终存储数据的载体,Paimon 在此层面针对不同表类型进行了深度优化。
1. 无主键表(Append Table)
- 文件内容:仅包含用户定义的列(
a, b, c
)。 - 格式:标准的列式存储(ORC/Parquet)。
- 特点:简单、高效,适用于批量写入和全表扫描。
2. 主键表(Primary Key Table)
主键表的数据文件为了支持更新和删除,存储了丰富的系统元数据列:
系统列 | 数据类型 | 说明 |
---|---|---|
_VALUE_KIND | TINYINT | 行状态标识(+I 插入, -D 删除, -U /+U 更新)。这是实现更新语义的核心。 |
_SEQUENCE_NUMBER | BIGINT | 序列号。用于在合并时解决冲突,值越大代表数据越新。保障了更新的正确性。 |
_KEY_* | (与主键同类型) | 主键列的重命名。用于避免与值列冲突(在非瘦身模式下)。 |
示例剖析:
对于表 T (a INT PRIMARY KEY, b INT, c INT)
,其数据文件在非瘦身模式下包含:
_KEY_a
(INT)_VALUE_KIND
(TINYINT)_SEQUENCE_NUMBER
(BIGINT)a
(INT) // 值列b
(INT) // 值列c
(INT) // 值列
瘦身模式(data-file.thin-mode
):
- 优化:通过移除
_KEY_*
列来减少存储开销和 I/O。 - 前提:要求主键字段也必须包含在值列中(如示例中的
a
)。 - 效果:文件只有
_VALUE_KIND
,_SEQUENCE_NUMBER
,a
,b
,c
五列。
早期版本的 Paimon 依赖
_KEY_*
列来定位主键,新版本(如 1.0+)可以从值列中提取主键,但保留_KEY_*
列确保向后兼容。
变更日志文件(Changelog File)
- 本质:变更日志文件在物理格式上与数据文件完全相同。
- 目的:它逻辑上记录了一个时间段内行级别的增量变更(
+I
,-D
,-U
,+U
)。 - 生产者:由
changelog-producer
(如'input'
或'lookup'
)在写入时生成。 - 消费者:用于下游的增量计算和CDC同步,是构建实时数仓流水线的关键。
设计哲学与生产启示
-
灵活性 vs. 性能:
- 动态分桶 (
bucket = -1
):灵活性高,适应主键随意分布,但需要维护全局索引,有一定开销。 - 固定分桶 (
bucket = N
):性能更优,扫描效率高,但要求数据分布均匀,否则易产生倾斜。
- 动态分桶 (
-
存储开销 vs. 功能丰富性:
- 瘦身模式:牺牲了少量清晰度(
_KEY_*
列),换取了显著的存储和I/O效率提升,生产环境推荐开启。 - 系统列:
_VALUE_KIND
和_SEQUENCE_NUMBER
是实现 LSM 树更新机制的成本,但带来了强大的功能。
- 瘦身模式:牺牲了少量清晰度(
-
统一的物理格式:
- 数据文件和变更日志文件格式统一,简化了底层处理逻辑,使流批处理可以共用同一套存储层。
总结:Paimon 通过分区、分桶和数据文件内部编码的协同设计,在数据湖的存储框架内实现了数据库级的核心功能(主键更新、增量消费)。这种设计使其能够在一个系统中同时高效地处理批量历史数据和实时流水数据,真正支撑起湖仓一体的架构愿景。用户可以根据业务场景(点查、批量分析、更新频率)灵活配置分区和分桶策略,以达到最佳性能。
Paimon 表索引机制
Paimon 的表索引系统是其实现高效主键查询和动态更新的核心组件,主要包括动态分桶索引和删除向量两大核心机制。
一、动态分桶索引(Dynamic Bucket Index)
当配置 bucket = -1
(动态分桶模式)时,Paimon 的分桶机制与传统分桶(如Hive分桶)有根本性差异:
特性 | 传统分桶(bucket = N ) | 动态分桶(bucket = -1 ) |
---|---|---|
分桶数量 | 固定(创建表时指定) | 动态增长(随数据量自动调整) |
数据分布 | hash(key) % N 固定到某桶 | 通过全局索引自由定位到任意桶 |
扩容代价 | 需重写所有数据 | 无需数据迁移 |
适用场景 | 数据分布均匀的批量加载 | 主键随机分布的高频写入/更新 |
动态性的真正体现
-
桶数量的动态性
初始时可能只有少量桶(如1个),随着数据增长:- 当单个桶文件过大时,自动分裂为更多桶
- 通过后台Compaction平衡桶的大小
- 无需用户干预,完全由系统管理
-
索引的全局性
动态分桶模式下,Dynamic Bucket Index
是一个全局的哈希映射表:- 记录 主键哈希 -> 当前所在桶 的映射关系
- 当桶分裂时,只需更新索引,不移动原始数据
HASH_VALUE | HASH_VALUE |...
描述的是索引文件的物理存储格式,但其逻辑含义是:
[文件偏移量] 0x0000: HASH_VALUE_1 → 指向 Bucket X
[文件偏移量] 0x0004: HASH_VALUE_2 → 指向 Bucket Y
[文件偏移量] 0x0008: HASH_VALUE_3 → 指向 Bucket Z
...
- 每个
HASH_VALUE
实际是一个 复合结构:- 前4字节:主键哈希值(如MurmurHash结果)
- 隐含信息:当前版本通过额外的元数据(如Manifest)记录桶位置
写入流程:
- 计算主键哈希
H = hash(pk)
- 查询索引:
- 若
H
已存在 → 获取目标桶号 - 若
H
不存在 → 选择负载最轻的桶(或新建桶)
- 若
- 追加数据到目标桶
- 更新索引:记录
H → 桶
的映射
查询流程:
- 计算
H = hash(pk)
- 查询索引获取桶号
- 直接读取对应桶的数据文件
为什么需要这样的设计?
1. 解决传统分桶的痛点
- 热点问题:固定分桶时,某些高频主键会导致单个桶过热
- 扩容困难:增加桶数需全表重写
- 空间浪费:预分配过多空桶
2. LSM树的天然适配
- 动态分桶 + 全局索引的组合,完美匹配LSM树的追加写特性:
- 新数据总是写到新文件(新桶)
- 通过Compaction逐步合并旧桶
- 索引只需记录最新映射
生产配置示例
-- 创建动态分桶表
CREATE TABLE dynamic_table (user_id BIGINT PRIMARY KEY NOT ENFORCED,user_name STRING
) WITH ('bucket' = '-1', -- 启用动态分桶'dynamic-bucket.target-row-num' = '2_000_000', -- 触发桶分裂的阈值'dynamic-bucket.initial-buckets' = '4' -- 初始桶数
);-- 固定分桶对比
CREATE TABLE fixed_bucket_table (user_id BIGINT PRIMARY KEY NOT ENFORCED,user_name STRING
) WITH ('bucket' = '16', -- 固定16个桶'bucket-key' = 'user_id'
);
二、删除向量(Deletion Vectors)
设计目标:实现高效删除操作,避免重写整个数据文件。
核心特性:
-
二进制存储格式
- 版本标识(1字节)
- 序列化数据块(含大小和校验和)
- 魔法数字校验(1581511376)
- RoaringBitmap位图数据
-
位图技术:
- 使用RoaringBitmap压缩位图
- 支持快速定位被删除的记录
- 典型空间节省:1百万记录删除标记仅需约125KB
工作流程:
-
删除操作:
- 定位记录所在数据文件
- 在对应分桶的删除向量中设置位标记
- 异步 compaction 时物理清除
-
查询时:
- 检查删除向量位图
- 动态过滤被标记删除的记录
优化手段:
- 校验机制:通过校验和保障数据完整性
- 版本控制:支持格式演进
- 压缩存储:RoaringBitmap的高效压缩
典型场景:
-- 删除操作转化为标记
DELETE FROM orders WHERE status = 'cancelled';
三、联合工作机制
-
更新场景:
- 通过动态索引定位记录
- 在删除向量中标记旧记录
- 追加写入新记录到对应分桶
-
Compaction过程:
- 合并数据文件时过滤被删除记录
- 重建更紧凑的索引文件
- 生成新的删除向量
四、生产环境启示
-
索引调优:
- 监控索引文件大小增长
- 定期执行
COMPACT
命令优化索引
-
删除策略:
- 设置
'deletion-vector.expire-time'
自动清理 - 避免单个分桶删除标记过多
- 设置
-
性能监控:
- 关注点查延迟指标
- 跟踪删除向量的内存占用
配置示例:
CREATE TABLE orders (order_id BIGINT PRIMARY KEY,item_id BIGINT,quantity INT
) WITH ('bucket' = '-1', -- 启用动态分桶'deletion-vector.enabled' = 'true','index.compact-interval' = '12h'
);
总结
Paimon的索引系统通过:
- 动态分桶索引实现O(1)复杂度主键定位
- 删除向量实现低成本删除操作
- LSM树结构保证高效的合并操作
这种设计使得Paimon在保持数据湖扩展性的同时,获得了接近传统数据库的点查性能,非常适合需要频繁键值操作的数据湖场景。
Paimon 文件索引系统
Paimon 的文件索引系统是其实现高效查询的关键组件,通过多种索引类型的组合,为不同查询模式提供针对性的加速能力。
Paimon 采用分层索引设计,包含三个核心层级:
-
全局索引(表级别)
- 动态分桶索引(Dynamic Bucket Index)
- 删除向量(Deletion Vectors)
-
文件索引(文件级别)
- 每个数据文件配套的独立索引文件
- 支持多种索引类型混合存储
-
内联索引(Manifest 内嵌)
- 小索引直接嵌入 Manifest 文件
- 减少小文件问题
文件索引格式解析
所有文件索引共享统一的容器格式:
[HEADER]Magic Number (8B) → 1493475289347502LVersion (4B)Header长度 (4B) 列数量 (4B)[列1元数据]列名 (2B+Modified UTF-8)索引数量 (4B)[索引1信息]索引类型 (2B)起始位置 (4B)长度 (4B)[索引2信息]...[列2元数据]...[冗余字段] (向前兼容)[BODY]索引数据块连续存储
设计特点:
- BIT_ENDIAN编码:统一字节序保证跨平台兼容
- 自描述结构:Header包含完整元数据
- 空间预分配:通过冗余字段支持未来扩展
索引类型详解
Paimon 布隆过滤器索引
Paimon 位图索引和BSI解析
对布隆过滤器和BSI、位图进行更详细的说明
1. 布隆过滤器索引 (BloomFilter)
配置参数:file-index.bloom-filter.columns
存储结构:
[哈希函数数量 (4B)]
[位数组字节]
特性:
- 采用64位长哈希
- 字符串使用XXHash,数值类型专用哈希
- 假阳性率:约1%(典型配置)
适用场景:
-- 高选择性点查
SELECT * FROM table WHERE uuid = 'a1b2c3';
2. 位图索引 (Bitmap)
配置参数:file-index.bitmap.columns
存储结构:
V1格式:
[版本号 (1B)]
[总行数 (4B)]
[非空值位图数 (4B)]
[是否存在空值 (1B)]
[空值偏移量 (可选4B)]
[值1+偏移1]
[值2+偏移2]...
[序列化位图数据]
特性:
- 使用RoaringBitmap压缩
- 支持NULL值标记
- 偏移量负值表示单值优化
适用场景:
-- 低基数列过滤
SELECT * FROM logs WHERE level IN ('ERROR', 'WARN');
3. 位切片索引 (BSI)
配置参数:file-index.bsi.columns
存储结构:
V1格式:
[版本号 (1B)]
[行数 (4B)]
[是否存在正值 (1B)]
[正数BSI数据]
[是否存在负值 (1B)]
[负数BSI数据]BSI数据块:
[版本号 (1B)]
[最小值 (8B)]
[最大值 (8B)]
[存在位图]
[位切片数量 (4B)]
[位图0]
[位图1]...
支持类型:
- 整型家族(TINYINT到BIGINT)
- 时间类型(DATE/TIMESTAMP)
- 精确小数(DECIMAL)
适用场景:
-- 数值范围查询
SELECT * FROM sales WHERE amount BETWEEN 1000 AND 5000;
索引选择策略
索引类型 | 适用列特征 | 典型加速场景 | 存储开销 |
---|---|---|---|
BloomFilter | 高基数主键列 | 点查、JOIN键 | 中等 |
Bitmap | 低基数枚举列 | 多值过滤、分类统计 | 低 |
BSI | 数值/时间范围列 | 范围查询、聚合 | 高 |
生产建议:
- 主键列默认启用BloomFilter
- 状态类字段(如
status
)配置Bitmap - 数值指标列(如
price
)配置BSI - 通过
COMPACT
命令定期优化索引
配置示例:
CREATE TABLE optimized_table (user_id BIGINT,region STRING,purchase_amount DECIMAL(10,2),PRIMARY KEY (user_id) NOT ENFORCED
) WITH ('file-index.bloom-filter.columns' = 'user_id','file-index.bitmap.columns' = 'region','file-index.bsi.columns' = 'purchase_amount'
);
设计哲学解析
-
存储计算分离:
- 索引与数据文件分离存储
- 支持按需加载索引
-
自适应结构:
- 小索引内联,大索引独立
- 通过Manifest统一管理
-
类型专业化:
- 不同索引类型针对不同查询模式优化
- 避免"一刀切"的性能妥协
这种设计使得Paimon能在数据湖的存储规模下,提供接近专业分析数据库的查询性能,同时保持对硬件资源的弹性适应能力。
Paimon 主键表
Paimon 的主键表通过结合 LSM 树结构、灵活的分桶策略和高效的索引机制,在数据湖的存储框架内实现了数据库级的 CRUD 操作和高性能查询。
一、核心架构:分桶 + LSM 树
Paimon 主键表的物理存储是两级分层结构:
- 分区(Partition) (可选):按照业务日期(如
dt
)等进行粗粒度数据划分。 - 分桶(Bucket):在每个分区内,根据分桶键的哈希值进行细粒度数据分布。每个桶都是一个独立的 LSM 树。
1. 分桶(Bucketing)策略
Paimon 提供了两种分桶模式,适应不同的场景:
特性 | 固定分桶 (bucket = N ) | 动态分桶 (bucket = -1 ) |
---|---|---|
机制 | hash(key) % N 固定映射 | 全局索引动态映射,桶数可增长 |
并行度 | 固定,由 N 决定 | 可扩展,随数据量增长 |
适用场景 | 批量写入,数据分布均匀 | 高频更新,主键随机到达 |
扩容 | 需重写数据(RESCALE ) | 自动分裂,无需数据迁移 |
内存开销 | 低 | 需维护全局索引(HASH) |
生产建议:
- 数据量可预估、分布均匀 → 固定分桶
- 主键随机、需频繁更新和扩容 → 动态分桶
2. LSM 树(Log-Structured Merge-Tree)
每个桶目录内都是一个独立的 LSM 树,其核心概念是排序运行(Sorted Run):
- 排序运行(Sorted Run):由一个或多个数据文件组成,单个排序运行内的文件主键范围不重叠。
- 多个排序运行:不同排序运行的主键范围可以重叠。查询时需要合并所有排序运行,并按序列号(Sequence Number) 合并相同主键的记录。
- 写入过程:
- 数据先写入内存缓冲区(MemTable)。
- 缓冲区满后,排序并刷新到磁盘,形成一个新的排序运行(Level 0)。
- 后台Compaction job 会合并多个排序运行,形成更高层级、更紧凑的排序运行。
价值:LSM 树将随机写转换为顺序写,极大提升了写入吞吐量,是支持高频更新的基础。
二、动态分桶的两种模式及其深远影响
动态分桶根据主键是否包含所有分区字段,分为两种模式,其行为和性能差异巨大:
1. 普通模式(主键包含全部分区字段)
- 场景:主键包含分区字段,如
PRIMARY KEY (id, dt)
+PARTITIONED BY (dt)
。 - 机制:使用 HASH 索引(内存)维护
主键 -> 桶
的映射。 - 性能:
- 优:性能接近固定分桶,仅额外消耗索引内存(约 1GB/1亿键)。
- 支持排序压缩:可加速查询。
- 要求:仅支持单写入作业,否则会导致数据重复。
2. 跨分区更新模式(主键不包含全部分区字段)
- 场景:主键不包含分区字段,如
PRIMARY KEY (id)
+PARTITIONED BY (dt)
。需要跨分区更新(如某id
的dt
从昨天变到今天)。 - 机制:使用 RocksDB(磁盘)维护
主键 -> (分区, 桶)
的完整映射。启动时需全量初始化索引。 - 性能与挑战:
- 劣:性能损耗显著。初始化慢,持续写入后 RocksDB 可能膨胀,性能逐渐下降。
- 解决方案:配置
'cross-partition-upsert.index-ttl'
为索引设置 TTL,避免无限膨胀,但可能引入重复数据风险。 - 合并引擎行为各异:
Deduplicate
:先删旧分区数据,再插新分区数据。PartialUpdate
:仍在旧分区更新。FirstRow
:忽略新数据。
核心结论:极力推荐让主键包含分区字段,避免使用跨分区更新模式。
指南给出了至关重要的分区字段选型策略,优先级从高到低:
-
创建时间(Creation Time) (推荐)
- 原因: immutable(不可变),绝对安全,可放心加入主键。
- 示例:数据入湖时间
process_time
。
-
事件时间(Event Time)
- 原因:CDC 数据或 Paimon 变更日志包含完整的
UPDATE_BEFORE
记录,配合'changelog-producer'='input'
仍可保证主键唯一。 - 示例:业务发生时间
event_time
。
- 原因:CDC 数据或 Paimon 变更日志包含完整的
-
CDC 操作时间(CDC op_ts) (不推荐)
- 原因:无法知晓前一条记录的时间戳,必须启用跨分区更新模式,资源消耗大,性能差。
设计哲学与生产启示
-
存储计算协同:
- 分桶数决定了读写并行度的上限,是最重要的性能调优参数之一。
- LSM 树结构天然适合异步 Compaction,将写放大(Write Amplification)的影响降到最低。
-
权衡的艺术:
- 固定 vs 动态分桶:在并行度、小文件、扩容灵活性间权衡。
- 内存 vs 磁盘索引:在查询性能、更新能力和资源开销间权衡。
- 索引 TTL:在历史数据查询正确性和索引性能/容量间权衡。
-
模式设计是性能的第一步:
- 最重要的决策:确保主键包含分区字段,这将避免巨大的性能陷阱。
- 根据数据到达模式和查询模式选择最合适的分桶策略。
主键表模式 MOR vs. COW vs. MOW
见 Paimon 删除向量和MOW
Paimon 主键表通过三种不同的模式,在写入性能(Write Performance) 和读取性能(Read Performance) 之间提供了灵活的权衡选择,以适应不同的业务场景。
特性 | MOR (Merge-On-Read) | COW (Copy-On-Write) | MOW (Merge-On-Write) |
---|---|---|---|
默认 | ✅ | ❌ | ❌ |
配置 | 默认,无需额外配置 | 'full-compaction.delta-commits' = '1' | 'deletion-vectors.enabled' = 'true' |
写入机制 | 写入 L0,异步 Compaction | 每次写入触发全量合并 | 写入时查询 LSM 并生成删除向量 |
读取机制 | 需合并所有文件(多路归并) | 直接读取最高层文件 | 读取时用删除向量过滤,无需归并 |
写入性能 | 极佳 (低延迟) | 极差 (高延迟, 写放大严重) | 佳 (中等延迟) |
读取性能 | 较差 (需合并开销) | 极佳 (无合并开销) | 极佳 (无合并开销) |
数据可见性 | 实时 | 实时 | 默认同步压缩:实时 异步压缩:有延迟 |
适用场景 | 写多读少,实时写入 | 读多写少,批量作业 | 读写均衡,需要高性能点查 |
模式深度解析
1. MOR (Merge-On-Read) - 默认模式
- 设计哲学:写入优先。最大化写入吞吐,将数据合并的计算开销推迟到读取时。
- 工作流:
- 写:数据快速写入内存(MemTable),刷盘后形成 L0 文件。Compaction 在后台异步进行。
- 读:需要合并(Merge) 多个层级的文件,对相同主键的记录进行排序和合并,最后返回最新值。
- 优势:极高的写入吞吐,适合实时数据摄入。
- 劣势:
- 读取延迟高:合并过程消耗 CPU 和内存。
- 读取并行度受限:单个桶(一个 LSM 树)的读取难以并行化。
- 无法跳数:非主键列的过滤下推可能失效,因为新数据可能覆盖旧值。
2. COW (Copy-On-Write) - 全量压缩模式
ALTER TABLE orders SET ('full-compaction.delta-commits' = '1');
- 设计哲学:读取优先。在写入时完成所有合并工作,保证读取时最高性能。
- 工作流:
- 写:每次提交(Checkpoint)都会触发一次全量 Compaction,将所有数据合并到最高层,产生全新的文件。
- 读:直接读取最新的、已完全合并的文件,无需任何计算。
- 优势:最佳的读取性能。
- 劣势:
- 写入延迟极高:写放大(Write Amplification)非常严重,一次小的更新可能导致重写整个表。
- 资源消耗大:不适合频繁更新的场景。
3. MOW (Merge-On-Write) - 删除向量模式 (推荐)
ALTER TABLE orders SET ('deletion-vectors.enabled' = 'true');
- 设计哲学:读写平衡。利用删除向量(Deletion Vectors) 技术,在写入时做少量工作,换取读取时的巨大提升。
- 工作流:
- 写:
- 当要更新或删除一条记录时,系统通过主键索引找到该记录所在的数据文件。
- 不是重写文件,而是生成一个删除向量文件,标记该文件中哪些行已被逻辑删除。
- 新数据照常写入新文件。
- 读:
- 读取数据文件时,同时加载对应的删除向量。
- 直接过滤掉被标记删除的行,无需归并排序,即可得到正确结果。
- 写:
- 优势:同时获得了接近 MOR 的写入性能和接近 COW 的读取性能。
- 注意事项:
- 数据可见性:为确保一致性,默认配置下,L0 文件需经同步压缩后才可见。启用异步压缩可提升写入速度,但会带来数据延迟。
- 额外开销:需要维护删除向量文件。
生产环境建议与选择策略
1. 模式选择指南
- 选择 MOR (
默认
) 如果:- 场景是高吞吐实时写入(如 CDC 同步)。
- 读取频率较低,或对读取延迟不敏感(如下游批处理作业)。
- 选择 COW (
'full-compaction.delta-commits' = '1'
) 如果:- 场景是批量作业,一天只写几次。
- 读取性能是绝对优先,且数据一旦写入很少更新。
- 警告:切勿在需要频繁更新的生产流作业中使用此模式。
- 选择 MOW (
'deletion-vectors.enabled' = 'true'
) 如果:- 场景需要兼顾读写性能(如交互式查询、实时报表)。
- 这是绝大多数通用场景的推荐选择。
2. MOR 模式的读取优化
即使使用 MOR 模式,也有办法优化读取:
- 查询
$ro
(Read-Optimized) 系统表:SELECT * FROM my_table$ro; -- 仅读取已压缩的顶层文件,数据可能非最新
- 调整 Compaction 策略:缩短
'compaction.optimization-interval'
,让压缩更频繁,使更多数据保持在已合并状态。
总结
Paimon 通过 MOR、COW、MOW 三种模式,为用户提供了从写优化到读优化的完整频谱选择。其核心设计思想是将“合并”这个计算密集型操作在不同时间点执行:
- MOR:在读取时合并 → 写快读慢
- COW:在写入时合并 → 写慢读快
- MOW:在写入时标记,读取时过滤 → 写较快读很快
对于大多数需要同时支持实时更新和快速查询的现代数据平台而言,MOW (删除向量) 模式是目前的最优推荐,它很好地平衡了性能和复杂度,是 Paimon 作为高性能湖仓格式的关键技术优势之一。
Paimon Sink 合并引擎
Paimon Sink 在接收到相同主键的多条记录时,会将其合并为一条记录以确保主键唯一性。用户可通过 merge-engine
表属性选择合并方式。重要提示:在 Flink SQL TableConfig 中必须设置 table.exec.sink.upsert-materialize = NONE
,否则可能导致异常行为。对于乱序数据,建议使用 Sequence Field 进行校正。
1. 去重(Deduplicate)引擎
- 默认合并引擎,仅保留最新记录,丢弃同主键的旧记录。
- 若最新记录是 DELETE 操作,则删除所有同主键记录。
- 可通过
ignore-delete
配置忽略 DELETE 操作。
2. 部分更新(Partial-Update)引擎
- 通过多次更新逐步完善记录的字段(按主键合并,非 NULL 值不覆盖)。
- 示例:三条记录
<1, 23.0, 10, NULL>
、<1, NULL, NULL, 'This is a book'>
、<1, 25.2, NULL, NULL>
合并结果为<1, 25.2, 10, 'This is a book'>
。 - 流查询要求:必须与
lookup
或full-compaction
changelog producer 配合使用(input
仅返回输入记录)。 - 删除记录处理:
- 配置
ignore-delete
忽略删除。 - 配置
partial-update.remove-record-on-delete
在收到 DELETE 时删除整行。 - 通过
sequence-group
撤回部分列(需配置partial-update.remove-record-on-sequence-group
)。
- 配置
序列组(Sequence Group)
- 解决多流更新时的乱序问题(避免 Sequence Field 被其他流的最新数据覆盖)。
- 示例:为字段分组(如
a,b
组依赖g_1
,c,d
组依赖g_2
),仅当序列字段值更新时才对组内字段更新。 - 支持多字段排序(如
fields.g_2,g_3.sequence-group
按顺序比较)。
部分更新的聚合
- 可为字段配置聚合函数(如
first_value
、sum
)。 - 支持为序列组配置聚合(如
fields.a.aggregate-function = 'sum'
)。 - 可通过
fields.default-aggregate-function
设置默认聚合函数。
3. 聚合(Aggregation)引擎
- 按主键对数值字段按聚合函数合并(如
max
、sum
)。 - 示例:输入
<1, 23.0, 15>
和<1, 30.2, 20>
,配置price
用max
、sales
用sum
,结果<1, 30.2, 35>
。 - 支持函数:
- 数值操作:
sum
、product
、count
(通过布尔转数值实现)。 - 极值:
max
、min
。 - 最新值:
last_value
、last_non_null_value
。 - 字符串:
listagg
(可配置分隔符)。 - 布尔:
bool_and
、bool_or
。 - 首次值:
first_value
、first_non_null_value
。 - 基数估算:
rbm32
、rbm64
(用于 RoaringBitmap)。 - 嵌套表:
nested_update
(需指定嵌套表主键nested-key
)。 - 集合:
collect
(可去重)、merge_map
(合并 Map)。 - 基数草图:
hll_sketch
(高精度低存储)、theta_sketch
(支持交并集但耗内存)。
- 数值操作:
基数草图注意事项
- HLL:适用于去重计数和合并,精度高且存储紧凑。
- Theta:支持集合操作(如交集、并集),但内存消耗大。
- 两者不能混合使用。
回撤(Retraction)支持
- 仅
sum
、product
、collect
、merge_map
、nested_update
、last_value
、last_non_null_value
支持回撤(UPDATE_BEFORE/DELETE)。 - 可配置
fields.${field_name}.ignore-retract='true'
忽略回撤。 collect
和merge_map
尽力处理回撤,但乱序时可能结果不准确(如误保留数据或过度删除)。
4. 首行(First-Row)引擎
- 保留同主键的第一条记录(与去重引擎的区别是仅生成 INSERT changelog)。
- 限制:
- 仅支持
none
和lookup
changelog producer(流查询需用lookup
)。 - 不支持
sequence.field
。 - 不接受 DELETE/UPDATE_BEFORE(可配置
ignore-delete
忽略)。 - 数据可见性:Level 0 文件仅在压缩后可见(默认同步压缩,异步压缩可能导致延迟)。
- 仅支持
关键配置总结
功能 | 配置属性示例 | 说明 |
---|---|---|
通用 | table.exec.sink.upsert-materialize = NONE | 必须设置,避免 Upsert 物化异常 |
忽略删除 | ignore-delete = true | 适用于所有引擎,忽略 DELETE 记录 |
部分更新 | merge-engine = partial-update | 需搭配 Sequence Field 或 Sequence Group 解决乱序 |
序列组 | fields.g_1.sequence-group = 'a,b' | 定义字段依赖的序列字段 |
聚合函数 | fields.price.aggregate-function = 'max' | 为字段指定聚合逻辑 |
默认聚合 | fields.default-aggregate-function = 'sum' | 全局默认聚合函数 |
嵌套表主键 | fields.sub_orders.nested-key = 'sub_order_id' | 配合 nested_update 使用 |
回撤忽略 | fields.uv.ignore-retract = 'true' | 忽略特定字段的回撤消息 |
注意:流式查询时,部分更新和聚合引擎需与 lookup
/full-compaction
changelog producer 配合使用。
Changelog Producer
流式写入时,通过指定 changelog-producer
表属性,可选择从表文件生成变更日志的模式。注意:启用 Changelog Producer 可能显著降低压缩性能,非必要不启用。
1. None(默认)
- 行为:不生成额外变更日志。Paimon Source 仅看到快照间的合并变更(如键的删除和新值)。
- 限制:无法提供完整变更日志(缺少旧值),需消费者自行记录键值状态(如 Flink 的
normalize
算子,但成本高昂)。 - 适用场景:数据库系统等能自行管理状态的消费者。可通过
scan.remove-normalize
强制移除归一化算子。
2. Input
- 配置:
'changelog-producer' = 'input'
- 行为:直接使用输入记录作为完整变更日志源(保存到独立 changelog 文件)。
- 要求:输入必须是完整变更日志(如数据库 CDC 或 Flink 有状态计算生成)。
- 适用场景:输入本身为 CDC 或完整 changelog。
3. Lookup
- 配置:
'changelog-producer' = 'lookup'
- 行为:在提交数据前通过查找生成变更日志(支持异步压缩)。
- 性能调优:
lookup.cache-file-retention
:缓存文件保留时间(默认 1 小时)。lookup.cache-max-disk-size
:本地磁盘缓存上限(默认无限制)。lookup.cache-max-memory-size
:内存缓存上限(默认 256 MB)。
- 去重:支持
changelog-producer.row-deduplicate
避免对相同记录生成-U/+U
。 - 注意:需增加 Flink 配置
execution.checkpointing.max-concurrent-checkpoints
以提升性能。 - 适用场景:输入非完整 changelog 但希望避免昂贵归一化算子。
4. Full Compaction
- 配置:
'changelog-producer' = 'full-compaction'
- 行为:通过全量压缩后的差异生成变更日志(延迟受压缩频率影响)。
- 频率控制:通过
full-compaction.delta-commits
设置(默认 1,即每次 checkpoint 触发全压缩)。 - 去重:同样支持
changelog-producer.row-deduplicate
。 - 缺点:开销较大,延迟高(如 30 分钟)。
- 适用场景:高延迟容忍场景(如离线分析),且输入非完整 changelog。
Changelog 合并
- 问题:短 checkpoint 间隔+多桶可能导致大量小 changelog 文件,增加存储压力。
- 解决方案:设置
changelog.precommit-compact = true
,在写入后合并小文件为大文件。
序列字段(Sequence Field)
- 作用:解决分布式计算中的数据乱序问题(默认按输入顺序合并)。
- 配置:
'sequence.field' = 'field_name'
(如时间戳字段)。- 值最大的记录最后合并(同值时按输入顺序)。
- 支持多字段(如
'update_time,flag'
,按顺序比较)。
- 注意:与
first_row
、first_value
等功能冲突,可能导致意外结果。
行类型字段(Row Kind Field)
- 默认:按输入行自动判断行类型(INSERT/UPDATE/DELETE)。
- 配置:
'rowkind.field' = 'field_name'
,从指定字段解析行类型。 - 有效值:
'+I'
(插入)、'-U'
(更新前)、'+U'
(更新后)、'-D'
(删除)。
关键配置总结
功能 | 配置属性 | 说明 |
---|---|---|
Changelog Producer | 'changelog-producer' = 'input' | 输入即完整 changelog(如 CDC) |
'changelog-producer' = 'lookup' | 通过查找生成 changelog(需调优缓存) | |
'changelog-producer' = 'full-compaction' | 通过全量压缩差异生成 changelog(延迟高) | |
去重 | changelog-producer.row-deduplicate | 避免重复生成 -U/+U |
文件合并 | changelog.precommit-compact = true | 合并小 changelog 文件 |
序列字段 | 'sequence.field' = 'update_time' | 解决乱序,按字段值排序合并 |
行类型字段 | 'rowkind.field' = 'op_kind' | 从字段解析行类型(+I/-U/+U/-D) |
推荐:
- 优先使用
input
(若输入为完整 changelog)。 - 否则用
lookup
(低延迟场景)。 full-compaction
仅适用于高延迟容忍的离线场景。
Compaction 概述
Compaction 是 LSM 树结构中的关键过程,用于合并多个排序运行(sorted runs)以提高查询性能,但会消耗 CPU 和 IO 资源。需要在查询性能和写入性能之间取得平衡。
Compaction 的作用
- 减少 Level 0 文件:避免查询性能下降。
- 生成 Changelog:通过
changelog-producer
生成变更日志。 - 生成删除向量:为 MOW(Merge On Write)模式生成删除向量。
- 数据过期:支持快照过期、标签过期、分区过期。
Compaction 的限制
- 并发限制:同一分区的 Compaction 只能由一个作业执行,否则会冲突报错。
- 写入性能影响:Compaction 几乎总是影响写入性能,需仔细调优。
异步 Compaction
通过配置使 Compaction 完全异步,不阻塞写入,适合需要高写入吞吐的场景:
num-sorted-run.stop-trigger = 2147483647 # 极大值,避免写入暂停
sort-spill-threshold = 10 # 防止内存溢出
lookup-wait = false # 不等待 Compaction
- 效果:写入高峰时生成更多文件,在低峰时逐步合并优化读取性能。
专用 Compaction 作业
- 适用场景:多个作业写入同一表时,需分离 Compaction 任务以避免冲突。
- 方式:使用独立的 Compaction 作业。
记录级过期(Record-Level Expire)
在 Compaction 中配置记录过期:
record-level.expire-time = 7d # 记录保留时间
record-level.time-field = update_time # 时间字段
- 注意:过期操作在 Compaction 时执行,无强实时性保证。
全量 Compaction(Full Compaction)
- 默认策略:使用 Universal-Compaction,自动触发全量 Compaction。
- 定期执行配置:
compaction.optimization-interval
:定期执行全量 Compaction(确保读优化表的查询及时性)。full-compaction.delta-commits
:每次 delta commit 后触发全量 Compaction(同步执行,影响写入效率)。
Compaction 关键配置
配置项 | 默认值 | 说明 |
---|---|---|
num-sorted-run.stop-trigger | (none) | 触发写入暂停的排序运行数(默认 compaction-trigger + 3 ) |
sort-spill-threshold | (none) | 排序读取器数量阈值,超过则溢出到磁盘防止 OOM |
num-sorted-run.compaction-trigger | 5 | 触发 Compaction 的排序运行数(包括 Level 0 文件和高层级运行) |
- 调优建议:
- 增大
stop-trigger
和compaction-trigger
可提高写入性能,但会增加查询时的内存和 CPU 开销。 - 需根据内存大小设置
sort-spill-threshold
防止 OOM。
- 增大
查询性能优化
1. 表模式(Table Mode)
-
MOR(Merge On Read):注意分桶数(
bucket
),它限制数据读取的并发度。 -
MOW(Deletion Vectors)或 COW(Copy On Write):无读取并发限制,可对非主键列使用过滤条件。
2. 主键过滤
-
对分桶表(如
bucket=5
),主键过滤条件可显著加速查询,减少文件读取量。
3. 文件索引(File Index)
为启用删除向量的表配置文件索引,在读取时过滤文件:
Bloom Filter:
CREATE TABLE t WITH ('deletion-vectors' = 'true','file-index.bloom-filter.columns' = 'c1,c2','file-index.bloom-filter.c1.items' = '200'
);
file-index.bloom-filter.columns
:需要 Bloom 过滤器的列。file-index.bloom-filter.<column>.fpp
:误报率。file-index.bloom-filter.<column>.items
:每个数据文件的预期唯一值数。
其他索引类型:
file-index.bitmap.columns
:位图索引。file-index.bsi.columns
:Bit-Slice 索引。
为已有表添加索引:
- 使用
rewrite_file_index
过程(无需重写数据)。 - 先通过
ALTER
配置file-index.<filter-type>.columns
。
关键总结
- Compaction 是平衡艺术:在写入和查询性能间权衡。
- 异步 Compaction:适合高写入吞吐场景。
- 专用作业:多写入作业时需分离 Compaction。
- 过期策略:在 Compaction 时执行,无实时保证。
- 查询优化:通过主键过滤和文件索引(Bloom、Bitmap、BSI)提升性能。
Paimon 无主键表(Append Table)
- 定义:无主键的表称为 Append 表,只能追加数据,不支持直接接收 changelog 或 upsert 更新。
- 特点:类似 Hive 分区表,但提供更多高级功能:
- 对象存储友好(S3、OSS)
- 时间旅行和回滚
- 低成本 DELETE/UPDATE
- 流式写入自动合并小文件
- 类队列的流式读写
- 高性能查询(支持排序和索引)
流式处理(Streaming)
1. 自动小文件合并
- 机制:无分桶定义时,写入器不执行 compaction,而是由 Compact Coordinator 扫描小文件并分配任务给 Compact Worker。
- 拓扑结构:Flink 插入 SQL 的拓扑包含 Coordinator 和 Worker(无背压问题)。
- 只写模式:设置
write-only = true
可移除压缩组件。 - 适用场景:仅 Flink 流模式支持自动压缩,也可通过 Flink Action 手动触发压缩(并设置
write-only
禁用其他压缩)。
2. 流式查询
- 默认行为:首次启动读取最新快照,之后持续读取增量记录。
- 增量读取:可通过
scan.mode
、scan.snapshot-id
、scan.timestamp-millis
或scan.file-creation-time-millis
指定只读增量。 - 顺序保证:默认无顺序保证(类似 Flink Kafka),需通过分桶键(Bucket Key)实现有序性(见 Bucketed Append)。
查询性能优化
1. 排序优化(Data Skipping By Order)
- 统计过滤:Manifest 文件记录每个字段的 min/max 值,根据 WHERE 条件过滤文件(高效时查询从分钟级加速到毫秒级)。
- 数据排序:若数据分布不利于过滤,可通过 Flink COMPACT Action 或 Spark COMPACT Procedure 按字段排序。
2. 文件索引(File Index)
- 支持类型:
- Bloom Filter:点查询加速。
file-index.bloom-filter.columns
:配置列。file-index.bloom-filter.<column>.fpp
:误报率。file-index.bloom-filter.<column>.items
:每文件预期唯一值数。
- Bitmap:更高精度,但耗空间。
- BSI(Bit-Slice Index):数值范围过滤。
- Bloom Filter:点查询加速。
- 添加索引:使用
rewrite_file_index
过程为已有表添加索引(无需重写数据),先通过ALTER
设置file-index.<filter-type>.columns
。
数据更新(UPDATE/DELETE)
- 目前仅 Spark SQL 支持:
- COW(Copy on Write):重写整个文件(成本高)。
- MOW(Merge on Write):启用删除向量(
deletion-vectors.enabled = true
),仅标记删除而不重写文件。
分桶 Append 表(Bucketed Append)
1. 创建分桶表
CREATE TABLE my_table (product_id BIGINT,price DOUBLE,sales BIGINT
) WITH ('bucket' = '8','bucket-key' = 'product_id'
);
2. 流式处理
- 顺序保证:同一桶内记录严格有序(类似 Kafka 的 key),流式读写按写入顺序传输。
- 压缩策略:
write-only
:跳过压缩和快照过期(需专用压缩作业)。compaction.min.file-num
:触发压缩的最小文件数(避免压缩几乎满的文件)。compaction.max.file-num
:触发压缩的最大文件数(避免堆积小文件)。full-compaction.delta-commits
:定期触发全量压缩。
3. 流式读取顺序
- 不同分区:
scan.plan-sort-partition=true
:按分区值升序输出。- 否则按分区创建时间早晚输出。
- 同分区同桶:按写入顺序输出。
- 同分区不同桶:无顺序保证(由不同任务处理)。
4. Watermark 支持
- 定义 Watermark:用于时间窗口计算。
- Watermark 对齐:
scan.watermark.alignment.group
:对齐组。scan.watermark.alignment.max-drift
:最大漂移(超时暂停消费)。
- 有界流:通过
scan.bounded.watermark
指定水印终止条件(读取至大于该水印的快照停止)。
批处理(Batch)
- 分桶优化:启用
spark.sql.sources.v2.bucketing.enabled=true
可避免 Shuffle。 - 示例:两个分桶策略相同的表(桶数相同、分桶键一致)进行 JOIN 时,无需 Shuffle。
关键配置总结
功能 | 配置项 | 说明 |
---|---|---|
只写模式 | write-only = true | 跳过压缩,需专用压缩作业 |
压缩触发 | compaction.min.file-num = 5 | 最小文件数触发压缩 |
compaction.max.file-num = 5 | 最大文件数触发压缩(防小文件堆积) | |
全量压缩 | full-compaction.delta-commits | 定期触发全压缩 |
文件索引 | file-index.bloom-filter.columns | 配置 Bloom 过滤器 |
水印对齐 | scan.watermark.alignment.group | 水印对齐组 |
有界流 | scan.bounded.watermark | 水印终止条件 |
Spark 分桶优化 | spark.sql.sources.v2.bucketing.enabled=true | 避免 Shuffle |
适用场景推荐:
- 高吞吐写入:使用异步压缩和分桶表。
- 有序流处理:配置分桶键(Bucket Key)。
- 点查询优化:添加 Bloom Filter 索引。
- 批量更新:通过 Spark SQL 执行(COW/MOW 模式)。