Apache Paimon 实战:流式湖仓一体与实时数仓落地

当 Kafka 里的订单还在以每秒上万条的速率滚动,业务却想知道「实时 GMV 和昨天的对账差多少」——传统数仓要等 T+1,纯流计算又留不下历史。Apache Paimon 正是为解决这个矛盾而生:它把流式湖仓(Streaming Lakehouse)做成了一张「既能实时写、又能随时查」的表。本文用 Docker + Flink 快速上手 Paimon,演示 Kafka 实时入湖、主键表更新以及同一张表的离在线一体化查询,并给出与 Iceberg/Hudi 的选型建议。

一、Apache Paimon 是什么

Apache Paimon(原 Flink Table Store,已毕业为 Apache 顶级项目)是一种面向流批一体的湖仓存储格式。它构建在对象存储或 HDFS 之上,底层用 LSM(Log-Structured Merge-Tree)结构组织数据,因此既支持像消息流一样持续追加写入,也支持基于主键的更新、删除与聚合。你可以把它理解为「给数据湖加上了数据库的更新能力」:上游是 CDC 或 Kafka,下游同一个 SQL 既能跑流式最新值,也能跑批式全量分析。

相比本站的 Apache Iceberg 数据湖表格式 更偏向批式 ACID 与海量分析,Paimon 的差异化在于「为流而生」——写入即合并、读取可指定新鲜度,天然适配 Flink 的流批执行模式。两者并非互斥,很多架构里 Paimon 负责实时层、Iceberg 负责归档层。

二、核心概念先理清

  • 主键表(Primary Key Table):声明 PRIMARY KEY,支持更新/删除,适合订单、用户画像等需要「最新状态」的场景。底层按 bucket 分桶,写入时做去重与合并。
  • 仅追加表(Append-only Table):没有主键,只负责写入,适合日志、埋点等不可变数据。
  • Bucket:数据分桶数,决定读写并行度与文件大小,主键表必须 bucket > 0。
  • Changelog Producer:决定表能否产出变更流,取值 input / full / lookup / none,做流式下游时很关键。
  • Merge Engine:主键冲突时的合并策略,如 deduplicate(保留最新行)、partial-update(字段级合并)、aggregation(聚合)。

三、用 Docker 起一套 Flink + Paimon 环境

最稳妥的方式是跑一个 Flink Standalone 集群,并把 Paimon 的 Flink 连接器 jar 放进 lib 目录。先准备 docker-compose:

version: "3"
services:
  jobmanager:
    image: flink:1.17.2
    command: jobmanager
    ports:
      - "8081:8081"
    volumes:
      - ./lib:/opt/flink/lib
  taskmanager:
    image: flink:1.17.2
    command: taskmanager
    depends_on:
      - jobmanager
    volumes:
      - ./lib:/opt/flink/lib
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    depends_on: [zookeeper]
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

把与 Flink 大版本匹配的 Paimon 连接器放进去即可,例如 paimon-flink-1.17-0.9.0.jar(从 Maven Central 的 org/apache/paimon/paimon-flink-1.17 下载)。也可以不依赖 compose,直接下载 jar 后用 SQL Client 加载:

wget https://repo1.maven.org/maven2/org/apache/paimon/paimon-flink-1.17/0.9.0/paimon-flink-1.17-0.9.0.jar -O lib/paimon-flink.jar
docker compose up -d
docker compose exec jobmanager ./bin/sql-client.sh

四、用 Flink SQL 建库建表

进入 SQL Client 后,先注册 Paimon Catalog(warehouse 指向存储根路径),再建库建表。下面建一张订单主键表,按 order_id 去重、分 4 个桶,并开启 input 级 changelog:

CREATE CATALOG paimon_catalog WITH (
  'type' = 'paimon',
  'warehouse' = 'file:/tmp/paimon/warehouse'
);
USE CATALOG paimon_catalog;
CREATE DATABASE IF NOT EXISTS default;

CREATE TABLE IF NOT EXISTS default.orders (
  order_id   BIGINT,
  user_id    BIGINT,
  amount     DECIMAL(10,2),
  status     STRING,
  ts         TIMESTAMP(3),
  PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
  'bucket' = '4',
  'changelog-producer' = 'input',
  'merge-engine' = 'deduplicate'
);

再把 Kafka 上的原始订单流声明成一张外部表,作为入湖的数据源:

CREATE TABLE default.kafka_orders (
  order_id   BIGINT,
  user_id    BIGINT,
  amount     DECIMAL(10,2),
  status     STRING,
  ts         TIMESTAMP(3)
) WITH (
  'connector' = 'kafka',
  'topic' = 'orders',
  'properties.bootstrap.servers' = 'kafka:9092',
  'properties.group.id' = 'paimon-ingest',
  'scan.startup.mode' = 'earliest-offset',
  'format' = 'json'
);

五、Kafka 实时入湖

实时入湖就是一条 INSERT INTO … SELECT 的流式作业。Flink 会持续把 Kafka 里的订单写进 Paimon,并自动按主键做更新合并:

INSERT INTO default.orders
SELECT order_id, user_id, amount, status, ts
FROM default.kafka_orders;

如果该作业以流式(streaming)模式常驻,Paimon 表就始终持有「最新订单状态」。配合 Kafka 消息队列入门 的基础,这条链路把「消息流」无缝落成了「可查询的表」。若订单状态会变化(待支付→已支付→已退款),由于表声明了主键与 deduplicate 合并引擎,后到的同 order_id 记录会自然覆盖旧值。

六、流批一体查询:同一张表,两种读法

Paimon 的杀手锏是「一张表两种读法」。默认 streaming 模式读最新快照,batch 模式则做全量扫描——非常适合既要实时大屏、又要离线对账的场景:

-- 流式:拿最新状态(实时大屏)
SELECT status, COUNT(*) AS cnt, SUM(amount) AS gmv
FROM default.orders
GROUP BY status;

-- 批式:全量分析(对账 / 报表)
SET 'execution.runtime-mode' = 'batch';
SELECT user_id, SUM(amount) AS total
FROM default.orders
GROUP BY user_id
ORDER BY total DESC
LIMIT 10;

还支持时间旅行,按快照或时间戳回看历史,排障和对账非常方便:

-- 读指定快照
SELECT * FROM default.orders /*+ OPTIONS('scan.snapshot-id' = '1') */;

-- 读某个时间点之前的状态
SELECT * FROM default.orders
/*+ OPTIONS('scan.timestamp-millis' = '1727000000000') */;

六之一、主键更新语义:partial-update 字段级合并

上面的订单表用的是默认 deduplicate 合并引擎——同主键后到的整行直接覆盖前一行。但真实业务里,不同系统往往只更新用户画像的「不同列」:点击流负责打标签,交易流负责更新等级。这时用 partial-update 引擎,多条流写不同字段,Paimon 会把非空字段合并成同一行,而不是互相覆盖:

CREATE TABLE default.user_profile (
  user_id BIGINT,
  tags     STRING,
  level    INT,
  updated  TIMESTAMP(3),
  PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
  'bucket' = '4',
  'merge-engine' = 'partial-update',
  'changelog-producer' = 'input'
);

-- 点击流只写 tags
INSERT INTO default.user_profile (user_id, tags, updated)
SELECT user_id, collect_tags(tag), NOW() FROM click_stream GROUP BY user_id;

-- 交易流只写 level,不会清掉 tags
INSERT INTO default.user_profile (user_id, level, updated)
SELECT user_id, vip_level, NOW() FROM order_stream;

partial-update 的代价是同一主键的多列更新无法按「整行时间戳」判定先后,只在列级别合并。如果你的场景要求「后到整行胜出」,就退回 deduplicate;如果要求「某列取最大值」,还可以用 aggregation 引擎配合 sum/max 聚合函数。选错引擎是 Paimon 最常见的坑之一。

六之二、CDC 入湖:从 MySQL Binlog 实时同步

Kafka 入湖适合已有消息总线的团队;如果上游就是 MySQL 业务库,更直接的方式是用 Flink CDC 连接器读 Binlog,把 OLTP 数据「原样」同步进湖,免去了自己造 Kafka topic 的环节,且天然支持 exactly-once 与 schema 变更:

CREATE TABLE default.mysql_orders (
  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' = 'mysql',
  'port' = '3306',
  'username' = 'cdc_user',
  'password' = 'secret',
  'database-name' = 'shop',
  'table-name' = 'orders'
);

INSERT INTO default.orders SELECT * FROM default.mysql_orders;

注意 mysql-cdc 需要额外的 flink-sql-connector-mysql-cdc jar 放进 lib。相比「业务写双份 / 定时 sqoop 全量抽取」,CDC 入湖把延迟从小时级压到秒级,且不会对业务库造成全表扫描压力——这也是 Paimon 被用来替换传统 T+1 数仓的关键一步。

六之三、小文件治理:compaction 调优

LSM 结构在高频写入下会不断产生小文件,读放大随之上升。Paimon 默认在后台自动 compaction,但高吞吐场景需要主动调大写入缓冲与触发阈值,避免「写得快、查得慢」:

ALTER TABLE default.orders SET OPTIONS (
  'write-buffer-size' = '256 MB',
  'num-sorted-run.compaction-trigger' = '5',
  'compact.min.file-num' = '5',
  'compact.max.file-num' = '50'
);

经验上:写入越猛,write-buffer-size 越大;读多写少则可以把 compaction 触发阈值调低以加速合并。compact.max.file-num 控制单次合并上限,过大会让单次 compaction 耗时过长、拖累实时性。生产环境建议配合监控观察文件数与查询 P95 延迟,再回头微调。

七、与 Iceberg / Hudi 怎么选

三者在「湖仓」赛道各有侧重,选型时看你的主链路是流还是批:

维度Apache PaimonApache IcebergApache Hudi传统 Hive
流批一体原生流批双模偏批,近实时写近实时(MOR)仅批
主键更新原生 LSM 更新需 rewrite / merge原生 upsert需全量覆盖
实时入湖延迟秒级分钟级分钟级小时级(T+1)
典型适用实时数仓、CDC 入湖海量分析、归档近实时更新离线数仓

经验法则:上游是 CDC/Kafka、要实时最新值且下游还要跑批,优先 Paimon;以对象存储上的超大数据集离线分析为主、强调开放生态,选 Iceberg;已经重度使用 Spark/HDFS 且要 upsert,Hudi 也很成熟。

八、生产落地检查清单

检查项建议
存储底座用对象存储(S3/OSS)或 HDFS 做 warehouse,别用本地盘,否则 TaskManager 漂移丢数据
Bucket 数按数据量与并发设 4~32,过小会热点、过大则小文件过多
Changelog有流式下游时设 changelog-producer=input 或 full,否则下游读不到变更
小文件合并开启 compaction(默认后台自动),高写入场景调大 write-buffer 与合并线程
新鲜度流式读默认追最新;对账场景用 batch 模式保证全量一致
CI 与回滚DDL 纳入 GitHub Actions 版本化,配合 snapshot 时间旅行可随时回看

Apache Paimon 把「流」和「湖」之间的裂缝填平了:一张表既是实时管道,也是分析底座。当你再被「实时 GMV 和昨天对不上」这类问题追问时,答案就在这张既能写、也能查的 Paimon 表里。环境搭建可复用本站 Docker Compose 实战 的底座,先把 Flink 与 Kafka 跑顺,再让数据自然地「流」进湖中。

上一篇 Spring Batch 实战:批处理任务调度、分片与断点续跑
下一篇 CORS 跨域排查实战:从预检请求到彻底解决
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]