Apache Hudi权威指南——基于 Hudi Streamer 构建数据湖仓

103 阅读33分钟

在现代组织中,数据孤岛(data silos)带来的不只是碎片化的数据,更是碎片化的努力。各业务团队常常各自为政地解决相同的数据工程问题,重复建设类似的 ETL 工具,并为模式(schemas)与格式(formats)制定各自的约定。这种冗余不仅浪费宝贵资源,还为数据共享与标准化树立了重重障碍。核心挑战上升为战略层面:组织如何摆脱这种低效,提供一套标准化工具统一平台?如何赋能团队在共同的数据集(datasets)目录(catalogs)监控看板(monitoring dashboards) 之上协同完成数据摄取与转换?

这一挑战的现代答案是数据湖仓(data lakehouse) ,而 Apache Hudi 是构建湖仓的强力之选。若你的组织饱受数据孤岛之困、尚未收敛到单一的数据存储方案,相较其他替代品,Hudi 提供了更大的灵活性。Hudi 不仅允许组织内不同团队对其数据栈与架构保持主权(sovereignty),还提供一个专门的摄取工具——Hudi Streamer,可连接广泛的上游数据源并简化数据湖仓的构建。

在本章中,我们引入一家虚构航空公司 Alcubierre,它正遭遇常见的数据孤岛难题。设想我们是推动 Alcubierre 数据统一工作的团队成员:我们将探索如何使用 Hudi Streamer 从公司多样化的数据源进行摄取;随后,我们会走通一个端到端应用示例,并分享搭建湖仓摄取流程时的技巧与经验;最后,我们将进一步拆解 Hudi Streamer 的各类选项,了解其如何支撑建设湖仓平台的不同方面。

Alcubierre 的数据孤岛之痛(Data Silo Woes)

成立于大约 10 年前,Alcubierre 仍是一家相对年轻的航空公司。它凭借新颖的福利与忠诚度计划迅速获得市场认可,但也逐渐因糟糕的客户体验而闻名。公司碎片化的数据系统常导致与运维相关的航班延误,而数据孤岛又让中断评估与预测性维护变得困难。投诉调查举步维艰,并常被来自各部门(客服、运营、飞机维护等)的数据孤岛所加剧。

过去十年,Alcubierre 的各部门在很大程度上被允许独立发展,形成各自的技术栈与架构。这种独立性曾帮助公司快速打造成功的全球业务,但也逐渐生成了数据孤岛,如今阻碍公司从整体上理解客户需求与组织低效。在下面的章节里,我们将进一步了解这些孤岛,其中一些场景你很可能似曾相识。

数据质量保障与去重(Data Quality Assurance and Deduplication)

客户服务(Customer Service) 部门的使命,是以高效、友好、个性化的方式,为 Alcubierre 乘客提供积极的旅行体验。该部门采用多层级(multitiered)的服务台(help desk) 来分流客户问题并尽快解决。它使用专门的 CRM 软件存储所有呼入记录及大量与客户问题与最终解决方案相关的元数据。这些记录会按夜批(nightly)从 CRM 批量导出为 CSV 文件并上传到 AWS S3 的对象存储(见图 8-1)。

image.png

图示:Alcubierre 客服的多层级服务台流程,从客户交互经过一线/二线到 CRM 系统的数据流。
图 8-1. Alcubierre 客户服务部门内的数据组织

不幸的是,多层级的服务台有时会引入重复记录:同一客户多次求助,或座席在录入时误填了客户 ID 或工单号。Alcubierre 已经开发了一些有效的去重技术,但这些手段只能在数据导出到 S3 之后,并与由财务(Finance)团队管理的主客户数据库进行比对时才能应用。一旦 CRM 导出失败或延迟,S3 上的对象存储就不再反映客户交互的最新状态,从而拖慢数据清洗与去重任务。

异构数据与模式演进(Heterogeneous Data and Schema Evolution)

安全与安保(Safety and Security)部门通过实施安全规程、开展风险管理与应急准备、维持合规并调查事故,来保障乘客、机组与飞机的最高安全标准。该部门近期采用了微服务架构来以流式数据协调多项服务。这些服务负责安全审计、风险评估、合规跟踪与事件报告,并通过 Kafka 流共享信息。最终,事件记录会落库到关系型数据库以长期保存(如用于合规审计),见图 8-2。

image.png

图示:Alcubierre 安全与安保部门的数据流与微服务架构,突出展示用 Kafka 流在安全审计、风险评估、合规跟踪与事件报告服务之间进行通信。
图 8-2. Alcubierre 安全与安保部门内的数据组织

各微服务输出的数据格式与元数据类型各异。例如,事件报告服务以 UTC 记录时间戳,而合规跟踪服务因区域差异采用本地时间。跨服务关联事件时容易引发混乱。再如,曾多次发生 API 变更未充分同步的事件:风险评估服务更新了模式,而事件报告服务未同步,导致下游出现破坏性变更。

数据管理、本地化与一致性(Data Management, Localization, and Consistency)

飞机维护(Aircraft Maintenance)部门在全球 10 个枢纽的维修团队共用一套MRO 系统(维护-修理-运营) ,用于跟踪维护计划、飞机维修日志与备件库存。维护主管借此掌握工程师的维护排班、飞机维护日志与零部件库存。这些工程记录会持久化到 PostgreSQL 数据库(见图 8-3)。

图示:MRO 软件与 PostgreSQL 数据库的关系,突出维护排班、飞机维护日志与部件库存等关键组件。

image.png

图 8-3. Alcubierre 飞机维护部门内的数据组织

问题在于,不同枢纽对部件的记录不一致:由于本地术语习惯差异,部件有时被以不同术语记录;且本地化实践的不一致会导致对维护日志或部件描述的误读。受限于 MRO 软件能力,各地库存并不总能正确同步,从而产生库存过量缺货并存的现象。

问题回顾(Problem Recap)

Alcubierre 需要识别一种架构性解法来消除数据孤岛带来的挑战:

  • 需要一个集中式数据存储库,承载各部门数据集并支持跨部门的业务分析,尽管上游来源与格式各异(如 S3 上的 CSV、Kafka 主题中的消息、Postgres 数据库中的记录)。
  • 方案应支持常见的数据处理任务,如去重记录格式转换(例如时间戳标准化)。
  • 方案还必须实施模式管理并支持模式演进,以适配上游数据源的变化。

湖仓架构来救场(Lakehouse Architecture to the Rescue)

外部顾问建议 Alcubierre 构建湖仓,以实现准实时洞察前瞻性决策。在初始上线阶段,Alcubierre 决定从三个部门接入源数据:飞机维护、客户服务、安全与安保

这个“理想版”的湖仓(见图 8-4)旨在增强部门间信息流动,既提升各部门自身职能,也能发掘面向全公司的新举措,从而显著提高运营效率并增加利润。

图示:Alcubierre 规划的湖仓设计,展示以 Hudi Streamer 集成各类软件与数据源以提升运营效率的方案。

image.png

图 8-4. Alcubierre 规划中的湖仓设计

幸运的是,我们可以使用 Hudi 来构建一个全面的湖仓平台,并将 Hudi Streamer 作为统一框架。在接下来的章节中,我们将进一步了解 Hudi Streamer 是什么,以及如何利用它在不推翻既有基础设施的前提下,为 Alcubierre 打造量身定制的湖仓。

什么是 Hudi Streamer?(What Is Hudi Streamer?)

Hudi Streamer 是一个以 Apache Spark 应用形式运行的实用工具(utility tool)。它由一组 Java 类构成,并打包在 Hudi 的 bundle JAR 中,这些 JAR 可通过 Maven 仓库公开下载。运行 Hudi Streamer 与运行任意标准 Spark 应用相似,只需使用 hudi-utilities-bundle 这个 JAR 即可。

Tip
你可以使用 wget 从公共 Maven 仓库下载该 JAR:

export REPO_URL=<URL>                     # 1
export HUDI_UTILITIES_JAR=<Jar path>      # 2
wget $REPO_URL/$HUDI_UTILITIES_JAR

1 Hudi 发布产物的 URL 为:https://repository.apache.org/content/repositories/releases
2 JAR 路径示例:org/apache/hudi/hudi-utilities-bundle_2.13/1.1.0/hudi-utilities-bundle_2.13-1.1.0.jar

在上述示例中,你下载的是 Hudi 1.1 的 bundle JAR(适配 Spark 3.5Scala 2.13)。注意:hudi-utilities-bundle JAR 会与对应 Hudi 发行版所支持的最新 Spark 版本匹配;请参考发行说明获取最新信息。

API 层级(API hierarchy)视角看,Hudi Streamer 实现了 Hudi Writer 接口,并在内部封装 HoodieWriteClient 来处理写操作。如图 8-5 所示,客户端写入层(client write layer) ——包含 Hudi 核心模型(core models)——负责依照 Hudi 表格式执行事务性写入。Hudi Streamer 层定义并实现与各类湖仓组件交互的能力,包括:摄取多样化的数据源格式、接入模式注册中心(schema registry) 、与**数据目录(data catalog)**同步等。

图示:Hudi Streamer 的 API 层级关系,展示源适配器、模式提供者、目录同步与存储等组件之间的交互。

image.png

图 8-5. Hudi Streamer API 层级剖析

Hudi Streamer 的设计目标,是简化将数据摄取进湖仓这一通常复杂的过程。它作为上游数据源Hudi 表之间的桥梁,提供可配置可定制的接口来管理图中所示的各个组件。

通过调整少量配置,各部门即可将 Hudi Streamer 作业定制到不同数据源之上,而底层基础设施(如作业调度、监控服务等)则保持与具体用例无关、高度可复用

下一节我们将深入讲解 Hudi Streamer 提供的相关选项,并说明它是如何帮助 Alcubierre 克服前文所述的数据孤岛挑战的。

Hudi Streamer 入门(Getting Started with Hudi Streamer)

Hudi Streamer 提供了多种可定制接口(见图 8-5),并配有一整套配置项。本节将聚焦与解决 Alcubierre 先前问题最相关的接口与选项。读完本节,你将对 Hudi Streamer 的能力有一个初步把握,并了解它如何帮助应对真实场景中的挑战。

从 S3 摄取数据(Ingesting Data from S3)

Alcubierre 的**客户服务(Customer Service)**部门将夜间通话记录存放在 S3 存储桶中。每天会按 yyyy/MM/dd 前缀的路径写入一批 CSV 文件。为配置其 Hudi Streamer 应用,客服部门首先需要选择合适的 Source 类。

Source 是 Hudi Streamer 提供的一个抽象,用于将上游源数据批量输入以供处理与写入。通过继承 Source 抽象类并将实现提供给 Hudi Streamer,用户即可将作业与各类数据系统无缝集成。

客服部门应将 --source-class 设置为 org.apache.hudi.utilities.sources.CsvDFSSource(由 hudi-utilities-bundle Jar 开箱即用提供)。该类专用于从存储路径加载 CSV 文件。每日的 Hudi Streamer 作业启动后,Source 将分布式读取由 --target-base-path 指定路径下的文件。

客服部门每日生成的 CSV 往往包含重复记录。为此,应启用布尔开关 --filter-dupes,该选项会在装载后移除重复,大幅提升下游分析质量。

从 Kafka 摄取数据(Ingesting Data from Kafka)

Kafka 是广泛使用的事件流平台,以高吞吐与低延迟著称。生产者将数据写入 Kafka 的主题(topic) ,数据以有序日志形式保存;下游的**消费者(consumer)**订阅主题以实时处理。

为将 **安全与安保(Safety and Security)**部门的 Kafka 数据落入湖仓,需要在 Hudi Streamer 应用中配置两个关键选项。每个应用从一个 Kafka 主题消费并写入一张对应的 Hudi 表:

  • hoodie.streamer.source.kafka.topic:指定要消费的 Kafka 主题。
  • --source-class:设为 org.apache.hudi.utilities.sources.AvroKafkaSource,作为目标主题的 Kafka 消费者组来拉取消息。

处理模式演进(Handling schema evolution)

该部门还应部署模式注册中心(schema registry) ,集中管理各 Kafka 主题的模式。借助它,可强制执行向后兼容的模式演进策略:仅允许新增可空列放宽已有列类型(如 int → long)。这样可避免因模式变更而导致消费者崩溃。

为将 Hudi Streamer 与注册中心集成,应配置:

  • hoodie.streamer.schemaprovider.registry.url:指向 schema registry 的 URL;
  • --schemaprovider-class:设为 org.apache.hudi.utilities.schema.SchemaRegistryProvider,表示源数据遵循注册中心提供的模式。

统一时间戳(Normalizing timestamps)

不同 Kafka 主题的时间戳格式不一:有的用 long 形式的 Unix timestamp,有的用各时区的人类可读格式。团队可利用 Hudi Streamer 提供的 Transformer 接口来统一。

Source 取到数据后,Transformer 执行轻量变换(增删列、扁平化等)。它接收一个 Spark Dataset,输出变换后的 Dataset,从而按摄取流水线需要完成数据改写。--transformer-class 可接收一个或多个 Transformer 实现的类名;多个时按顺序链式执行,前者输出即后者输入,既灵活又利于维护。

安全与安保部门可以实现一个 Transformer,将指定的时间戳列统一转换为 UTC 的 ISO-8601 格式(原文“ISO-8061”应为 “ISO-8601”)。通过 --transformer-class 提供该实现,Hudi Streamer 作业就会应用通用转换逻辑,将规范化后的时间戳写入 Hudi 表,提升数据质量并减少后续处理中的错误与解读成本。

从关系型数据库摄取(Ingesting Data from RDBMS)

飞机维护(Aircraft Maintenance)部门将应用数据存放在 Postgres。要把这些数据复制到湖仓,首先需要抽取:由于 SQL 查询通常只能获取最新状态,而周期性全量导出不可行,因此更适合采用**变更数据捕获(CDC, Change Data Capture)**的增量抽取。

与多数 OLTP 数据库一样,Postgres 会在 WAL(预写日志)中记录所有事务性变更(增/改/删)。CDC 通过读取并重放这些变更,让 CDC 应用恢复各表的精确状态;更重要的是,CDC 能持续高效处理新变更,保持副本实时同步

Debezium 是针对多种数据库(含 Postgres)的 CDC 软件。它作为 Postgres 插件读取 WAL,并充当 Kafka 生产者,将抽取的数据写入 Kafka 主题,便于下游灵活消费。

Hudi Streamer 对 Debezium CDC 数据提供开箱即用支持。 维护部门可在其 Postgres 上安装 Debezium,并复用由安全与安保部门管理的 Kafka 平台,将 Debezium 输出写入 Kafka 主题。团队应将 --source-class 配置为:

org.apache.hudi.utilities.sources.debezium.PostgresDebeziumSource

从而让 Hudi Streamer 处理 Kafka 中的 Debezium 数据格式。

与安全与安保部门的 Kafka Source 设置类似,维护部门需要指定要读取的 Kafka 主题,并使用模式注册中心治理模式;此外,可在部分 Hudi Streamer 作业中实现自定义 Transformer,以统一命名约定

通过正确配置 Hudi Streamer,维护部门即可顺利接入 Alcubierre 的湖仓平台,从而受益于更高的数据质量、更广的洞察,并改进维护管理流程。

Hudi Streamer 支持的数据源(Table 8-1)

Hudi Streamer 支持广泛的数据源进行摄取。下表汇总了当前支持的 Source 类与其数据来源:

表 8-1. Hudi Streamer 的数据摄取来源

Source 类数据来源与位置
MysqlDebeziumSource, PostgresDebeziumSource安装在 MySQL / Postgres 上的 Debezium 连接器产生的 CDC 数据
JdbcSource来自各类 RDBMS 的数据
AvroDFSSource, CsvDFSSource, JsonDFSSource, ORCDFSSource, ParquetDFSSourceDFS 路径上的 Apache Avro / CSV / JSON / Apache ORC / Apache Parquet 数据
AvroKafkaSource, JsonKafkaSource, ProtoKafkaSource从 Kafka 主题消费 Avro / JSON / Protobuf 记录
PulsarSource从 Apache Pulsar 主题消费数据
HoodieIncrSourceHudi 表;使用 Hudi 增量查询获取变更
HiveIncrPullSourceApache Hive 表;使用 Hudi 增量查询获取变更
SqlSource, SqlFileBasedSourceSpark 表;使用 SQL 查询并获取记录
GcsEventsSource, GcsEventsHoodieIncrSource, S3EventsSource, S3EventsHoodieIncrSource云存储事件;支持构建可靠流水线处理 GCS 或 AWS S3 上的文件(详见相关博客)

Hudi Streamer 实战(Hudi Streamer in Action)

在上一节了解了 Hudi Streamer 的能力后,本节通过一个端到端的实践示例展示其在真实环境中的应用。我们将演示如何使用 Hudi Streamer 将数据摄取进湖仓,聚焦来自 Alcubierre 飞机维护(Aircraft Maintenance)部门的一个示例数据集。该示例将展示如何配置数据流水线的各个组件,以确保完整且及时地摄取数据。

图 8-6 展示了示例应用:首先生成样例数据并存入 Postgres 数据库。我们基于飞机维护部门设计了一个 maintenance_schedule 表,并生成用于插入、更新与删除操作的样例记录。表 8-2 给出了表模式。

表 8-2. maintenance_schedule 表的模式(Schema)

列名数据类型说明
schedule_idINT表主键
aircraft_idVARCHAR(255)需维护的飞机
due_dateDATE维护任务到期日
technician_idsINT[]该维护任务分配的技术员列表

图示:利用 Hudi Streamer 将数据摄取到湖仓的流程图,从 Postgres 经 Debezium 与 Kafka,集成 schema registry,写入湖仓存储,并通过 Presto 与 Superset 进行查询与分析。

image.png

图 8-6. 使用 Hudi Streamer 将数据摄取到数据湖仓(箭头表示动作)

如“从 RDBMS 摄取数据”所述,飞机维护部门使用 Debezium + Kafka 通过 CDC 提取并存储 Postgres 数据。湖仓的摄取组件使用 Hudi Streamer 实现:配置为从 Kafka 消费、对接 schema registry,并将数据写入湖仓存储中的 Hudi 表。本示例复刻了上述设置,以模拟真实配置。

此外,示例还将 Hudi 表与 Hive Metastore 同步(该数据目录服务可与查询引擎 Presto 及可视化平台 Apache Superset 集成)。通过阅读本节余下部分的配置细节,你将更深入理解 Hudi Streamer 的工作方式,以及一个湖仓平台在实践中的样貌。

Note
下述章节给出端到端应用的本地开发搭建方法,供读者参考。生产运行流水线时,请根据具体环境复核并调整这些配置,并按需补充。

准备上游数据源(Preparing the Upstream Source)

上游由安装了 DebeziumPostgres,以及连接了 schema registryKafka 组成。我们使用 Debezium 官方的 Postgres Docker 镜像(预装并配置好 Debezium 插件)。在 docker-compose.yml 中,新增如下服务条目以 postgres 名称运行 Postgres 与 Debezium:

postgres:
  image: debezium/postgres:16-alpine
  hostname: postgres
  container_name: postgres
  ports:
    - 5432:5432
  environment:
    POSTGRES_USER: myuser
    POSTGRES_PASSWORD: mypassword
    POSTGRES_DB: postgres

创建第一批数据(Creating the first batch of data)

服务启动后,在 Postgres 中准备样例数据。登录 Postgres 控制台:

docker compose exec -it postgres psql -U myuser -d postgres

在控制台执行以下 SQL 创建表并插入首批记录:

CREATE TABLE debezium_signal
(
    id   VARCHAR(100) PRIMARY KEY,
    type VARCHAR(100) NOT NULL,
    data VARCHAR(2048) NULL
);

CREATE TABLE maintenance_schedule
(
    schedule_id      INT PRIMARY KEY,
    aircraft_id      VARCHAR(255) NOT NULL,
    due_date         DATE         NOT NULL,
    maintenance_type VARCHAR(255) NOT NULL,
    technician_ids   INT[]        NOT NULL
);

INSERT INTO maintenance_schedule
    (schedule_id, aircraft_id, due_date, maintenance_type, technician_ids)
VALUES (1, 'AC001', '2024-08-15', 'corrective', ARRAY[101, 102, 103]),
       (2, 'AC002', '2024-09-01', 'routine',   ARRAY[104, 105]),
       (3, 'AC003', '2024-07-30', 'routine',   ARRAY[106]),
       (4, 'AC001', '2024-10-05', 'routine',   ARRAY[107, 108]);

搭建 Kafka 栈(Setting up the Kafka stack)

Kafka 栈包括 schema registryKafka brokerKafka Connect。Broker 在生产者与消费者之间路由读写请求;Kafka Connect 是 Kafka 的可插拔、声明式的数据集成框架,支持运行可配置的源/汇连接器。Debezium 本质上就是由 Kafka Connect 执行的一种源连接器

在示例中,我们采用 Confluent 维护的 Docker 镜像。定义 broker 服务(Kafka broker),以及 schema-registry 服务(监听 8081 端口以处理 schema 获取请求):

broker:
  image: confluentinc/cp-kafka:7.6.1
  hostname: broker
  container_name: broker
  ports:
    - "9092:9092"
  environment:
    KAFKA_ADVERTISED_LISTENERS: <LISTENERS>  # 1

schema-registry:
  image: confluentinc/cp-schema-registry:7.6.1
  hostname: schema-registry
  container_name: schema-registry
  depends_on:
    - broker
  ports:
    - "8081:8081"
  environment:
    SCHEMA_REGISTRY_HOST_NAME: schema-registry
    SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'broker:29092'
    SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081

1 示例取值:PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092

接着定义 connect 服务(运行在 8083 端口)。注意 connect 需要依赖 broker 与 schema registry。为使 Debezium 可用,我们在 command 部分安装 Debezium 连接器;Debezium 输出为 Avro 格式(压缩率高),因此设置值转换器为 io.confluent.connect.avro.AvroConverter

connect:
  image: confluentinc/cp-kafka-connect-base:7.6.1
  hostname: connect
  container_name: connect
  depends_on:
    - broker
    - schema-registry
  ports:
    - "8083:8083"
  environment:
    CONNECT_BOOTSTRAP_SERVERS: 'broker:29092'
    CONNECT_REST_ADVERTISED_HOST_NAME: connect
    CONNECT_VALUE_CONVERTER: io.confluent.connect.avro.AvroConverter
    CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry:8081
    CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components"
  command:
    - bash
    - -c
    - |
      echo "Installing Connector"
      confluent-hub install --no-prompt \
        debezium/debezium-connector-postgresql:2.5.4
      #
      echo "Launching Kafka Connect worker"
      /etc/confluent/docker/run &
      #
      sleep infinity

启动 Debezium 连接器任务(Starting the Debezium connector tasks)

基础设施就绪后,Kafka 尚未产生数据,因为 Kafka Connect 还未启动 Debezium 任务。我们需要通过 Kafka Connect 的 REST API(8083) 注册连接器配置。将配置写入 register-postgres.json 作为请求负载:

{
  "name": "pg-debezium-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "tasks.max": "1",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "myuser",
    "database.password": "mypassword",
    "database.dbname": "postgres",
    "topic.prefix": "hudi_tdg",
    "time.precision.mode": "connect",
    "tombstones.on.delete": false,
    "table.include.list": "public.maintenance_schedule",
    "signal.data.collection": "public.debezium_signal",
    "signal.enabled.channels": "source,kafka",
    "signal.kafka.topic": "signal-topic",
    "signal.kafka.bootstrap.servers": "broker:29092"
  }
}

在此,我们将 io.debezium.connector.postgresql.PostgresConnector 设为连接器类型,使任务通过 5432 连接到 postgres 数据库。Debezium 的信号机制(signaling)通过 signal.* 配置启用,用于触发数据库端操作并进行相应记录(例如 CDC 初始需要对目标表进行快照)。实际数据将写入 Kafka 主题 hudi_tdg.public.maintenance_schedule(由 topic.prefixtable.include.list 共同决定)。

使用如下命令向 Kafka Connect 注册配置并创建任务:

curl -i -X POST -H "Content-Type:application/json" \
  http://localhost:8083/connectors/ \
  -d @kafka-connect/register-postgres.json

此时 Debezium 任务启动,开始扫描 Postgres WAL,并将 CDC 数据发送到 hudi_tdg.public.maintenance_schedule。当 Postgres 表有新变更写入时,连接器会实时把相应的变更数据产出到该主题。

到这一步,我们已完成上游源的配置,能够模拟飞机维护部门所用的基础设施,并为 maintenance_schedule 表提供样例源数据。

设置 Hudi Streamer(Setting Up Hudi Streamer)

如前所述,运行 Hudi Streamer 作业与运行标准的 Spark 应用类似:使用 spark-submit 指定主类与 JAR。此示例主类设为 org.apache.hudi.utilities.streamer.HoodieStreamer,并使用 hudi-utilities-bundle

/opt/spark/bin/spark-submit \
  --name hudi_tdg_ch08_hudi_streamer \
  --class org.apache.hudi.utilities.streamer.HoodieStreamer\
  /opt/hudi/jars/hudi-utilities-bundle_2.13-<HUDI_VERSION>.jar \
  ...
  --op UPSERT \
  --continuous

需要关注的两个关键选项:

  • --op(写操作)
    因为我们处理的是来自 Postgres 的数据(包含插入/更新/删除),将其设为 UPSERT,确保更新与删除正确应用到对应记录,在湖仓中复刻 Postgres 表。
  • --continuous
    为模拟真实场景中源源不断的新数据,将 Hudi Streamer 以持续模式运行。若不加此标志,应用将只处理一批上游数据后退出。

接下来配置Source(从 Kafka 读取)写入相关选项,并与本示例中的 Hive Metastore 数据目录进行同步。

配置 Source(Configuring the source)

Hudi Streamer 提供多种可插拔接口,其中之一是 Source。同时,Hudi 针对常见场景提供了开箱即用的实现,Debezium 数据抽取即属其一。我们将 --source-class 设为:

org.apache.hudi.utilities.sources.debezium.PostgresDebeziumSource

以消费包含 Debezium CDC 数据的 Kafka 主题。连接 Kafka broker 与 schema registry 还需提供以下选项,主要用于指定地址,使 Hudi Streamer 作为 Kafka 消费者持续拉取消息:

--source-class org.apache.hudi.utilities.sources.debezium.PostgresDebeziumSource
--hoodie-conf hoodie.streamer.source.kafka.topic=hudi_tdg.public.maintenance_schedule
--hoodie-conf hoodie.streamer.schemaprovider.registry.url=<url>  # 1
--hoodie-conf schema.registry.url=http://schema-registry:8081
--hoodie-conf bootstrap.servers=broker:29092

1 示例 URL:http://schema-registry:8081/subjects/hudi_tdg.public.maintenance_schedule-value/versions/latest

经 Kafka 传递的 CDC 数据遵循 Debezium 的特定 schema:例如,原始 Postgres 表的模式嵌套在 before 与/或 after 字段下,代表变更前后状态。PostgresDebeziumSource 已实现扁平化逻辑,从这些字段中提取所需字段以复刻原始表模式。

这正是使用 Hudi Streamer 的优势之一:无需自写这类常见转换逻辑,减少工程工作量。

配置 Hudi 写入(Configuring the Hudi writer)

使用 --target-base-path 指定目标 Hudi 表在湖仓存储中的路径。为正确复刻更新/删除,需要通过 hoodie.datasource.write.recordkey.field 指定与 Postgres 表主键对应的记录键字段。此外,将 hoodie.datasource.write.keygenerator.type 设为 非分区,并将 hoodie.datasource.write.precombine.field 设为 Postgres 提供的排序字段 _event_lsn,以获得期望的非分区表布局与正确的合并行为:

--target-base-path /opt/external_tables/maintenance_schedule
--hoodie-conf hoodie.datasource.write.recordkey.field=schedule_id
--hoodie-conf hoodie.datasource.write.keygenerator.type=NON_PARTITION
--hoodie-conf hoodie.datasource.write.precombine.field=_event_lsn

对接数据目录(Working with data catalogs)

如第 4 章所述,数据目录(data catalog)在数据平台架构中至关重要:既是查询引擎的入口,也是集中管理表的元数据中心。摄取作业通常需要与目录同步(sync) 。从第一天起,Hudi Streamer 就支持该能力(见图 8-7)。当设置 --enable-sync 时,Hudi Streamer 将按顺序对目标表执行由 --sync-tool-classes 指定的 sync 工具。这些工具会从目标表提取元数据(表属性、模式、以及可用时的分区值等),并调用目录服务 API 上传。

图示:Hudi Streamer 的数据目录同步流程,展示与 AWS Glue、Google BigQuery、Hive Metastore、DataHub 的连接与对应 sync 工具如何对接存储中的 Hudi 表。

image.png

图 8-7. Hudi Streamer 的数据目录同步流程

可配置多个 sync 工具(填入其全限定类名)以连接不同目录。Hudi 开箱即用提供的 sync 工具类见表 8-3

表 8-3. 支持的数据目录及对应 sync 工具类
数据目录Sync 工具类
AWS Glue Data Catalogorg.apache.hudi.aws.sync.AwsGlueCatalogSyncTool
Google BigQueryorg.apache.hudi.gcp.bigquery.BigQuerySyncTool
Hive Metastoreorg.apache.hudi.hive.HiveSyncTool
DataHuborg.apache.hudi.sync.datahub.DataHubSyncTool

除类名外,还需配置若干选项以确保能正确连接到目录服务,下一小节将说明。

Tip
你也可以给 Hudi Streamer 添加 Apache XTable 提供的 Hudi 扩展 JAR,以支持同步到 XTable,并转换为 Apache IcebergDelta Lake 等其他表格式,从而连接更多目录(如 Apache Polaris)。详见文档。

配置目录同步(Configuring the data catalog sync)

本示例选择 Hive Metastore 作为数据目录服务。Hive Metastore 需要后端数据库存储所有已注册表的元数据。演示中使用轻量的内嵌关系库 Apache Derby 作为后端;但生产环境不推荐 Derby(可扩展性有限),通常会使用 Postgres 等更可扩展的数据库作为后端。

连接 Hive Metastore 的配置如下:

--enable-sync
--sync-tool-classes org.apache.hudi.hive.HiveSyncTool
--hoodie-conf hoodie.datasource.hive_sync.mode=hms
--hoodie-conf hoodie.datasource.hive_sync.metastore.uris=\
  thrift://hive-metastore:9083
--hoodie-conf hoodie.datasource.hive_sync.database=hudi_tdg

在同步过程中,Hudi 表的元数据被提取并传递给 Hive Metastore 客户端,后者调用 API 将元数据发送至服务端。目录同步在 Hudi Streamer 成功提交写入之后同步执行(即与写过程“内联”),因此每次写入会增加少量额外时延。

Tip
尽管目录同步通常在 1 分钟内完成,但仍可能为下一批次的启动带来小延迟。若你更关注写入时延,可将 sync 工具作为独立进程运行。

触发 CDC(Triggering CDC)

当 Debezium 连接器从 Postgres 抽取变更并发送到 Kafka,且 Hudi Streamer 以 continuous 模式消费该主题时,端到端 CDC 流水线即告就绪。此时在 Postgres 表中执行插入/更新/删除,Hudi Streamer 将在湖仓中做相应的 upsert 以捕获这些变更:

INSERT INTO maintenance_schedule
    (schedule_id, aircraft_id, due_date, maintenance_type, technician_ids)
VALUES (5, 'AC002', '2024-11-15', 'routine', ARRAY[105, 106, 109]);

UPDATE maintenance_schedule
SET due_date = '2024-08-20'
WHERE schedule_id = 1;

UPDATE maintenance_schedule
SET technician_ids = array_append(technician_ids, 109)
WHERE schedule_id = 3;

DELETE
FROM maintenance_schedule
WHERE schedule_id = 4;

至此,结合 Debezium + Kafka + Hudi Streamer + 数据目录的完整链路即已运转:上游变更被实时捕获,经规范与同步后,落地到湖仓的 Hudi 表,并可由 Presto/Superset 等进行查询分析。

释放分析的力量(Unlocking the Power of Analytics)

恭喜!至此,你已为飞机维护(Aircraft Maintenance)部门搭建好了湖仓摄取流水线。随着 Hudi 表持续接收新写入并与数据目录(data catalog)保持同步,你现在可以借助强大的查询引擎分析看板来构建洞察。这使得基于 SQL 的灵活分析与可视化成为可能,从而释放数据的全部潜能。

用 SQL 校验数据(Verifying the data using SQL)

我们选择流行的 SQL 引擎 Presto,通过 Hive Metastore 目录来查询 Hudi 表。先在基于 Docker 的环境中进入 Presto CLI 控制台:

docker compose -f ../compose.yaml exec -i presto \
  presto-cli --catalog hudi --schema hudi_tdg

在控制台执行以下 SQL,列出表中的全部计划及相关信息:

SELECT schedule_id, due_date, maintenance_type, technician_ids
FROM maintenance_schedule
ORDER BY schedule_id;

该查询返回四行。结合“创建第一批数据(Creating the first batch of data)”与“触发 CDC(Triggering CDC)”中的 SQL,我们可验证:

  • schedule_id = 1 的到期日已更新为 2024-08-20
  • 技术员 109 已被添加到计划 3
  • 计划 4 已被删除。
 schedule_id |  due_date  | maintenance_type | technician_ids
-------------+------------+------------------+-----------------
           1 | 2024-08-20 | corrective       | [101, 102, 103]
           2 | 2024-09-01 | routine          | [104, 105]
           3 | 2024-07-30 | routine          | [106, 109]
           5 | 2024-11-15 | routine          | [105, 106, 109]
(4 rows)

这表明对 Postgres 表的更改已成功传递到终端用户,且流水线各组件工作正常。为其他部门的数据集构建类似流水线后,分析师即可联接多样数据集执行更复杂分析查询,获得更深洞察,并为业务需求提供更契合的解决方案。

用仪表盘可视化数据(Visualizing the data using dashboards)

通过仪表盘进行可视化,是让干系人接收业务洞察的有力方式。演示中我们使用可视化平台 Apache Superset 在仪表盘上构建一个简单图表,以清晰、交互的方式展示由数据得出的洞察。

Superset 支持连接多种数据源,包括 Presto。一旦配置为连接示例 Docker 栈中的 Presto 实例,Superset 即可访问注册在 Hive Metastore 目录下的 Hudi 表。本例中,maintenance_schedule 可作为 Superset 数据集的数据源来创建图表并放入仪表盘。

图 8-8 展示了飞机维护部门的一个简单仪表盘示例。饼图“Maintenance types(维护类型)”统计不同计划类型出现的次数并展示其分布。仪表盘可设置为定期刷新;随着 Hudi Streamer 处理新数据,用户将在图表中持续看到最新信息

图示:展示维护类型分布的饼图,例中“routine(例行维护)”占比高于 “corrective(纠正性维护)”。

image.png

图 8-8. 维护类型分布的样例分析仪表盘

探索 Hudi Streamer 选项(Exploring the Hudi Streamer Options)

Alcubierre 的团队很快体会到以 Hudi Streamer 作为标准化摄取框架后工作流被显著简化。随之,公司决定成立专门的 Infra 团队,负责运行 Hudi Streamer 应用的基础设施与各部门在湖仓上使用的配置集管理。该团队作为 Hudi Streamer 跨部门作业的一线支持,创建模板以便各部门按业务需求裁剪配置集,保障平稳运行并处理问题。Infra 团队梳理了 Hudi 文档与示例,发现了许多实用的 Hudi Streamer 选项,本节将加以介绍。

Hudi Streamer 的功能面广、选项多,对新手可能略显繁杂。为便于理解与使用,我们按功能对选项分类。表 8-4 给出八大类与其关联选项。

表 8-4. Hudi Streamer 支持的可用选项(Available options)

类别(Category)描述(Description)选项(Options)
General通用功能,例如打印帮助、传递 Hudi 配置。--help--hoodie-conf--props
Writer控制 Hudi Streamer 使用的 writer 行为。--target-base-path--target-table--table-type--op--filter-dupes--base-file-format--payload-class--commit-on-errors
Bootstrap控制目标 Hudi 表的引导(bootstrapping)操作。--run-bootstrap--bootstrap-overwrite--bootstrap-index-class
Source定义上游数据源并控制消费行为。--source-class--source-ordering-field--source-limit--schemaprovider-class--transformer-class
Checkpoint控制 checkpoint 行为。--checkpoint--initial-checkpoint-provider--ignore-checkpoint--allow-commit-on-no-checkpoint-change
Catalog sync控制与数据目录的同步行为。--enable-sync--force-empty-sync--sync-tool-classes
Table service管理表服务(如压缩/聚类)的调度。--retry-last-pending-inline-clustering--retry-last-pending-inline-compaction--max-pending-compactions--max-pending-clustering--compact-scheduling-weight--compact-scheduling-minshare--cluster-scheduling-weight--cluster-scheduling-minshare--disable-compaction
Operational控制作业的运行/运维行为。--continuous--min-sync-interval-seconds--delta-sync-scheduling-weight--delta-sync-scheduling-minshare--retry-on-source-failures--retry-interval-seconds--max-retry-count--post-write-termination-strategy-class--ingestion-metrics-class--config-hot-update-strategy-class--spark-master

在大多数情况下,Hudi Streamer 的摄取作业只需针对特定用例选择少量选项即可。接下来的内容,我们将对General / Source / Operational 三类补充一些先前未覆盖的细节;要获取完整选项介绍,请参阅 Hudi 文档页面。

General 选项(General Options)

此类为通用选项。一个关键用法是以键值对的形式将任意 Hudi 配置或属性传给 Hudi Streamer 作业,使底层的 Hudi 写客户端据此生效。可重复的 --hoodie-conf 接受 key=value 形式的配置;或使用 --props 指定 .properties / .conf 文件路径,从文件批量加载配置。

示例:

--hoodie-conf hoodie.upsert.shuffle.parallelism=100
--hoodie-conf hoodie.delete.shuffle.parallelism=100
--props file:///etc/conf/hudi.dev.properties

注意:--hoodie-conf优先级最高(高于 --props 与其它同名命令行项);而通过 --props 指定的配置优先级最低

Tip
一个 .properties 文件可在首行通过 include=<other properties file> 引入其他属性文件。若当前文件后续行中重新定义了同名属性,则会覆盖被引入文件中的值。该模式常用于在湖仓平台上定义大多数 Hudi Streamer 作业的基础属性,然后在领域特定的属性文件中 include 并覆盖少量差异配置,避免重复、便于管理。

Source 选项(Source Options)

我们已在“Hudi Streamer 入门(Getting Started with Hudi Streamer) ”中通过 --source-class 介绍了 Source 抽象,并在“Hudi Streamer 实战(Hudi Streamer in Action) ”里为演示应用完成了配置。至此,我们已清楚地了解了 Hudi 对多种上游数据源的摄取支持。为进一步扩展信息,本节将简要介绍更多可用的 Source 实现。

  • ParquetDFSSource
    用于从文件存储系统读取原生 Parquet 文件,存储系统可以是本地文件系统、HDFS,或云对象存储(如 AWS S3GCSAzure Blob Storage)。
  • HoodieIncrSource
    通过增量查询(incremental queries)Hudi 表作为 Source 读取。当需要对表中数据做进一步处理(如与其他表关联)时尤为有用。该实现使用带 checkpoint 的时间戳作为增量查询参数,仅获取来源 Hudi 表中变更的数据(新增或更新) ,从而减少冗余处理、提升流水线效率。

其他值得注意的 Source 实现包括:

  • MysqlDebeziumSource
    PostgresDebeziumSource 类似,但消费来自 MySQLCDC 数据。
  • ProtoKafkaSource
    消费包含 Protobuf 编码消息的 Kafka 主题。
  • PulsarSource
    消费来自 Apache Pulsar 的数据。

--schemaprovider-class 选项用于定义 Hudi Streamer 如何获取 Source 数据的模式(schema) 。在“入门”部分我们看到,Alcubierre 的安全与安保部门使用了 SchemaRegistryProvider 来处理模式演进场景。另一种常用的提供者是 FileBasedSchemaProvider,指向一个 Avro schema 文件以提供模式。注意:并非所有 Source 都需要 schema provider,例如 ParquetDFSSource自提供模式信息。

--source-ordering-field 选项用于指示 Source 模式中的排序字段,决定记录间的先后,这等价于 hoodie.datasource.write.precombine.field:允许在落盘前对输入记录进行合并,节省计算成本。

--source-limit 选项为每次从 Source 抓取数据设置上限,以更好地控制摄取过程。上限单位可能是字节消息数量,取决于具体的 Source 实现。

运维选项(Operational Options)

Hudi Streamer 提供多项能力以简化运维,可用于指定运行模式、控制重试行为、微调调度优先级等。

运行模式(Operation modes)

Hudi Streamer 有两种运行模式:

  • Run once(默认)
    面向一次性批量摄取。作业在处理完抓取到的源数据后自动退出。适用于周期性批处理,通常需外部调度工具触发作业。
  • Continuous
    通过添加 --continuous 启用(见“设置 Hudi Streamer”)。该模式下,Streamer 在循环中持续抓取源数据(受 --source-limit 限制)并写入存储。适用于处理无界流自我批处理的一系列输入批次。

最小同步间隔(Minimum sync interval)

--min-sync-interval-secondscontinuous 模式配合使用,用于定义相邻摄取周期之间的最小间隔(秒) 。例如:

  • 若一次摄取耗时 40s,而最小间隔设为 60s,则 Streamer 会暂停 20s 再开始下一轮。
  • 若一次摄取耗时 70s,则会立即开始下一轮,不再等待。

此特性有助于确保上游积累到足够数据再处理,降低产生小文件的可能,避免影响性能。

优雅终止(Graceful termination)

要优雅地停止持续运行的 Streamer,可实现自定义 --post-write-termination-strategy-class 来定义终止条件。例如 org.apache.hudi.utilities.streamer.NoNewDataTerminationStrategy:在指定次数的拉取轮次后若仍无新数据则结束循环。

当处理的数据体量大、通常需要大量资源时,这种策略尤其有用。你可以将数据分批处理,让 Streamer 在较小的集群上持续运行;待摄取完成后自我终止,以优化资源使用。

其他运维选项(Other operational options)

  • 错误处理与重试:--retry-on-source-failures--retry-interval-seconds--max-retry-count 用于定义作业遇到错误时的行为。
  • 调度优先级与资源份额:--delta-sync-scheduling-weight--delta-sync-scheduling-minshare 用于告知 Spark 调度器在与同一应用中运行的表服务(table services)作业之间,应给予摄取工作多大权重/最小份额。关于调度优先级的详细说明可参见 Spark 文档。

总结(Summary)

本章系统性地构建了一个数据湖仓平台:先回顾了虚构航空公司 Alcubierre 在各部门数据孤岛下的挑战(各自为政的存储让跨部门洞察与流程优化困难),并给出湖仓架构可显著缓解这些问题的结论。

为打造完整的湖仓平台,我们引入 Hudi Streamer 作为摄取层核心,说明其多样特性与选项如何对应地解决 Alcubierre 的痛点;随后聚焦某部门用例,构建端到端应用,演示湖仓所需的工作配置与服务,并通过接近实战的示例展示其收益。

最后,我们扩展介绍了更多 Hudi Streamer 的重要选项及其实际应用,为在实践中建设湖仓提供可操作的洞见。
在下一章(第 9 章),我们将深入讨论生产环境中的更多场景与用例,并探讨 Hudi 如何用其能力解决关键业务挑战。