文章目录
- Timeline
- Table & Query Types
- Table Types
- 查询类型
- COW
- MOR
- 索引
- Hudi索引类型
- 索引选择策略
- File Layouts
- 元数据表
- 元数据表的动机
- 研究中的一些数字:
- 支持多模态索引
- 写操作
- 操作类型
- UPSERT
- INSERT
- BULK_INSERT
- DELETE
- 写入路径
- schema 演进
- key生成
- 并发控制
- Datasource Writer
- DeltaStreamer
Timeline
在hudi的核心,维护了在不同 instants 在table上执行的所有操作的 timeline ,这有助于提供 table 的即时视图,同时也有效地支持按到达顺序检索数据。Hudi instant 由以下组件组成
Instant action:对表格执行的操作类型
Instant time:即时时间通常是一个时间戳(例如:20190117010349),它按照动作开始时间的顺序单调增加。
state:当前状态
Hudi保证在 timeline 上执行的操作是原子的,并且基于Instant time的时间线是一致的。
执行的关键 action 包括:
COMMITS-提交表示将一批记录原子写入表。
CLEANS-清除表中不再需要的旧版本文件的后台活动。
DELTA_COMMIT-增量提交是指将一批记录原子写入MergeOnRead类型表,其中一些/所有数据可以直接写入增量日志。
COMPACTION -协调Hudi内部差异数据结构的后台活动,例如:将更新从基于行的日志文件移动到列格式。在内部,压缩表现为时间轴上的特殊提交
ROLLBACK-表示提交/增量提交不成功并回滚,删除在写入过程中生成的任何部分文件
SAVEPOINT-将某些文件组标记为“已保存”,这样清理不会删除它们。它有助于在发生灾难/数据恢复情况时将表恢复到时间线上的某个点。
任何给定的 instant 都可以处于以下状态之一
REQUESTED -表示已计划但尚未启动的操作
INFLIGHT-表示当前正在执行操作
COMPLETED -表示时间线上某项操作的完成
上面的示例显示了Hudi表上10:00到10:20之间发生的异常,大约每5分钟发生一次,在Hudi时间线上留下提交元数据,以及其他后台清理/压缩。要做的一个关键点是,提交时间表示数据的 arrival time(上午10:20A),而实际数据组织反映了数据的实际时间或 event time(从07:00开始的每小时时段)。在推断数据的延迟和完整性之间的权衡时,这是两个关键概念。
当有延迟到达的数据(预计9点到达的数据>10点20分延迟1小时)时,我们可以看到将新数据复制到更旧的时间段/文件夹中。在 timeline 的帮助下,尝试获取自10:00小时以来成功提交的所有新数据的增量查询能够非常高效地仅使用已更改的文件,而无需扫描所有时间段>07:00。
Table & Query Types
Hudi表类型定义了如何在DFS上对数据进行索引和布局,以及如何在这样的组织之上实现上述原语和时间线活动(即如何写入数据)。反过来,查询类型定义了底层数据如何向查询公开(即如何读取数据)。
表类型 | 支持的查询类型 |
---|---|
Copy on write | 快照查询,增量查询 |
Merge on read | 快照查询,增量查询,读优化查询 |
Table Types
Hudi支持以下表类型。
Copy on write:使用列式文件格式(例如 parquet)存储数据。通过在写入期间执行同步合并,更新简单地对文件进行版本和重写。
Merge on read:使用列式(例如 parquet)+基于行的(例如avro)文件格式的组合存储数据。更新被记录到增量文件中,然后被压缩以同步或异步地生成新版本的列式文件。
下表总结了这两种表类型之间的区别:
sa | COW | MOR |
---|---|---|
Data 延迟 | 高 | 低 |
Query 延迟 | 低 | 高 |
update IO成本 | 高 | 低 |
Parquet 文件大小 | 小 | 大 |
写膨胀 | 高 | 低 |
查询类型
快照查询:查询给定提交或压缩操作时表的最新快照。在读取表上进行合并的情况下,它通过动态合并最新文件切片的基本文件和增量文件来暴露接近实时的数据(几分钟)。对于写表复制,它提供了对现有 parquet 表的直接替换,同时提供了追加启动/删除和其他写端功能。
增量查询:自从给定的提交/压缩之后,查询只能看到写入到表中的新数据。这有效地提供了更改流,以启用增量数据管道。
读取优化查询:查询查看给定提交/压缩操作时表的最新快照。只显示最新文件切片中的基本/列式文件,并保证与非hudi列式表相比具有相同的列式查询性能。
trade-off | Snapshot | read Optimized |
---|---|---|
数据延迟 | 低 | 高 |
查询延迟 | 高 | 低 |
COW
“写入时复制”表中的文件切片仅包含基本/列文件,每次提交都会生成新版本的基本文件。换句话说,我们在每次提交时都隐式压缩,这样只存在列数据。因此,写入放大率(为1字节的输入数据写入的字节数)高得多,其中读取放大率为零。这是分析工作负载非常需要的属性,因为分析工作负载主要是重读取的。
下面从概念上说明了当数据写入到写时拷贝表中并在其上运行两个查询时,这是如何工作的。
当数据被写入时,对现有文件组的更新会为该文件组生成一个新的切片,该切片标记有提交instant time,而插入会分配一个新文件组并为该文件群写入其第一个切片。这些文件切片及其提交时间在上面用颜色编码。针对这样一个表运行的SQL查询(例如:select count(*)统计该分区中的总记录),首先检查最新提交的时间线,并过滤每个文件组中除最新文件片段之外的所有文件片段。正如您所看到的,一个旧的查询不会看到当前飞行中提交的文件以粉色编码,而是在提交后开始的一个新的查询会拾取新的数据。因此,查询不受任何写入失败/部分写入的影响,仅在提交的数据上运行。
COW表的目的,是通过一下几个方面功能上提高表的组织能力:
文件级自动更新数据,而不是重写整个表/分区
能够增量更改,而不是浪费扫描或试探摸索
严格控制文件大小以保持出色的查询性能(小文件会严重影响查询性能)。
MOR
读时合并表是写时复制的超集,从某种意义上说,通过只在最新的文件切片中显示基/列文件来支持表的读优化查询。此外,它将每个文件组的传入的 upserts 存储到基于行的增量日志中,以便通过在查询期间将增量日志应用到每个文件id的最新版本来支持快照查询。因此,这种表类型试图智能地平衡读和写放大,以提供接近实时的数据。这里最重要的变化将是压缩程序,它现在仔细选择需要将哪些增量日志文件压缩到其列状基础文件上,以保持查询性能的检查(较大的增量日志文件将导致较长的合并时间,而合并数据位于查询端)
下面说明了该表的工作原理,并显示了两种类型的查询:快照查询和读取优化查询。
我们现在每1分钟左右就会提交一次,这是其他表类型无法做到的。
在每个文件id组中,现在有一个增量日志文件,它保存对基本列文件中记录的传入更新。在示例中,增量日志文件保存10:05到10:10之间的所有数据。与之前一样,基本列式文件仍然使用提交进行版本控制。因此,如果只查看基本文件,那么表布局看起来就像一个写时复制表。
定期压缩过程从增量日志中协调这些更改,并生成新版本的基础文件,就像示例中10:05发生的情况一样。
查询同一基础表有两种方法:读取优化查询和快照查询,这取决于我们选择的是查询性能还是数据的新鲜度。
当来自提交的数据可用于查询时,对于读优化查询,语义会以微妙的方式发生变化。注意,这样一个在10:10运行的查询在10:05之后不会看到数据,而快照查询总是看到最新的数据。
当我们触发压缩时,它决定压缩什么,这是解决这些难题的关键。通过实施压缩策略(与旧分区相比,我们积极压缩最新分区),我们可以确保读取优化的查询在X分钟内以一致的方式查看发布的数据。
合并读表的目的是直接在DFS上实现近乎实时的处理,而不是将数据复制到可能无法处理数据量的专用系统。该表还有一些次要的好处,例如通过避免数据的同步合并来减少写入放大,即一批中每1字节数据写入的数据量
索引
Hudi通过索引机制将给定的 hoodie key(record key + partition path)一致地映射到文件id,从而提供高效的 upserts。record key 和文件组/文件id之间的映射在记录的第一个版本写入文件后不会更改。简而言之,映射的文件组包含一组记录的所有版本。
对于“写入时复制”表,这可以实现快速的追加启动/删除操作,避免了对整个数据集进行连接以确定要重写的文件的需要。对于“读取时合并”表,此设计允许Hudi绑定任何给定基础文件需要合并的记录量。具体来说,给定的基本文件只需要根据作为该基本文件一部分的记录的更新进行合并。相比之下,没有索引组件(例如:Apache Hive ACID)的设计可能最终不得不根据所有传入的更新/删除记录合并所有基本文件:
Hudi索引类型
目前,Hudi支持以下索引选项。
Bloom索引(默认):使用基于记录键构建的Bloom过滤器,也可以选择使用记录键范围修剪候选文件。
简单索引:根据从存储表中提取的键,对传入的更新/删除记录执行精简联接。
HBase索引:管理外部ApacheHBase表中的索引映射。
自定义:您可以扩展此公共API以实现自定义索引。
可以使用 hoodie.index.type 配置选项选择其中一个选项。此外,还可以使用 hoodie.index.class 使用自定义索引实现,并提供 SparkHoodieIndex 的子类(用于Apache Spark编写器)
另一个值得理解的关键方面是全局索引和非全局索引之间的差异。bloom和simple index都有全局选项 hoodie.index.type=global_bloom和 hoodie.inindex.type=global_simple。HBase 索引本质上是一个全局指数。
- 全局索引:全局索引在表的所有分区中强制执行键的唯一性,即确保表中对于给定的记录键只存在一条记录。全局索引提供了更强的保证,但更新/删除成本会随着表O的大小(表的大小)而增加,这对于较小的表来说仍然是可以接受的。
- 非全局索引:另一方面,默认索引实现仅在特定分区内强制执行此约束。可以想象,非全局索引依赖于写入程序在更新/删除期间为给定的记录键提供相同的一致分区路径,但由于索引查找操作变为O(更新/删除的记录数),并且可以随写入量进行很好的扩展,因此可以提供更好的性能。
由于数据的输入量、速度和访问模式不同,因此可以针对不同的工作负载类型使用不同的索引。让我们来看看一些典型的工作负载类型,看看如何为此类用例利用正确的Hudi索引。这是基于我们的经验,您应该努力决定相同的策略是否最适合您的工作负载。
索引选择策略
工作量1:延迟更新事实表
许多公司将大量事务数据存储在NoSQL数据存储中。例如,在电子商务网站上共享乘车、买卖股票和订单的情况下的行程表。这些表通常会随着对最新数据的随机更新而不断增长,而长尾更新会转到较旧的数据,这可能是由于事务在稍后的日期结算/数据更正。换句话说,大多数更新都会进入最新的分区,很少有更新会进入旧的分区。
对于这样的工作负载,BLOOM索引表现良好,因为索引查找将基于大小合适的布隆过滤器来删除大量数据文件。此外,如果可以构造密钥以使它们具有一定的顺序,则通过范围修剪可以进一步减少要比较的文件的数量。Hudi构建了一个包含所有文件密钥范围的间隔树,并有效地筛选出与更新/删除记录中的任何密钥范围不匹配的文件。
为了有效地将传入的记录密钥与布隆过滤器进行比较,即以最少的布隆过滤器读取次数和执行器之间的工作均匀分布,Hudi利用了输入记录的缓存,并采用了自定义分区器,可以使用统计数据消除数据偏差。有时,如果布隆过滤器的误报率很高,则可能会增加执行查找所需的数据量。Hudi支持动态布隆过滤器(使用hoodie.bloom.index.filter.type=dynamic_V0启用),该过滤器根据存储在给定文件中的记录数调整其大小,以提供配置的误报率。
工作负载2:事件表中的重复数据消除
事件流无处不在。来自Apache Kafka或类似消息总线的事件通常是事实表大小的10-100倍,通常将“时间”(事件的到达时间/处理时间)视为头等公民。例如,IoT事件流、点击流数据、广告展示等。插入和更新仅跨越最后几个分区,因为这些分区大多是仅附加数据。由于重复事件可以在端到端管道中的任何位置引入,因此在存储到数据湖之前进行重复数据消除是一项常见的要求。
工作负载3:随机更新/删除维度表
这些类型的表通常包含高维数据并保存参考数据,例如用户配置文件、商家信息。这些是高保真表,其中更新通常很小,但也分布在从旧到新的数据集中的许多分区和数据文件中。通常,这些表也是未分区的,因为也没有很好的方法来分区这些表。
如前所述,如果无法通过比较范围/过滤器来删除大量文件,BLOOM索引可能不会带来好处。在这样的随机写入工作负载中,更新最终会触及表中的大多数文件,因此布隆过滤器通常会根据某些传入的更新指示所有文件的真正。因此,我们最终会比较范围/过滤器,最终检查所有文件的传入更新。SIMPLE索引将更适合,因为它不基于任何预先修剪,而是直接与每个数据文件中的感兴趣字段连接。如果操作开销可以接受,并且可以为这些表提供更好的查找时间,则可以使用HBASE索引。
当使用全局索引时,用户还应考虑设置hoodie.bloom.index.update.partition.path=true或hoodie.simple.index.update.partition/path=true,以处理分区路径值可能因更新而改变的情况,例如,按家庭城市划分的用户表;用户迁移到不同的城市。这些表也是“读取时合并”表类型的优秀候选表。
File Layouts
以下描述了Apache Hudi的通用文件布局结构
- Hudi将数据表组织到分布式文件系统的基本路径下的目录结构中
- 表被划分为多个分区
- 在每个分区中,文件被组织成文件组,由文件ID唯一标识
- 每个文件组包含多个文件切片
- 每个切片包含在某个提交/压缩瞬间生成的基文件(.parquet),以及一组日志文件(.log.*),这些文件包含自生成基文件以来对基文件的插入/更新。
Hudi采用了多版本并发控制(MVCC),其中压缩操作合并日志和基本文件以生成新的文件切片,而清理操作清除未使用的/旧的文件切片以回收文件系统上的空间。
元数据表
元数据表的动机
ApacheHudi元数据表可以显著提高查询的读/写性能。元数据表的主要目的是消除对“list files”操作的要求。
读取和写入数据时,执行文件列表操作以获取文件系统的当前视图。当数据集很大时,列出所有文件可能是一个性能瓶颈,但更重要的是,在AWS S3这样的云存储系统中,由于某些请求限制,大量文件列出请求有时会导致节流。元数据表将主动维护文件列表,并消除递归文件列表操作的需要。
研究中的一些数字:
运行TPCDS基准测试,单个文件夹的p50列表延迟与文件/对象的数量成线性关系:
Number of files/objects | 100 | 1K | 10K | 100K |
---|---|---|---|---|
P50 list latency | 50ms | 131ms | 1062ms | 9932ms |
而元数据表中的列表不会随文件/对象计数线性扩展,而是每次读取大约需要100-500ms,即使是非常大的表。更棒的是,时间轴服务器缓存了部分元数据(目前仅用于编写者),并为列表提供了约10ms的性能。
支持多模态索引
多模式索引可以大大提高文件索引的查找性能,并通过数据跳过来提高查询延迟。包含文件级布隆过滤器的布隆过滤器索引有助于密钥查找和文件修剪。包含所有列统计信息的列统计索引改进了基于写入器和读取器中的键和列值范围的文件修剪,例如在Spark中的查询规划中。多模式索引被实现为包含元数据表中索引的独立分区。
启用Hudi元数据表和多模式索引
从0.11.0开始,默认情况下启用具有同步更新的元数据表和基于元数据表的文件列表。部署注意事项中有安全使用此功能的先决条件配置和步骤。通过将hoodie.metadata.enable设置为false,仍然可以关闭元数据表和相关文件列表功能。对于0.10.1和以前的版本,默认情况下禁用元数据表,您可以通过将相同的配置设置为true来启用它。
如果在启用后关闭元数据表,请确保等待几次提交,以便在再次启用元数据表之前完全清理元数据表。
在0.11.0版本中引入了多模态索引。默认情况下,它们被禁用。当启用元数据表时,您可以选择通过将hoodie.metadata.index.bloom.filter.enable设置为true来启用布隆过滤器索引,并通过将hootie.metadata_index.column.stats.enable设置成true来启用列统计信息索引。在0.11.0版本中,为了改进Spark中的查询,数据跳过现在依赖于元数据表中的列统计索引。启用元数据表和列统计索引是使用hoodie.enable.data.skipping启用数据跳过的先决条件。
部署的注意事项
为了确保元数据表保持最新,同一Hudi表上的所有写入操作都需要在不同的部署模型中进行上述配置之外的其他配置。在启用元数据表之前,必须停止同一表上的所有写入程序。
部署模型A:具有内联表服务的单个编写器
如果您当前的部署模型是单编写器,并且所有表服务(清理、集群、压缩)都配置为内联,例如Deltastremer sync once模式和具有默认配置的Spark Datasource,则不需要额外的配置。将hoodie.metadata.enable设置为true后,重新启动单个写入程序就足以安全地启用元数据表。
部署模型B:具有异步表服务的单个编写器
如果您当前的部署模型是单编写器,以及在同一进程中运行的异步表服务(如清理、集群、压缩),如Deltastremer连续模式写入MOR表、Spark流(默认情况下压缩是异步的),以及您自己的作业设置在同一编写器中启用异步表服务,则必须具有乐观的并发控制,在启用元数据表之前配置的锁提供程序和延迟失败写入清理策略如下。这是为了在启用元数据表时保证乐观并发控制的正确行为。不遵守配置指南会导致数据丢失。请注意,只有在此部署模型中启用元数据表时,才需要这些配置。
hoodie.write.concurrency.mode=optimistic_concurrency_control
hoodie.cleaner.policy.failed.writes=LAZY
hoodie.write.lock.provider=org.apache.hudi.client.transaction.lock.InProcessLockProvider
如果不同进程中存在多个写入程序,包括一个具有异步表服务的写入程序,请参阅部署模型C:多写入程序以了解配置,区别在于使用分布式锁提供程序。请注意,在摄取写入程序之外运行单独的压缩(HoodieCompactor)或群集(HoodieClusteringJob)作业被视为多写入程序部署,因为它们不是在同一进程中运行的,不能依赖进程内锁提供程序。
部署模型C:多编写器
如果您当前的部署模型是多编写器,并且为每个编写器设置了锁提供程序和其他必需的配置,如下所示,则不需要额外的配置。您可以在停止写入程序以启用元数据表之后,依次启动写入程序。仅对部分写入程序应用正确的配置会导致不一致写入程序的数据丢失。所以,请确保在所有编写器中启用元数据表。
hoodie.write.concurrency.mode=optimistic_concurrency_control
hoodie.cleaner.policy.failed.writes=LAZY
hoodie.write.lock.provider=<distributed-lock-provider-classname>
请注意,有三种不同的分布式锁提供程序可供选择:ZookeperBasedLockProvider、HiveMetastoreBasedLockProvider和DynamoDBBasedLockProvider。
写操作
了解Hudi的不同写入操作以及如何最好地利用它们可能会有所帮助。可以在针对表发出的每个提交/增量提交中选择/更改这些操作。
操作类型
UPSERT
这是默认操作,首先通过查找索引将输入记录标记为插入或更新。在运行试探法以确定如何最好地将记录打包到存储中以优化文件大小等操作之后,记录最终会被写入。建议在数据库更改捕获等输入几乎肯定包含更新的用例中使用此操作。目标表永远不会显示重复项。
INSERT
该操作在启发式/文件大小方面与upstart非常相似,但完全跳过了索引查找步骤。因此,对于日志重复数据消除(与下面提到的过滤重复数据的选项结合使用)等用例,它可以比追加发布快得多。这也适用于表可以容忍重复,但只需要Hudi的事务写入/增量拉取/存储管理功能的用例。
BULK_INSERT
upstart和insert操作都将输入记录保存在内存中,以加快存储启发式计算的速度(除其他外),因此对于最初加载/引导Hudi表来说可能会很麻烦。大容量插入提供了与插入相同的语义,同时实现了基于排序的数据写入算法,该算法可以很好地扩展到几百TB的初始负载。然而,这只是在调整文件大小方面做得最好,而不是像插入/追加部分那样保证文件大小。
DELETE
Hudi支持对存储在Hudi表中的数据执行两种类型的删除,方法是允许用户指定不同的记录有效负载实现。
软删除:保留记录键,并将所有其他字段的值设为空。这可以通过确保表模式中适当的字段可以为空,并在将这些字段设置为空后简单地重新启动表来实现。
硬删除:更强的删除形式是从表中物理删除记录的任何痕迹。这可以通过三种不同的方式实现。
使用DataSource,将OPERATION_OPT_KEY设置为DELETE_OPERATION_OPT_VAL。这将删除正在提交的数据集中的所有记录。
使用DataSource,将PAYLOAD_CLASS_OPT_KEY设置为“org.apache.hudi.EmptyHoodieRecordPayload”。这将删除提交的数据集中的所有记录。
使用DataSource或DeltaStreamer,将名为_hoodie_is_deleted的列添加到DataSet。对于要删除的所有记录,必须将此列的值设置为true,对于要重新排序的任何记录,必须设置为false或留空。
写入路径
下面是关于Hudi写入路径和写入过程中发生的事件序列的内部视图。
- Deduping
首先,您的输入记录可能在同一批中有重复的键,重复的键需要组合或减少。
- 索引查找
接下来,执行索引查找以尝试匹配输入记录,以识别它们属于哪个文件组。
- 文件大小调整
然后,根据以前提交的平均大小,Hudi将制定计划,向一个小文件中添加足够的记录,使其接近配置的最大限制。
- 分区
现在到了分区阶段,我们决定将在哪些文件组中放置某些更新和插入,或者是否创建新的文件组
- 写入I/O
现在,我们实际执行写操作,即创建新的基础文件,附加到日志文件,或对现有基础文件进行版本控制。
- 更新索引
现在执行了写操作,我们将返回并更新索引。
- Commit
最后,我们以原子方式提交所有这些更改。(显示回调通知)
- Clean(如果需要)
提交后,如果需要,将调用清理。
- 压缩
如果您使用MOR表,压缩将内联运行,或异步调度
- 归档
最后,我们执行归档步骤,将旧的时间线项目移动到归档文件夹。
schema 演进
模式演化允许用户轻松更改Hudi表的当前模式,以适应随时间变化的数据。截至0.11.0版本,Spark SQL(Spark 3.1.x、3.2.1及更高版本)已经添加了对Schema演化的DDL支持,并且是实验性的。
情节
可以添加、删除、修改和移动列(包括嵌套列)。
分区列无法进化。
不能在Array类型的嵌套列上添加、删除或执行操作。
SparkSQL模式演变和语法描述
在使用模式进化之前,请设置spark.sql.extensions。对于spark 3.2.1及更高版本,还需要设置spark.ql.catalog.spark_catalog。
# Spark SQL for spark 3.1.x
spark-sql --packages org.apache.hudi:hudi-spark3.1.2-bundle_2.12:0.11.1 \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension'
# Spark SQL for spark 3.2.1 and above
spark-sql --packages org.apache.hudi:hudi-spark3-bundle_2.12:0.11.1 \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension' \
--conf 'spark.sql.catalog.spark_catalog=org.apache.spark.sql.hudi.catalog.HoodieCatalog'
语法:
ALTER TABLE tableName ADD COLUMNS(col_spec[, col_spec …])
ALTER TABLE tableName ALTER [COLUMN] col_old_name TYPE column_type [COMMENT] col_comment[FIRST|AFTER] column_name
ALTER TABLE tableName DROP COLUMN|COLUMNS cols
ALTER TABLE tableName RENAME COLUMN old_columnName TO new_columnName
ALTER TABLE tableName SET|UNSET tblproperties
ALTER TABLE tableName RENAME TO newTableName
key生成
https://hudi.apache.org/docs/key_generation
并发控制
在本节中,我们将介绍Hudi的并发模型,并描述如何将数据从多个编写器接收到Hudi表中;使用DeltaStreamer工具以及Hudi数据源。
支持的并发控制
MVCC:Hudi表服务(如压缩、清理、集群)利用多版本并发控制在多个表服务编写器和读取器之间提供快照隔离。此外,使用MVCC,Hudi在摄取写入器和多个并发读取器之间提供快照隔离。使用此模型,Hudi支持并发运行任意数量的表服务作业,而不会发生任何并发冲突。这是通过确保这样的表服务的调度计划总是在单个写入器模式下发生来实现的,以确保没有冲突并避免竞争条件。
[新增]最佳并发性:写入操作(如上文所述的操作(UPSERT、INSERT)等),利用乐观并发控制来启用同一Hudi表的多个摄取写入器。Hudi支持文件级OCC,即,对于发生在同一个表上的任何2个提交(或写入程序),如果它们没有对正在更改的重叠文件进行写入,则允许两个写入程序成功。该功能目前处于试验阶段,需要Zookeeper或HiveMetastore来获取锁。
了解通过Hudi数据源或delta流器进行写入操作所提供的不同保证可能会有所帮助。
单一 writer 保证
UPSERT保证:目标表不会显示重复项。
INSERT保证:如果启用了重复数据消除,目标表将永远不会有重复数据。
BULK_INSERT保证:如果启用了重复数据消除,目标表将永远不会有重复数据。
增量拉动保证:数据消耗和检查点从未出现故障。
多编写器保证
在多个编写器使用OCC的情况下,上述一些保证如下所示
UPSERT保证:目标表不会显示重复项。
INSERT保证:即使启用了重复数据消除,目标表也可能有重复数据。
BULK_INSERT保证:即使启用了重复数据消除,目标表也可能有重复数据。
增量拉动保证:由于多个写入程序作业在不同时间完成,数据消耗和检查点可能会出现故障。
启用多重写入
需要正确设置以下属性才能启用乐观并发控制。
hoodie.write.concurrency.mode=optimistic_concurrency_control
hoodie.cleaner.policy.failed.writes=LAZY
hoodie.write.lock.provider=<lock-provider-classname>
有3个不同的基于服务器的锁提供程序需要设置不同的配置。
基于Zookeeper的锁提供程序
hoodie.write.lock.provider=org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider
hoodie.write.lock.zookeeper.url
hoodie.write.lock.zookeeper.port
hoodie.write.lock.zookeeper.lock_key
hoodie.write.lock.zookeeper.base_path
基于 HiveMetastore 的锁提供者
hoodie.write.lock.provider=org.apache.hudi.hive.HiveMetastoreBasedLockProvider
hoodie.write.lock.hivemetastore.database
hoodie.write.lock.hivemetastore.table
基于Amazon DynamoDB的锁提供程序
基于AmazonDynamoDB的锁提供了一种支持跨不同集群进行多写的简单方法。有关每个相关配置旋钮的详细信息,请参阅基于DynamoDB的锁配置部分。
hoodie.write.lock.provider=org.apache.hudi.aws.transaction.lock.DynamoDBBasedLockProvider
hoodie.write.lock.dynamodb.table (required)
hoodie.write.lock.dynamodb.partition_key (optional)
hoodie.write.lock.dynamodb.region (optional)
hoodie.write.lock.dynamodb.endpoint_url (optional)
hoodie.write.lock.dynamodb.billing_mode (optional)
使用基于DynamoDB的锁提供程序时,充当Hudi锁表的DynamoDB表的名称由config hoodie.write.lock.DynamoDB.table指定。此DynamoDB表格由Hudi自动创建,因此您不必自己创建表格。如果要使用现有的DynamoDB表,请确保表中存在具有名称键的属性。key属性应该是DynamoDB表的分区键。config hoodie.write.lock.dynamodb.partition_key指定要为key属性(而不是属性名称)放置的值,该值用于同一表上的锁。默认情况下,将hoodie.write.lock.dynamodb.partition_key设置为表名,以便写入同一表的多个写入程序共享相同的锁。如果自定义名称,请确保多个编写器的名称相同。
Datasource Writer
hudi spark模块提供DataSource API来将spark DataFrame写入(和读取)hudi表。
以下是如何通过spark数据源使用optimic_currency_control的示例
inputDF.write.format("hudi")
.options(getQuickstartWriteConfigs)
.option(PRECOMBINE_FIELD_OPT_KEY, "ts")
.option("hoodie.cleaner.policy.failed.writes", "LAZY")
.option("hoodie.write.concurrency.mode", "optimistic_concurrency_control")
.option("hoodie.write.lock.zookeeper.url", "zookeeper")
.option("hoodie.write.lock.zookeeper.port", "2181")
.option("hoodie.write.lock.zookeeper.lock_key", "test_table")
.option("hoodie.write.lock.zookeeper.base_path", "/test")
.option(RECORDKEY_FIELD_OPT_KEY, "uuid")
.option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath")
.option(TABLE_NAME, tableName)
.mode(Overwrite)
.save(basePath)
HoodieDeltaStreamer实用程序(hudi实用程序包的一部分)提供了从不同来源(如DFS或Kafka)进行摄取的方法,具有以下功能。
DeltaStreamer
通过delta streamer使用optimic_currency_control需要将上述配置添加到可以传递给作业的属性文件中。例如,将配置添加到kafka-source.properties文件并将其传递给deltastreamer将启用乐观并发。然后可以按如下方式触发deltastreamer作业:
通过delta streamer使用optimic_currency_control需要将上述配置添加到可以传递给作业的属性文件中。例如,将配置添加到kafka-source.properties文件并将其传递给deltastreamer将启用乐观并发。然后可以按如下方式触发deltastreamer作业:
[hoodie]$ spark-submit --class org.apache.hudi.utilities.deltastreamer.HoodieDeltaStreamer `ls packaging/hudi-utilities-bundle/target/hudi-utilities-bundle-*.jar` \
--props file://${PWD}/hudi-utilities/src/test/resources/delta-streamer-config/kafka-source.properties \
--schemaprovider-class org.apache.hudi.utilities.schema.SchemaRegistryProvider \
--source-class org.apache.hudi.utilities.sources.AvroKafkaSource \
--source-ordering-field impresssiontime \
--target-base-path file:\/\/\/tmp/hudi-deltastreamer-op \
--target-table uber.impressions \
--op BULK_INSERT
使用乐观并发控制时的最佳实践
并发写入Hudi表需要使用Zookeeper或HiveMetastore获取锁。由于几个原因,您可能需要配置重试以允许应用程序获取锁。
网络连接或服务器上的过度负载增加了获取锁的时间,从而导致超时
运行大量正在写入同一hudi表的并发作业可能会导致锁获取过程中的争用,从而导致超时
在一些冲突解决方案中,Hudi提交操作可能需要10秒的时间,而锁被持有。这可能导致等待获取锁的其他作业超时。
设置正确的本机锁提供程序客户端重试次数。注意,有时这些设置在服务器上设置一次,所有客户端都继承相同的配置。在启用乐观并发之前,请检查您的设置。
hoodie.write.lock.wait_time_ms
hoodie.write.lock.num_retries
为Zookeeper和HiveMetastore设置正确的hudi客户端重试次数。这在无法更改本机客户端重试设置的情况下非常有用。请注意,除了您可能设置的任何本机客户端重试之外,还会发生这些重试。
hoodie.write.lock.client.wait_time_ms
hoodie.write.lock.client.num_retries
为这些设置正确的值取决于具体情况;已为一般情况提供了一些默认值。
禁用多重写入
删除用于启用多编写器或使用默认值覆盖的以下设置。
hoodie.write.concurrency.mode=single_writer
hoodie.cleaner.policy.failed.writes=EAGER
注意事项
如果您使用的是WriteClient API,请注意,需要从写入客户端的两个不同实例启动对表的多次写入。不建议使用写客户端的同一实例来执行多写。