键生成
Hudi 需要某种方式来定位表中的记录,以便对更新/删除操作高效地合并 base/log 文件,让索引条目能够引用这些行,并且在聚类过程中记录可以在表内移动而不会产生副作用。事实上,大多数数据库也采用类似的技术。Hudi 中的每条记录都由记录键(record key)与可选的分区路径(partition path)组成的唯一标识对来确定,其中分区路径用于限定键的唯一性范围(非全局索引)。对于使用全局索引的表,记录仅由记录键来标识,唯一性会在所有分区间统一生效。
借助键,Hudi 可以施加分区/表级别的唯一性完整性约束,并实现对记录的快速更新与删除。记录键会以特殊字段 _hoodie_record_key 的形式物化(materialize)到表中,以确保即使表生命周期内记录的生成方式发生变化,键的唯一性也能得到维持。如果没有这种物化机制,就无法保证过去为某个新键写入的数据在整张表范围内是唯一的。
Hudi 在写入时提供了多种方式,从输入数据生成记录键。
- 对于 Java 客户端/Spark/Flink 写入器,Hudi 提供了内置的键生成器类(下文介绍),以及一个 接口,供用户编写自定义实现。
- SQL 引擎提供了传入键字段的选项,并使用
PARTITIONED BY子句来控制分区方式。
默认情况下,Hudi 会为 INSERT、BULK_INSERT 写入操作自动生成键,这些键在计算、存储和读取方面都具有高效率,同时满足主键的唯一性要求。与 UUID 相比,自动生成的键具有更高的压缩率,在云存储中每 GB 成本约为 0.023 美元,且生成时的计算开销比 base64/uuid 编码的键低 3-10 倍。
键生成器
Hudi 开箱即用地提供了多种键生成器,JVM 用户可以根据自身需求选用,同时 Hudi 还提供了可插拔的接口,便于用户实现并使用自己的键生成器。
在深入了解不同类型的键生成器之前,我们先来看一些与键生成器相关的常用配置。
| 配置项名称 | 默认值 | 说明 |
|---|---|---|
| hoodie.datasource.write.recordkey.field | 无(可选) | 记录键字段。该字段的值将用作 HoodieKey 的 recordKey 组件。- 配置后,实际值将通过对字段值调用 .toString() 获得。嵌套字段可使用点号表示法指定,例如: a.b.c。- 未配置时,记录键将由 Hudi 自动生成。该功能非常适合日志摄取等没有天然记录键的场景。 Config Param: RECORDKEY_FIELD_NAME |
| hoodie.datasource.write.partitionpath.field | 无(可选) | 分区路径字段。该字段的值将用作 HoodieKey 的 partitionPath 组件。如果需要分区表,则必须指定此项。实际值通过调用 .toString() 获得。Config Param: PARTITIONPATH_FIELD_NAME |
hoodie.datasource.write.keygenerator.typeSIMPLE表示键生成器类型的字符串
Config Param: KEYGENERATOR_TYPE
hoodie.datasource.write.keygenerator.classN/A(可选)键生成器类,实现 org.apache.hudi.keygen.KeyGenerator,用于从输入记录中提取键。
- 设置该配置时,配置值优先生效,不会触发自动推断。
- 未设置时,若设置了
hoodie.datasource.write.keygenerator.type,则使用其配置值,否则触发自动推断。 - 对于自动生成的记录键,若既未配置键生成器类也未配置类型,Hudi 还会自动推断分区方式。例如,若未配置分区字段,Hudi 将认为数据集未分区。
Config Param: KEYGENERATOR_CLASS_NAME
hoodie.datasource.write.hive_style_partitioningfalse(可选)标识是否使用 Hive 风格分区。设置为 true 时,分区目录的名称遵循 <partition_column_name>=<partition_value> 格式。默认为 false(分区目录名称仅为分区值)
Config Param: HIVE_STYLE_PARTITIONING_ENABLE
hoodie.datasource.write.partitionpath.urlencodefalse(可选)在创建目录结构之前,是否对分区路径值进行 URL 编码。
Config Param: URL_ENCODE_PARTITIONING
所有高级配置请参阅此处。
SIMPLE
这是最常用的选项。记录键由模式中的两个字段生成,一个用于记录键,一个用于分区路径。值按 DataFrame 中的原样解释并转换为字符串。
COMPLEX
记录键和分区路径均由一个或多个按名称指定的字段组成(多个字段的组合)。配置值中的字段应以逗号分隔。例如 "Hoodie.datasource.write.recordkey.field" : “col1,col4”
NON_PARTITION
如果你的 Hudi 数据集未分区,可以使用这个 “NonpartitionedKeyGenerator”,它会为所有记录返回空的分区路径。换句话说,所有记录都会进入同一个分区(即空分区 “”)
CUSTOM
这是 KeyGenerator 的通用实现,用户可以同时获得 SimpleKeyGenerator、ComplexKeyGenerator 和 TimestampBasedKeyGenerator 各自的优势。记录键和分区路径既可以配置为单个字段,也可以配置为多个字段的组合。
hoodie.datasource.write.recordkey.field
hoodie.datasource.write.partitionpath.field
hoodie.datasource.write.keygenerator.class=org.apache.hudi.keygen.CustomKeyGenerator该 keyGenerator 在你需要定义包含普通字段和基于时间戳字段的复杂分区路径时特别有用。它要求属性 "hoodie.datasource.write.partitionpath.field" 的值采用特定格式,格式应为 "field1:PartitionKeyType1,field2:PartitionKeyType2..."。
完整的分区路径由 <field1 依据 PartitionKeyType1 的值>/<field2 依据 PartitionKeyType2 的值> 依次拼接而成。每种分区键类型可以是 SIMPLE 或 TIMESTAMP。
配置值示例:“field_3:simple,field_5:timestamp”
RecordKey 的配置值在使用 SimpleKeyGenerator 时为单个字段;若使用 ComplexKeyGenerator,则为以逗号分隔的字段名列表。示例:
hoodie.datasource.write.recordkey.field=field1,field2这将以 field1:value1,field2:value2 的格式创建你的记录键,依此类推;否则,对于简单的记录键,你可以只指定一个字段。CustomKeyGenerator 类定义了一个枚举 PartitionKeyType 用于配置分区路径。它有两个可能的值——SIMPLE 和 TIMESTAMP。对于分区表,hoodie.datasource.write.partitionpath.field 属性的值需要以 field1:PartitionKeyType1,field2:PartitionKeyType2 的格式提供,依此类推。例如,如果你想要使用 country 和 date 两个字段创建分区路径,其中后者是基于时间戳的值并且需要按给定格式自定义,你可以指定以下内容
hoodie.datasource.write.partitionpath.field=country:SIMPLE,date:TIMESTAMP这将根据是否启用 Hive 风格分区,创建格式为 <country_name>/<date> 或 country=<country_name>/date=<date> 的分区路径。
TIMESTAMP
该密钥生成器的分区字段依赖于时间戳。在为记录生成分区路径值时,字段值会被解析为时间戳,而不仅仅是转换为字符串。记录键与之前相同,仍按字段名指定。使用此 KeyGenerator 时,用户需要额外设置一些配置。
需要设置的配置:
| 配置名称 | 默认值 | 说明 |
|---|---|---|
| hoodie.keygen.timebased.timestamp.type | 无 (必填) | 仅当密钥生成器为 TimestampBasedKeyGenerator 时需要,取所支持的时间戳类型之一(UNIX_TIMESTAMP、DATE_STRING、MIXED、EPOCHMILLISECONDS、SCALAR) |
| hoodie.keygen.timebased.output.dateformat | ""(可选) | 输出日期格式,例如 yyyy-MM-dd'T'HH:mm:ss.SSSZ |
| hoodie.keygen.timebased.timezone | "UTC"(可选) | 当输入和输出时间戳的时区相同时,指定输入和输出时间戳的时区,例如 UTC。如果输入和输出时区不同,请改用 hoodie.keygen.timebased.input.timezone 和 hoodie.keygen.timebased.output.timezone。 |
| hoodie.keygen.timebased.input.dateformat | ""(可选) | 输入日期格式,例如 yyyy-MM-dd'T'HH:mm:ss.SSSZ。 |
下面我们来看一些 TimestampBasedKeyGenerator 的配置示例值。
时间戳为 GMT
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "EPOCHMILLISECONDS" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyy-MM-dd hh" |
hoodie.streamer.keygen.timebased.timezone | "GMT+8:00" |
输入字段值:“1578283932000L”
由 key generator 生成的分区路径:“2020-01-06 12”
如果某些行的输入字段值为 null。
由 key generator 生成的分区路径:“1970-01-01 08”
时间戳类型为 DATE_STRING
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "DATE_STRING" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyy-MM-dd hh" |
hoodie.streamer.keygen.timebased.timezone | "GMT+8:00" |
hoodie.streamer.keygen.timebased.input.dateformat | "yyyy-MM-dd hhss" |
输入字段值:“2020-01-06 12:12:12”
由 key generator 生成的分区路径:“2020-01-06 12”
如果某些行的输入字段值为 null。
由 key generator 生成的分区路径:“1970-01-01 12:00:00”
标量示例
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "SCALAR" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyy-MM-dd hh" |
hoodie.streamer.keygen.timebased.timezone | "GMT" |
hoodie.streamer.keygen.timebased.timestamp.scalar.time.unit | "days" |
输入字段值:“20000L”
由 key generator 生成的分区路径:“2024-10-04 12”
如果输入字段值为 null。
由 key generator 生成的分区路径:“1970-01-02 12”
ISO8601WithMsZ 单输入格式
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "DATE_STRING" |
hoodie.streamer.keygen.timebased.input.dateformat | "yyyy-MM-dd'T'HHss.SSSZ" |
hoodie.streamer.keygen.timebased.input.dateformat.list.delimiter.regex | "" |
hoodie.streamer.keygen.timebased.input.timezone | "" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyyMMddHH" |
hoodie.streamer.keygen.timebased.output.timezone | "GMT" |
输入字段值:"2020-04-01T13:01:33.428Z"
由 key generator 生成的分区路径:"2020040113"
ISO8601WithMsZ 多输入格式
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "DATE_STRING" |
hoodie.streamer.keygen.timebased.input.dateformat | "yyyy-MM-dd'T'HHssZ,yyyy-MM-dd'T'HHss.SSSZ" |
hoodie.streamer.keygen.timebased.input.dateformat.list.delimiter.regex | "" |
hoodie.streamer.keygen.timebased.input.timezone | "" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyyMMddHH" |
hoodie.streamer.keygen.timebased.output.timezone | "UTC" |
输入字段值:"2020-04-01T13:01:33.428Z"
由密钥生成器生成的分区路径:"2020040113"
使用多种输入格式的带偏移量的 ISO8601 无毫秒格式
| 配置名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "DATE_STRING" |
hoodie.streamer.keygen.timebased.input.dateformat | "yyyy-MM-dd'T'HHssZ,yyyy-MM-dd'T'HHss.SSSZ" |
hoodie.streamer.keygen.timebased.input.dateformat.list.delimiter.regex | "" |
hoodie.streamer.keygen.timebased.input.timezone | "" |
hoodie.streamer.keygen.timebased.output.dateformat | "yyyyMMddHH" |
hoodie.streamer.keygen.timebased.output.timezone | "UTC" |
输入字段值:"2020-04-01T13:01:33-05:00"
由密钥生成器生成的分区路径:"2020040118"
输入为简短日期字符串,并期望输出指定日期格式的日期
| 配置项名称 | 值 |
|---|---|
hoodie.streamer.keygen.timebased.timestamp.type | "DATE_STRING" |
hoodie.streamer.keygen.timebased.input.dateformat | "yyyy-MM-dd'T'HHssZ,yyyy-MM-dd'T'HHss.SSSZ,yyyyMMdd" |
hoodie.streamer.keygen.timebased.input.dateformat.list.delimiter.regex | "" |
hoodie.streamer.keygen.timebased.input.timezone | "UTC" |
hoodie.streamer.keygen.timebased.output.dateformat | "MM/dd/yyyy" |
hoodie.streamer.keygen.timebased.output.timezone | "UTC" |
输入字段值:"20200401"
由键生成器生成的分区路径:"04/01/2020"
斜杠分隔的日期分区
默认情况下,Hudi 会将日期类型的分区路径写成扁平字符串(例如 2024-03-15)。当 hoodie.datasource.write.slash.separated.date.partitioning 设置为 true 时,格式为 yyyy-MM-dd 的分区字段值将存储为 yyyy/MM/dd 的目录层级结构(例如 2024/03/15)。
| 配置项名称 | 默认值 | 说明 |
|---|---|---|
hoodie.datasource.write.slash.separated.date.partitioning | false | 为 true 时,将日期分区值从 yyyy-MM-dd 转换为 yyyy/MM/dd 目录路径。不能与 Hive 风格分区(hoodie.datasource.write.hive_style_partitioning=true)同时使用。 |
示例:
df.write.format("hudi")
.option("hoodie.datasource.write.partitionpath.field", "event_date")
.option("hoodie.datasource.write.slash.separated.date.partitioning", "true")
.option("hoodie.table.name", tableName)
.mode("append")
.save(basePath)event_date = "2024-03-15" 的记录将被存储在 basePath/2024/03/15/ 下,而不是 basePath/2024-03-15/。
注意
Spark SQL 中的 SHOW PARTITIONS 能够正确处理以斜杠分隔的日期分区路径:为了便于阅读,它会以 yyyy-MM-dd 形式显示该值(即将 / 分隔符规范化回 -)。
博客
评论
登录后参与评论
KnowForge