最近一位 Hudi 用户询问他们是否可以在不需要任何锁的情况下同时从多个写入端写入单个 Hudi 表。 他们场景是一个不可变的工作负载。 一般来说对于任何多写入端功能,Hudi 建议启用锁定配置。 但这是一个有趣的问题,我们进行探索并找到了解决方案,因此与更广泛的社区分享。
需要并发写入的锁提供程序
对于某些场景来说可能是必要的,但可能并不适合所有场景。 因此我们首先看看为什么当并发写入Hudi 或任何表格式时我们需要锁提供程序。 如果两个并发写入修改同一组数据,我们只能允许其中一个成功并中止另一个,因为至少与乐观并发控制(OCC)存在冲突。 我们可以尝试设计和实现基于 MVCC 的模型,但当前还没有做到这一点。 因此仅使用纯 OCC,任何两个并发写入重叠数据都无法成功。 因此为了解决冲突和某些表管理服务,我们需要锁,因为在任何时间点只有其中一个可以操作临界区。 因此我们采用锁提供程序来确保两个写入之间协调此类冲突解决和表管理服务。总结如下
- 出于解决冲突的目的,我们不会让两个写入端成功写入重叠的数据。
- 对于清理、归档、聚簇等表管理服务,需要协调不同写入端。
那么如果上述两个原因可以放宽呢?
- 如果工作负载是不可变的,或者不同的写入端写入完全不同的分区,那么真的不需要解决任何冲突。显然声称没有一个写入端重叠这是由用户承担的,因为 Hudi 可能不会做任何冲突解决。
- 禁用除一个写入端之外的所有写入端的表服务。
不可变的工作负载
不可变的工作负载是关键。 因此建议他们使用 bulk_insert
作为操作类型,因为它相当于写入Parquet表。 没有索引查找,没有小文件管理,因此两个写入端不会以任何方式发生冲突。
表服务
Hudi 有一个全局配置,可以在需要时禁用表服务("hoodie.table.services.enabled")。 默认情况下配置设置为 true,因此启动的每个写入端都可能正在执行表服务。但我们可以使用此配置来禁用除一个之外的所有写入端。
元数据表
必须禁用元数据表,因为我们有一个先决条件,即如果有多个写入端,需要锁定元数据表。
本质上其中一个写入端将与所有表服务一起进行摄取,而所有其他写入端只会进行摄取,这可能不会与任何其他写入端重叠。如下是两个写入端的配置。
写入端1
忽略典型的必填字段,如记录键、表名等。这些是必须为写入端 1 设置的配置。
option("hoodie.datasource.write.operation","bulk_insert").
option("hoodie.write.concurrency.mode","OPTIMISTIC_CONCURRENCY_CONTROL").
option("hoodie.cleaner.policy.failed.writes","LAZY").
option("hoodie.write.lock.provider","org.apache.hudi.client.transaction.lock.InProcessLockProvider").
option("hoodie.metadata.enable","false").
注意到我们启用了 InProcessLockProvider
并将操作类型设置为"bulk_insert"并禁用了元数据表。
因此写入端将负责清理和归档等表服务。
写入端2
写入端2设置如下
option("hoodie.datasource.write.operation","bulk_insert").
option("hoodie.cleaner.policy.failed.writes","LAZY").
option("hoodie.metadata.enable","false").
option("hoodie.table.services.enabled","false").
注意到我们禁用了表服务和元数据表,并将操作类型设置为"bulk_insert"。 因此写入端2所做的就是将新数据摄取到表中,而无需担心任何表服务。
小文件管理
如果希望利用小文件管理也可以将写入端1的操作类型设置为"insert"。 如果希望将"insert"作为所有写入的操作类型,则应小心。 如果它们都写入不同的分区,那么它可能会起作用。 但如果它们可能写入相同的分区,则可能会导致意想不到的后果,需要避免。
或者我们可以将操作类型保留为"bulk_insert",但使用写入端1启用聚簇来合并小文件,如下所示:
option("hoodie.datasource.write.operation","bulk_insert").
option("hoodie.write.concurrency.mode","OPTIMISTIC_CONCURRENCY_CONTROL").
option("hoodie.cleaner.policy.failed.writes","LAZY").
option("hoodie.write.lock.provider","org.apache.hudi.client.transaction.lock.InProcessLockProvider").
option("hoodie.metadata.enable","false").
option("hoodie.clustering.inline","true").
option("hoodie.clustering.inline.max.commits","4").
为两个并发 Spark 写入端尝试上述一组配置,并使用清理和归档设置进行了 100 多次提交测试。 还进行故障演练并且事物完好无损。 输入数据与两个写入端从 Hudi 读取的快照相匹配。
结论
如果用例符合前面提到的约束,这将非常有助于提高 Hudi 写入的吞吐量。不必为锁提供者管理基础设施也将减轻操作负担。