Apache Hudi权威指南——Hudi 中的并发控制

65 阅读36分钟

在数据库与数据湖仓(data lakehouse)的世界中,并发控制(concurrency control)是一个关键概念,它在面对多个并发操作时确保数据的完整性与一致性。并发控制定义了不同进程(无论读还是写)如何协调对共享数据的访问,以避免冲突并维持数据完整性。并发控制之所以重要,是因为如果缺乏它,不受协调的数据访问会导致各种异常,例如更新丢失(lost updates)、脏读(dirty reads)以及数据不一致。

想象一个电商平台:两位顾客同时尝试购买一款高需求商品的最后一件库存。如果系统缺乏恰当的并发控制,两笔交易都可能在“看起来有库存”的情况下继续,最终导致其中一位顾客为已缺货的商品被扣款。此类场景会引发用户投诉、增加退款处理成本,并损害品牌声誉。通过实施并发控制机制,系统能够确保仅有一笔交易成功,从而防止超卖并维持准确的库存记录。

在第 3 章,我们概述了 Apache Hudi 的写入流程;而在本章,我们将更深入地探讨 Hudi 如何处理并发操作,以防范上述问题。

为什么数据湖仓中的并发控制更难
许多数据库系统通过并发控制机制来处理多写与多读。例如,PostgreSQL 使用 MVCC(多版本并发控制,Multi-Version Concurrency Control),在写入方通过悲观的行级锁等技术修改数据的同时,让读取方访问一致性的快照。

联机事务处理(OLTP)数据库针对单条记录操作,使用具备事务能力的存储引擎,为读写提供毫秒级时延。与之相对,数据湖仓面向海量存储,写事务往往长时间运行,从持续数分钟(例如准实时摄取)到数小时(例如大规模机器学习训练管道),而读取则以大规模列式(OLAP)扫描为主。这使得将 OLTP 中的并发控制技术(如细粒度锁或版本化)“原样”应用到湖仓变得困难,容易引入显著的性能开销或扩展瓶颈。

此外,湖仓通常依赖云对象存储(如 Amazon S3、GCS)来获得可扩展的存储能力。尽管这些存储已逐步提供了对象级操作的强一致性(例如写后读一致),但它们并不具备传统数据库那样原生的、多操作的事务能力,这会让复杂提交的原子性变得更难。因此,数据湖仓中的并发控制需要在分布式环境下,在一致性、可扩展性与性能之间寻求创新性的平衡。

鉴于数据湖仓的分布式特性以及底层存储系统在事务保证方面可能的限制,支持多写入者对于可扩展性与效率至关重要。单写入者模型也许能满足简单场景,但在现代湖仓中它很快会成为瓶颈:数据从多个来源以巨大吞吐涌入,插入、更新、删除与聚类(clustering)等各类操作必须并发且安全地运行。单写入者强加的顺序执行会拖累性能、限制扩展性,还会引入单点故障,从而增加运维风险。

除了速度层面,支持多写还能让组织将不同工作流拆分为独立的管道。例如,数据摄取、回填(backfilling)与聚类可以并行调度与执行,从而优化资源利用率。聚类操作(如排序或加密)可在离峰时段运行而不拖慢摄取管道;同时,合规性任务(如 GDPR 要求的删除)也能在不打扰实时数据流的情况下推进。Hudi 通过在保持一致性的前提下支持并发写,来应对这些挑战,确保湖仓工作负载在数据量与复杂性不断增长时依然高效扩展、灵活演进。

在管理并发写入者是首要挑战的同时,一个健壮的系统也必须保证读操作与这些写操作隔离,避免向用户与下游应用暴露不一致或不完整的数据。必须确保读取不会受到进行中的写入(inflight writes)的影响,否则会向下游暴露不一致或不完整的数据。Hudi 采用快照隔离(snapshot isolation) 来解决这一问题:无论写入是否正在进行,读者总能看到数据集的一致性快照。通过提供此类保证,Hudi 让读写双方在不引入传统加锁开销的情况下都能基于稳定可靠的数据进行操作,从而解决前文提出的那些问题。

并发控制技术

现代系统中的并发控制可通过多种技术实现。OCC(乐观并发控制,Optimistic Concurrency Control) 是其中一种:系统假设冲突较少,允许多个进程并行推进;在提交阶段再检查并按需解决冲突。这种方式能将加锁开销降到最低,但一旦检测到冲突,可能需要重试。在数据湖仓中,这些重试的代价可能非常高,因为长达数小时的写入成果可能被放弃并需要重新执行。

另一种广泛使用的方法是 MVCC(多版本并发控制) ,让读写方在不同版本上工作。读取方基于其事务开始时刻对应的数据快照进行操作,而写入方生成数据的新版本。该方法确保隔离并避免阻塞读者。Hudi 还为湖仓工作负载设计了一种更为契合的方式:NBCC(非阻塞并发控制,Non-Blocking Concurrency Control) 。各进程写入增量变更(delta changes),随后依据提交顺序或记录中的排序字段,以确定性的方式在后处理阶段解决冲突。该方式对流式工作负载尤为有效,因为它能够最大限度降低时延并确保零停机。

Hudi 审慎地组合使用这三种技术,构建出既稳健又高性能、并贴合数据湖仓独特需求的并发模型:它使用 OCC 来管理并发写入者之间的冲突,确保更新以一致的顺序串行化;使用 MVCC 在写入者、读取者与表服务(table services)之间提供快照隔离,使得诸如压缩(compaction)与清理(cleaning)等表服务可以异步运行而不阻塞写入;此外,NBCC 面向高吞吐湖仓表,允许多个写入者与并发表服务在同一张表上协同而不互相失败,从而避免 OCC 带来的大规模算力浪费。通过组合这些机制,Hudi 允许多个进程并发运行,同时维持一致性、隔离性与性能。

本章将详细解释 Hudi 如何在分布式数据湖仓中管理并发。通过深入其并发控制机制,你将了解 Hudi 如何在保持数据一致性的前提下,使数据摄取、更新与删除等操作得以协调、同时地进行。这将帮助你设计并运营可扩展、高性能的数据平台,高效应对多并发工作负载而不牺牲可靠性。

需要注意的是,尽管 Hudi 的并发控制机制可以防止并发写入导致的数据损坏,但它们并不会自动解决所有业务层面的逻辑冲突。例如,在 OCC 场景中,如果两个写入者处理了同一份源数据,仍可能插入重复记录。Hudi 保证底层表结构始终保持一致,但用户仍需确保其数据管道具备幂等性(idempotent),并通过合理分区等方式避免逻辑不一致。

多写入(Multiwriter)场景

在现代湖仓(lakehouse)环境中,支持**多个写入者(multiwriter)**不只是一个特性,而是一种必需。对高吞吐、运维灵活性与高效资源利用的需求,使得单写入(single-writer)系统在大多数真实场景中并不足够。Hudi 通过允许多个写入者并发运行、同时保证数据一致性与完整性,来满足这一需求。

为什么需要多写入

多写入系统之所以不可或缺,主要有以下原因:

应对资源限制
当涉及大规模数据摄取或处理工作流时,单个写入者很快会成为瓶颈。多写入能力可以把负载分散到多个进程上,从而提升吞吐与可扩展性。

支持相互独立的管道
摄取(ingestion)、ETL、回填(backfilling)与聚类(clustering)等不同操作在资源与优先级上差异很大。将它们拆分为独立管道不仅提高效率,也降低运维开销。

减少运维延迟
关键写操作往往需要不被其它数据管理任务拖慢。多写入系统可确保清理(cleaning)、压缩(compaction)等尽力而为(best-effort)的表服务不会干扰对时效敏感的数据管道。

成本效率
将高优先级/高资源任务与低优先级任务分离,便于更有效地分配资源。例如,回填可在离峰时段使用低成本资源执行,而常规写入得以不间断进行。

可扩展性
多写入使写入可以水平扩展,从而在不牺牲性能的前提下应对不断增长的数据量。

适用于 OCC 的多写入场景

典型的数据湖仓多写支持使用 OCC(乐观并发控制,Optimistic Concurrency Control) ,可应对写入者在彼此独立的数据子集上操作、从而将冲突风险最小化的情况。以下是一些基于 OCC 表现良好的常见场景。

历史数据回填(Backfilling data)
设想一家金融服务公司要把多年的交易记录从旧系统迁移到一张 Hudi 表。一个专用的回填写入者可以与主写入管道并行运行,把历史数据补入,而不减慢或干扰新交易的写入。

删除过期数据(Deleting older data)
处理敏感客户数据的组织需要遵循 GDPR 等法规,在到期后删除某些记录。例如某保险公司需要清理五年前的部分记录。一个独立写入者可以负责这些删除,而不打断实时订单的摄取,从而在不影响应用性能的情况下确保合规。

使用聚类服务进行后处理(Post-processing via clustering)
大型分析平台常需定期重组数据以提升效率。以视频流媒体服务为例,其在湖仓中存放用户互动日志。为加速查询,可按用户 ID 或时间区间进行聚类。与其给主摄取流程增加负担,不如在离峰时段用独立写入者优化文件大小并排序,从而提升分析型读取性能。

扩展摄取/ETL(Scaling ingestion/ETL)
在数据密集环境中,依赖单一管道会成为高吞吐表的瓶颈。比如网络安全平台需要每小时从上千台服务器摄取 TB 级日志。通过将多个写入者分配到 Apache Kafka 主题的不同分区上,可高效分摊摄取负载,避免积压并提升总体吞吐。

适用于 NBCC 与 MVCC 的多写入场景

涉及对**相同文件组(file group)**的重叠写入会引发冲突,导致资源浪费或操作中止。尤其在以下情况中,不建议使用 OCC 的多写入;相反,应采用 NBCC(非阻塞并发控制,Non-Blocking Concurrency Control)MVCC(多版本并发控制,Multi-Version Concurrency Control) 以获得更顺畅的运行。

数据修改重叠(Overlapping data modifications)
设想两位数据工程师分别基于不同来源更新零售库中的客户记录,如果两者同时修改同一批用户资料,其中一次写入可能被中止,既浪费算力,也造成数据不一致。在这种情况下,使用 NBCC 以确定顺序或合并冲突写入更有效。例如,Hudi 可基于提交时间(commit time ordering)选择最新提交的记录,或基于来源事件时间(event time ordering)选择时间戳更高的记录,从而两方写入都不失败;而基于 OCC 的做法,即便只有一条记录发生交集,也会导致其中一个写入失败。

高争用负载(High-contention workloads)
当写入者与表服务在同一文件组或记录上持续争用时(例如某超大规模平台以每分钟 100 GB 的速度向 Hudi 表写入),难以承受基于 OCC 的反复失败与重试。此时采用 MVCC 有助于让压缩与写入在同一共享版本上协同工作。

简单默认:单写入 + 表服务

尽管多写入在许多场景中至关重要,但最简单且最常见的用法仍是不存在并发写入的单写入者。开箱即用时,Hudi 以此模型运行,无需外部锁提供者即可开始,从而简化架构。对较简单的工作流而言,单写入配合表服务往往已经高效且足够。

单写入 + 内联表服务(inline)
在单写模型中,清理、压缩、聚类等表服务可在每次写入后内联执行,从而无需额外的并发控制也能完成表的优化与管理。内联表服务具有幂等性,可在失败时重试,并会自动写入时间线(timeline)。

单写入 + 异步表服务(async)
或者,表服务可在后台异步运行,让写入者不被阻塞地持续摄取数据。对于耗时较长的操作(如压缩),异步模式尤其有用。Hudi 借助 MVCC 确保这些表服务能与写入者并发而不发生冲突。

下表(表 7-1)概述了在不同写入工作负载与并发读取下,不同并发控制机制所提供的保证。

表 7-1 多写入场景中的保证(Guarantees in multiwriter scenarios)

场景UPSERT 保证INSERT 保证BULK_INSERT 保证增量查询(INCREMENTAL QUERY)保证
单写入(Single writer)无重复无重复无重复无乱序数据
多写入(OCC)无重复可能出现重复(若无自定义冲突解决)可能出现重复无乱序数据
多写入(NBCC)无重复可能出现重复(若无自定义冲突解决)可能出现重复

注:表中 “无重复/可能出现重复” 指写入层面的去重/冲突处理保证;增量查询保证强调结果中事件时间/提交时间的有序性。NBCC 行对增量查询的保证需结合具体实现与配置,一般通过事件/提交时间排序策略维持查询有序语义。

Hudi 如何处理并发控制(Concurrency Control)

一个良好的多写入(multiwriter)系统应具备若干关键特性,以确保并发操作高效可靠:当不存在对同一底层数据的冲突写入时,应允许多个写入者并发写入与提交;同时应具备最小化、检测并解决修改同一数据时写入者之间冲突的机制。为覆盖多样化用例与工作流,它还应提供可插拔接口,允许用户定义自有的冲突解决策略。为简化架构并提升可扩展性,一个优秀的多写入系统应尽量减少对额外外部组件的依赖,并降低对长时间持有锁的依赖。此外,它还应允许表服务(table services)与写入者并发运行且不相互阻塞,确保数据管理顺畅高效。

Hudi 的并发控制设计天然支持以上全部特性。

Hudi 并发控制的基础

Hudi 并发控制的核心是快照隔离(snapshot isolation) :无论是写入者、表服务还是读取者,都在一张表的一致性快照上操作。为实现这一点,Hudi 组合使用 OCC(乐观并发控制,Optimistic Concurrency Control)MVCC(多版本并发控制,Multi-Version Concurrency Control)NBCC(非阻塞并发控制,Non-Blocking Concurrency Control) 。这些机制协同工作,提供稳健的并发处理框架。举例而言,即便采用 MVCC 与 NBCC,各进程对其并发操作所基于的快照也有共享共识,并通过短生命周期的分布式锁来实现。

快照隔离(Snapshot isolation)

快照隔离是 Hudi 在多进程间维持一致性的关键能力。当某个写入者将更改提交到表时,这些更改不会立即对读取者可见。读取者会继续访问写入开始之前的那一版一致性快照。这样即使写入正在进行,读取者也不会看到部分写入或不一致的数据。

OCC

Hudi 使用 OCC 来管理写入者之间的冲突。在 OCC 下,写入者以“冲突较少”的假设乐观推进操作;若两个写入者同时尝试修改同一文件组(file group),Hudi 会检测到冲突,并通过中止其中一个写入来解决。

不过,OCC 也有挑战:在冲突频发的场景,中止与重试的代价会变得显著。为缓解此问题,Hudi 引入早期冲突检测(early conflict detection) (本章后文详述),在数据写入阶段就识别潜在冲突,并尽早中止冲突写入,从而减少算力浪费并提升整体效率。

MVCC

MVCC 是 Hudi 用来在不同类型操作之间提供 NBCC 的基础理念。可将其视作一种让读取者、写入者与表服务互不干扰的机制。MVCC 允许多个版本并存:读取者可在一致性快照上读取,而写入者与表服务在后台修改数据。比如,压缩(compaction)作业可在后台安全地重写数据文件;在它生成新版本文件的同时,一个在压缩开始前已启动的长查询仍将读取旧的、一致的文件版本。这样既避免读取者看到部分结果,也避免维护性操作阻塞写入。

NBCC

Hudi 的 NBCC 是为在同一张表上处理同时写入无需因冲突而中止任何一方写入而设计的精巧机制。该方式显著提升摄取吞吐、减少写失败,尤其适合高并发流式场景。它使多个写入者能够在同一张表、甚至同一文件组上工作,并由查询读取端与压缩器在后续自动化解冲突

NBCC 的核心利用了一种基于提交完成时间(commit completion time)的新颖文件布局策略,并引入“TrueTime 语义”(见“About TrueTime Semantics”),以在所有写入者间确保全局时间戳单调性。当多个写入者尝试写入同一文件组时,NBCC 不会阻塞或让某一方失败,而是允许双方继续写入:它为每个并发写入创建独立的文件切片(file slice) ,并在读取阶段异步压缩过程中解决冲突。这与传统 OCC 的“同组冲突即失败并重试”有本质区别。

Hudi 1.0 引入的 LSM 时间线(LSM Timeline,Log-Structured Merge Timeline) 是 NBCC 的基石。LSM Timeline 以可扩展且高效的方式管理表元数据,对跟踪并发写入及其状态至关重要。它为每个动作记录请求时间(requested time)完成时间(completion time) ,从而在多写并行下依然维持表的一致视图。时间线中详尽的记录使系统能为读取者正确重构数据状态,在不引入传统加锁瓶颈的前提下保证一致性与完整性。

图 7-1 展示了当多个写入者尝试写入相同文件组时,OCC 与 NBCC 的对比:OCC 会让第二个写入者失败,而 NBCC 则允许两者并进。

image.png

图 7-1. OCC 与 NBCC 的对比(示意:OCC 会阻塞/失败第二个写;NBCC 通过并行文件写入与分片允许同时推进)

三阶段提交过程(The Three-Step Commit Process)

Hudi 通过一个三阶段提交流程(见图 7-2)来实现并发控制,确保写入既具原子性又保持一致性。该流程旨在最小化锁竞争、允许多个写入者并发工作,包含以下阶段:

阶段 1:Request(请求)
写入者在时间线上记录写入意图。此步骤会生成该事务的请求时间,在 OCC 中仅作为事务 ID,在 NBCC 中用于为文件切片排序。

阶段 2:Inflight(进行中)
写入者(可选地)记录其计划更改的内容,包括将写入或修改的文件组。Inflight 阶段不需要加锁,多个写入者可以并行推进,写入新的基文件或日志文件。此阶段与其他写入者并行进行,且在最终提交前,这些更改对读取者不可见。

阶段 3:Commit(提交)
写入者更新时间线以反映其已完成的更改。这是原子提交点,此时需要再次获取一个短时分布式锁:其一,用于生成完成时间以对写入进行排序;其二,确保没有其他写入者在同一时刻提交互相冲突的更改。提交完成后立即释放锁,更改自此对读取者可见。

这一设计在并发与一致性之间取得平衡:在保持强正确性保证的同时尽可能提高并行度。系统通过确保读取者始终看到最近一次成功提交的版本来维持一致性。

image.png

图 7-2. 三阶段提交与加锁时序(示意:两个并发写入者在 requested 与 inflight 阶段并行推进;在 requested 阶段获取锁生成时间戳,在 commit 阶段获取锁完成提交;写入者 1 先提交创建新快照,写入者 2 随后提交且仅在无冲突时成功)

注意(Note)
尽管多个写入者可能几乎同时发起操作,Hudi 的时间线会对动作进行严格的顺序化。底层锁提供者确保即便在近乎同时的提交尝试下,也会串行化:总有一方先成功,其结果将作为后续冲突检查的基准。

该协议的成功依赖于对文件状态与元数据的精细管理。每个阶段都会记录进时间线,形成清晰的审计轨迹,并在故障时支持恢复。并将并发操作的精密编排与可插拔的加锁与冲突解决部件结合,使 Hudi 成为构建可扩展湖仓方案的有力平台。这一设计深刻把握了分布式数据处理中的固有挑战,在性能、一致性与运维复杂度之间给出了务实解法。

冲突检测与解决(Conflict Detection and Resolution)

在并发系统中,当多个写入者同时修改相同数据时,冲突在所难免。Hudi 的处理方式务实且高效:系统允许多个写入者尽量并发推进,从而最大化无冲突路径的吞吐。

当确实发生冲突且需要选出“胜出”的写入或表服务操作时,Hudi 采用一个偏向简洁与一致性的默认策略:SimpleConcurrentFileWritesConflictResolutionStrategy。该策略允许多个写入者在不修改同一文件组的前提下各自提交;若两个写入者试图修改同一文件组,则后提交的一方会被中止。此默认做法覆盖了湖仓上最常见的并发模式,但在部分场景可能不够,因此 Hudi 提供可插拔的冲突解决接口,便于组织按需实现自定义策略。比如,你可以实现一个策略:实读(read out)冲突提交的**记录键(record keys)**并在检测到重复键时中止写入。这样会为每次写入增加 I/O 开销(通常湖仓用户不偏好),但 Hudi 给予了这种强大的灵活性。

进一步说明,参见图 7-3:若两个写入者尝试在同一文件组内修改记录,写入者 1 开始提交并获取锁;当写入者 2 尝试提交时,Hudi 的冲突检测机制会识别到文件组重叠。在默认配置下,写入者 2 的操作将被中止并需重试。而自定义策略则可能实现更复杂的处理,例如合并更改,或依据业务规则按序应用。

image.png

图 7-3. Hudi 冲突解决协议示意(显示多个写入者在 inflight 与 commit 阶段遇到冲突,以及写入者 1 的早期冲突检测)

加锁机制(Locking Mechanisms)

为在分布式多写(multiwriter)环境中协调写入,Hudi 支持多种分布式加锁机制,各有优劣。下文列出若干最常用的机制,并在表 7-2中作特性对比。具体选择取决于用例需求与可用基础设施。关于每种提供者(provider)的配置细节,见“Configuring the Locking Mechanism”。

基于 Zookeeper 的加锁

该机制使用 Apache ZooKeeper 在多个写入者之间协调锁。ZooKeeper 是高度可靠的分布式协调服务,提供强一致性保证,但在大规模部署中可能带来额外运维开销。若组织已在基础设施中使用 ZooKeeper,那么它会是自然之选,能无缝融入既有运维实践。

在缺乏自定义锁机制的云环境(如 Google CloudMicrosoft Azure)下,最常见且稳健的做法是在虚拟机(如 Google Compute Engine 或 Azure VMs)上运行 ZooKeeper 集群。这与本地部署提供同等的强一致性保证,也是任何云上运行 Hudi 的成熟模式。

基于存储的加锁(Storage-based locking)

该机制利用现代云存储系统的条件写(conditional writes) ,通过主选举(leader election) 算法实现分布式加锁。Amazon S3 近期已引入条件写,GCSAzure Storage 也已支持。每个进程都会尝试对一个基于表根路径计算出的文件执行一次原子条件写;第一个成功者被选为领导者(leader) 并负责独占操作。该方法无需外部服务,简单可靠、成本可控、对基础设施要求轻,是云原生场景的高性价比选择。

基于 Amazon DynamoDB 的加锁

该机制使用 DynamoDB 作为分布式锁提供者。DynamoDB 具备极高可扩展性且易于管理,但在大规模部署中可能带来额外成本。在云端,尤其 AWS 环境下,这是云原生的优选方案;其按用量计费在负载波动的场景中也可能更具成本效率。

自定义加锁机制

对于高级用例,Hudi 的可插拔锁接口允许实现自定义提供者,可利用云原生服务(如 Google 的 Zonal LockAzure 的 Blob Lease API)来构建。

表 7-2 各类加锁机制对比(Comparison of various locking mechanisms)
加锁机制外部依赖可扩展性一致性成本适用性
Zookeeper需要专用 ZooKeeper 集群适当配置下高度可扩展,可处理成千上万并发操作通过主选举与分布式一致性提供强一致性维护 ZK 集群的中等基础设施与运维成本适合大规模、跨机房生产部署,且对强一致性要求高
Storage(存储)复用用于表存储的同一分布式存储(如 HDFS、Amazon S3)取决于底层存储,可承载大规模并发一致性取决于存储系统视存储而定;对既有基础设施通常成本友好适合利用既有分布式存储的云原生部署
DynamoDB需要 AWS 账户与 DynamoDB 表卓越的自动扩缩容,可处理百万级操作行级 ACID 事务带来强一致性按用量计费;成本随使用增长非常适合 AWS 部署,尤其无服务器与云原生应用
内存(单写入)受限于单个 JVM进程内强一致性无额外成本适用于开发、测试与简单的单写部署

上述各机制各有取舍:例如 ZooKeeper 可靠性强但引入基础设施复杂度;DynamoDB 云集成顺滑但需关注成本。不论选择何种机制,Hudi 的设计都确保只在提交阶段短暂持锁、尽量降低竞争并提升并发度,以避免锁成为瓶颈。

选择加锁机制时,应综合考虑外部依赖、可扩展性、一致性保证与成本等因素,评估权衡,选出与自身运维需求与基础设施最契合的方案。

多写系统的挑战(Challenges in Multiwriter Systems)

多写系统的一个基本挑战,是如何高效地将数据划分为彼此独立、可并发处理的部分。Hudi 通过其文件组(file group)抽象来应对,它提供了天然的并行单元;写入者可同时作用于不同文件组,实现写入的水平扩展。

系统还必须有效进行资源分配。当多个写入者争夺内存、CPU 与网络带宽等资源时,系统需要在保持公平的同时避免死锁(deadlock)饥饿(starvation) 。Hudi 通过限制锁的作用范围与持有时长、并提供早期冲突检测(early conflict detection) 等机制,降低资源争用。

早期冲突检测对资源效率尤为重要。早期版本的 Hudi 常在提交时才发现冲突:写入者可能完成了整次作业却在提交阶段失败,造成大量算力浪费。引入早期冲突检测后,一旦在写入阶段识别出冲突,写入者即可尽早中止,迅速释放本应被“注定失败”的操作所占用的资源。

在 Hudi 中使用多写入(Multiwriter)支持

要在 Hudi 中启用多写入支持,需要进行仔细配置,以确保多个写入者能够并发运行且不发生冲突。本节给出启用多写入的分步指南,涵盖所需配置、加锁机制以及常见用例的代码示例。

启用多写入支持(Enabling Multiwriter Support)

要启用多写入支持,需要在 Hudi 的 properties 文件或作业配置中设置相应参数。关键配置如下:

  • hoodie.write.concurrency.mode
    将其设置为 optimistic_concurrency_control 以启用多写入(OCC,乐观并发控制)。
  • hoodie.write.lock.provider
    指定要使用的加锁机制(例如 ZookeeperBasedLockProviderHiveMetastoreBasedLockProviderDynamoDBBasedLockProvider)。
  • hoodie.cleaner.policy.failed.writes
    设为 EAGER,以确保失败写入被及时清理,防止阻塞其他写入者。
  • 与锁提供者相关的专用设置(Lock provider–specific settings)
    为所选的锁提供者配置必要的参数(详见“Storage-based locking”一节的说明)。

示例:使用 Zookeeper 加锁启用多写入

# 启用多写入(OCC)
hoodie.write.concurrency.mode=optimistic_concurrency_control

# 使用基于 Zookeeper 的加锁
hoodie.write.lock.provider=\
org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider
hoodie.write.lock.zookeeper.url=<zookeeper_url>
hoodie.write.lock.zookeeper.port=<zookeeper_port>
hoodie.write.lock.zookeeper.lock_key=<lock_key>

# 及时清理失败写入
hoodie.clean.failed.writes.policy=EAGER

上述配置指定 Hudi 使用 OCC 作为并发模式,并以 Zookeeper 作为加锁机制。其他类型的加锁配置方式类似。

配置加锁机制(Configuring the Locking Mechanism)

加锁机制的选择取决于具体用例与基础设施条件。下述为可用选项的简要概览。

基于 Zookeeper 的加锁

Zookeeper 加锁在分布式环境中可靠且具备强一致性,但需要额外的运维开销来管理 ZK 集群:

hoodie.write.lock.provider=\
org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider
hoodie.write.lock.zookeeper.url=<zookeeper_url>
hoodie.write.lock.zookeeper.port=<zookeeper_port>
hoodie.write.lock.zookeeper.lock_key=<lock_key>

基于 Hive Metastore 的加锁

Hive Metastore 加锁较为轻量,适合已使用 HMS 的环境,适用于低到中等并发场景:

hoodie.write.lock.provider=org.apache.hudi.hive.HiveMetastoreBasedLockProvider
hoodie.write.lock.hivemetastore.database=<database_name>
hoodie.write.lock.hivemetastore.table=<table_name>

基于 DynamoDB 的加锁

DynamoDB 加锁适合云原生(尤其在 AWS 上)的可扩展部署,具备强一致性且易于管理:

hoodie.write.lock.provider=\
org.apache.hudi.aws.transaction.lock.DynamoDBBasedLockProvider
hoodie.write.lock.dynamodb.table=<dynamodb_table_name>
hoodie.write.lock.dynamodb.region=<aws_region>

基于存储的加锁(Storage-based locking)

该方式利用云存储(Amazon S3、GCS)的**条件写(conditional writes)**提供分布式锁,无需额外基础设施;每张表在云存储上维护一个锁文件,十分适合无服务器与云原生部署:

hoodie.write.lock.provider=\
org.apache.hudi.client.transaction.lock.StorageBasedLockProvider

通过 Hudi Streamer 使用多写入(Multiwriters Using Hudi Streamer)

Hudi Streamer 是一个将 DFS 或 Kafka 等数据源摄取到 Hudi 表的工具。要启用多写入支持,需要在 properties 文件中添加相应配置。

示例:使用 Zookeeper 加锁的 Streamer 配置

# Hudi Streamer properties
hoodie.write.concurrency.mode=optimistic_concurrency_control
hoodie.write.lock.provider=\
org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider
hoodie.write.lock.zookeeper.url=<zookeeper_url>
hoodie.write.lock.zookeeper.port=<zookeeper_port>
hoodie.write.lock.zookeeper.lock_key=<lock_key>
hoodie.cleaner.policy.failed.writes=EAGER

触发 Hudi Streamer 作业:

spark-submit \
  --packages <dependency identifier for a Hudi utilities bundle jar> \  # 1
  --class org.apache.hudi.utilities.deltastreamer.HoodieDeltaStreamer \
  --master yarn \
  --deploy-mode cluster \
  --conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
  --conf spark.sql.hive.convertMetastoreParquet=false \
  --table-type COPY_ON_WRITE \
  --source-class org.apache.hudi.utilities.sources.JsonKafkaSource \
  --source-ordering-field ts \
  --target-base-path /path/to/hudi_table \
  --target-table hudi_table \
  --props /path/to/file/with/additional/hudi.properties

1 示例依赖坐标:org.apache.hudi:hudi-utilities-bundle_2.13:1.1.0
注:hudi-utilities-bundle JAR 与相应 Hudi 版本所支持的最新 Apache Spark 版本匹配,请查阅发行说明以获取最新信息。

通过 Spark Data Source Writer 使用多写入(Multiwriters Using Spark Data Source Writer)

Hudi 的 Spark 模块提供 Data Source API,可将 Spark DataFrame 写入 Hudi 表。如下示例展示如何在该 API 中启用多写入支持:

import org.apache.spark.sql.SaveMode
import org.apache.spark.sql.SparkSession

val spark = SparkSession.builder()
  .appName("Hudi Multiwriter Example")
  .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
  .getOrCreate()

// 每个并发写入者从各自的数据源读取
val df_1 = spark.read.json("/path/to/source_data_1.json")

df_1.write.format("hudi") // 推荐使用 "hudi" 作为 format alias
  .option("hoodie.write.concurrency.mode", "optimistic_concurrency_control")
  .option("hoodie.write.lock.provider",
    "org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider")
  .option("hoodie.write.lock.zookeeper.url", "<zookeeper_url>")
  .option("hoodie.write.lock.zookeeper.port", "<zookeeper_port>")
  .option("hoodie.write.lock.zookeeper.lock_key", "<lock_key>")
  .option("hoodie.clean.failed.writes.policy", "EAGER")
  .option("hoodie.table.name", "hudi_table")
  .option("hoodie.datasource.write.operation", "upsert")
  .option("hoodie.datasource.write.recordkey.field", "id")
  .option("hoodie.datasource.write.precombine.field", "ts")
  .mode(SaveMode.Append)
  .save("/path/to/hudi_table")

单写入与多表服务(Single Writer and Multiple Table Services)

当场景是单写入者 + 多个表服务时,可将表服务配置为 inline(内联) 、**async execution(异步执行)standalone(独立进程)**模式。

示例:配置内联表服务(在写入进程内执行)

# 启用内联表服务
hoodie.compact.inline=true
hoodie.cluster.inline=true
hoodie.clean.inline=true

现代且推荐的做法是将表服务作为独立作业并发运行(例如运行专用的 HoodieCompactorHoodieClusteringJob),与摄取写入作业解耦,以获得更好的资源隔离可控性。若采用此方案,请确认写入作业未启用内联表服务。

关闭多写入支持(Disabling Multiwriter Support)

如需关闭多写入支持,可从 Hudi properties 中移除相关配置,或用默认值覆盖:

# 关闭多写入支持
hoodie.write.concurrency.mode=single_writer
hoodie.cleaner.policy.failed.writes=EAGER

——以上即为在 Hudi 中启用与使用多写入支持的配置要点与示例。

小贴士与最佳实践(Tips and Best Practices)

在多写(multiwriter)环境下使用 Hudi,遵循最佳实践对获得最优性能、最小化资源争用并避免常见陷阱至关重要。本节基于真实的数据湖仓架构与数据库系统经验,给出一份完整的最佳实践、技巧与性能优化指南。

实施分区与文件分组(Partitioning & File Grouping)

在多写环境中,有效的分区是降低冲突最有效的方式之一。通过将数据划分为相互独立的分区或文件组(file groups) ,可确保不同写入者作用在不同数据片段上,从而减少冲突概率。

示例:对时序数据,可按日期分区(如 year=2023/month=10/day=01)。这样多个写入者即可并行处理不同分区,互不重叠。

Tip:使用 Hudi 的聚类(clustering) 功能优化分区内的文件大小与分组,使写入者总能在组织良好的数据上工作。

启用早期冲突检测(Early Conflict Detection)

在标准的 Hudi 写入流程中,冲突检测是保障数据完整性的关键环节,但传统上发生在最终提交(commit)阶段:写入者完成了昂贵的数据处理并将新数据文件(如 Parquet)写入存储后,尝试以原子方式将更改发布到 Hudi 时间线;只有到这一刻,Hudi 才校验是否自本次操作开始以来,已经有其他写入者成功提交到同一底层文件。若检测到冲突,整个写入将被中止——此前消耗的所有计算资源被浪费,并被迫重试,成本高昂。

为缓解这一低效,Hudi 引入了早期冲突检测:将冲突检查从写入周期末端前移到开端,并将其集成到写入操作的初始阶段

增强后的写入流程如下:

  1. 声明意图(Declare intent) :在进入昂贵的数据处理阶段前,写入者首先声明它将修改的具体文件组,并在时间线上将这些文件标记为新的待提交(pending commit)的一部分。
  2. 检查时间线(Check timeline) :随后立即检查时间线,确认自当前事务启动以来,是否已有其它事务成功提交到同一文件组。
  3. 快速失败或继续(Fail fast or proceed) :若发现冲突,立刻中止写入,在投入大量计算资源前就停止;若无冲突,则进入昂贵的数据写入阶段,此时对最终提交成功的置信度很高。

通过采取这种“快速失败(fail-fast) ”方式,早期冲突检测显著降低了并发冲突的成本。它对大规模多写部署尤为有益:并发写概率更高、一次失败写入的资源代价也更大。该前置校验让计算资源更聚焦于更可能成功的事务,从而明显提升湖仓的整体效率与吞吐。

示例:在多写入者的大规模部署中,早期冲突检测可避免写入者处理完整批次数据后,才在提交阶段因冲突而被迫中止的情况。

启用配置:

hoodie.write.concurrency.early.conflict.detection.enable=true

优化加锁机制(Optimize Locking Mechanisms)

根据基础设施与负载特点选择合适的加锁机制。例如:

  • 本地/自建机房,强一致需求:使用 Zookeeper-based locking
  • AWS 云原生、看重扩展与易运维:使用 DynamoDB-based locking
  • 无原生锁机制的云(如 GCP 或 Azure) :可在虚机上部署 ZooKeeper 集群;或基于 Hudi 的可插拔接口扩展自定义锁,利用云特定服务(如 Google Zonal LockAzure Blob Lease API)。

高并发环境中,大量写入者竞争同一把锁会导致延迟与超时,从而降低吞吐。可通过配置锁获取的重试策略来提升写作业的韧性。

示例:为锁获取配置重试以应对瞬时失败或高争用

hoodie.write.lock.wait_time_ms=10000  # 最长等待 10 秒获取锁
hoodie.write.lock.num_retries=5       # 最多重试 5 次

以异步方式运行表服务(Run Asynchronous Table Services)

将压缩(compaction)、聚类(clustering)、清理(cleaning)等表服务异步运行,避免阻塞主摄取(ingestion)流水线。这样,写入者能持续写入,而表服务在后台优化表。详见第 6 章。

示例:在流式管道中,以异步模式执行压缩,可在后台重写大文件而不影响新数据的摄取:

hoodie.compact.inline=false
hoodie.compact.schedule.inline=true

减少写冲突与资源浪费(Reduce Write Conflicts & Wasted Resources)

在多写环境中,冲突会导致作业中止与算力浪费,尤其当冲突在写生命周期的末端才被发现时。
分区化工作负载是减少冲突最有效的方式,它天然地隔离不同写入者;同时,启用早期冲突检测可在数据写入前发现冲突,尽早终止注定失败的写入。

示例:为进一步优化性能,可启用 NBCC(non_blocking_concurrency_control) ,允许写入者在无须等待锁的情况下继续:

hoodie.write.concurrency.mode=non_blocking_concurrency_control

多写环境下防止数据重复(Prevent Data Duplication with Multiple Writers)

需要特别理解的限制是:Hudi 的多写模式并不保证跨写入者的数据去重。若两个写入者同时摄取拥有相同主键的记录,表中可能出现重复——因为每个写入者只会针对其事务开始时可见的数据进行去重。

Hudi 的一项最佳实践是确保幂等的数据源:例如按记录主键对上游数据进行分片,避免同一数据被并发写入。若无法控制数据源,可采用分层表(staging → clean)方案:先将原始数据摄取到预处理 Hudi 表,再由单写作业执行去重,最终写入干净表

总结(Summary)

并发控制是数据系统的基石:在并行读写环境中确保数据一致性与运维效率。Hudi 将这一原则扩展到分布式数据湖仓,通过机制使多个写入者可并发工作,同时维持数据完整性。多写能力弥补了单写系统的局限,为复杂的摄取、更新与删除场景提供所需的灵活性。

本章全面回顾了 Hudi 的并发控制:其在保障一致性、支持高效多写中的重要性;OCC 如何让多个写入者并发且尽量减少冲突;以及 Hudi 提供的多种加锁机制及其优缺点,帮助你按需选型。

尽管如此,Hudi 的并发控制也有需要注意的限制:如晚期冲突检测可能导致资源浪费、以文件组为粒度的冲突检测带来的细粒度局限、在极端并发下锁提供者可能成为瓶颈。理解这些限制,并应用诸如合理的工作负载分区早期冲突检测等最佳实践,有助于更高效地利用 Hudi。通过直面分布式数据湖仓的内在复杂性,Hudi 提供了一个在可扩展性与一致性间取得平衡的强大框架。

最后,我们也讨论了 Hudi 多写实现的局限,例如跨写入者的去重缺失与因冲突导致的潜在资源浪费。理解这些限制,有助于更好地规划与优化你的 Hudi 部署,以获得最大化的效率与可靠性。随着你进一步深入 Hudi 的世界,请牢记:掌握并发控制是构建健壮、可扩展数据湖仓解决方案的关键。