Hudi快速入门
Hudi中文文档
Hudi介绍(含有测试代码,见博客github)
spark2.4版本
以上才能整合Hudi
流式读/写
流
的输入和输出特殊列
添加并跟踪记录级的元数据自管理
万物皆日志
键-值数据模型
COW表写的时候数据直接写入basefile,(parquet)不写log文件。所以COW表的文件片只包含basefile
(一个parquet文件构成一个文件片)
关键目标是是使用partitioner将tagged Hudi记录RDD(所谓的tagged是指已经通过索引查询,标记每条输入记录在表中的位置)分成一些列的updates
和inserts
.为了维护文件大小,我们先对输入进行采样,获得一个工作负载profile,这个profile记录了输入记录的insert和update、以及在分区中的分布等信息。把数据从新打包,这样:
1)对于updates, 该文件ID的最新版本都将被重写一次,并对所有已更改的记录使用新值
2)对于inserts,记录首先打包到每个分区路径中的最小文件中,直到达到配置的最大大小。
之后的所有剩余记录将再次打包到新的文件组,新的文件组也会满足最大文件大小要求。
MOR表写数据时,记录首先会被快速的写进日志文件,稍后会使用时间轴上的压缩操作将其与基础文件合并。
根据查询是读取日志中的合并快照流还是变更流,还是仅读取未合并的基础文件,MOR表支持多种查询类型。在高层次上,MOR writer
在读取数据时会经历与COW writer
相同的阶段。这些更新将追加到最新文件篇的最新日志文件中,而不会合并。对于insert,Hudi支持两种模式:
1)插入到日志文件:有可索引日志文件的表会执行此操作(HBase索引);
2)插入parquet文件:没有索引文件的表(例如布隆索引)与写时复制(COW)一样,对已标记位置的输入记录进行分区,
以便将所有发往相同文件id的upsert分到一组。
这批upsert会作为一个或多个日志块写入日志文件。Hudi允许客户端控制日志文件大小。对于写时复制(COW)
和读时合并(MOR)writer
来说,Hudi的WriteClient
是相同的。几轮数据的写入将会累积一个或多个日志文件
。这些日志文件与基本的parquet文件(如有)一起构成一个文件片,而这个文件片代表该文件的一个完整版本。这种表是用途最广、最高级的表。为写(可以指定不同的压缩策略,吸收突发写流量)和查询(例如权衡数据的新鲜度和查询性能)提供了很大的灵活性。同时它包含一个学习曲线,以便在操作上掌控他。
package com.qf.bigdata
import com.qf.bigdata.conf.Config
import com.qf.bigdata.extract.NewsAction
import com.qf.bigdata.util.{
HudiConfig, Meta, SparkHelper}
import org.apache.hudi.DataSourceWriteOptions
import org.apache.hudi.config.{
HoodieIndexConfig, HoodieWriteConfig}
import org.apache.hudi.index.HoodieIndex
import org.apache.spark.sql.streaming.Trigger
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.{
DataFrame, Dataset, SaveMode}
/**
* 数据落地Hudi
*/
object Log2Hudi {
def main(args: Array[String]): Unit = {
System.setProperty("hadoop.home.dir","E:\\hadoop-common")
// 解析命令行参数
val parmas = Config.parseConfig(Log2Hudi, args)
// 初始化上下文
val spark = SparkHelper.getSparkSession(parmas.env)
spark.sparkContext.setLogLevel("WARN")
// 获取数据
val df: DataFrame = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", parmas.brokerList)
.option("subscribe", parmas.sourceTopic)
.option("startingOffsets", "earliest")
.load()
// 导入隐式转换
import spark.implicits._
// 导入内置函数
import org.apache.spark.sql.functions._
// 数据转换DS
val ds: Dataset[String] = df.selectExpr("cast(value as STRING)")
.as[String]
// 加载动态元数据信息
val metaUser = spark.read.json(List(Meta.getMetaJson("user")).toDS())
val metaEvent = spark.read.json(List(Meta.getMetaJson("event")).toDS())
val metaUserSchema: StructType = metaUser.schema
val metaEventSchema: StructType = metaEvent.schema
// 广播元数据信息
val bcSchema = spark.sparkContext.broadcast((metaEventSchema, metaUserSchema))
// 将数据解析拆分
ds.writeStream
.queryName("log2Hudi")
// 保存Offset
.option("checkpointLocation",parmas.checkpointDir)
// 每个批次提交的数据时间间隔 默认300秒
.trigger(Trigger.ProcessingTime(parmas.trigger+" seconds"))
.foreachBatch((batchDF,batchId)=>{
batchDF.cache() // 持久化
// 数据解析
val newDF = batchDF.map(NewsAction.apply.unionMetaAndBody)
.filter(_ != null)
// 加载动态元数据
if(!newDF.isEmpty){
// Event表
newDF.select(
from_json('value,bcSchema.value._1).as("data_event"))
.select("data_event.*") // 查询所有字段
.filter("type='track'") // 符合Event数据
// 写入hudi表
.write.format("org.apache.hudi")
// 配置hudi参数 (Event)
.options(HudiConfig.getEventConfig(parmas.tableType,
parmas.syncJDBCUrl,parmas.syncJDBCUsername))
.option(HoodieWriteConfig.TABLE_NAME,"event") // 设置hudi表名
.option(DataSourceWriteOptions.HIVE_DATABASE_OPT_KEY,parmas.syncDB) // hudi表同步的Hive数据库
// 设置当前分区是否要变更,说白了就是分数据是否要更新或者追加
.option(HoodieIndexConfig.BLOOM_INDEX_UPDATE_PARTITION_PATH,"true")
// 如果要设置追加状态,你要保证我们的数据索引要设置全局
.option(HoodieIndexConfig.INDEX_TYPE_PROP,HoodieIndex.IndexType.GLOBAL_BLOOM.name())
.mode(SaveMode.Append)
.save(parmas.hudiEventBasePath)
// User表
newDF.select(
from_json('value,bcSchema.value._2).as("data_user"))
.select("data_user.*") // 查询所有字段
.filter("type='profile_set'") // 符合Event数据
// 写入hudi表
.write.format("org.apache.hudi")
// 配置hudi参数 (User)
.options(HudiConfig.getUserConfig(parmas.tableType,
parmas.syncJDBCUrl,parmas.syncJDBCUsername))
.option(HoodieWriteConfig.TABLE_NAME,"user") // 设置hudi表名
.option(DataSourceWriteOptions.HIVE_DATABASE_OPT_KEY,parmas.syncDB) // hudi表同步的Hive数据库
// 设置当前分区是否要变更,说白了就是分数据是否要更新或者追加
.option(HoodieIndexConfig.BLOOM_INDEX_UPDATE_PARTITION_PATH,"true")
// 如果要设置追加状态,你要保证我们的数据索引要设置全局
.option(HoodieIndexConfig.INDEX_TYPE_PROP,HoodieIndex.IndexType.GLOBAL_BLOOM.name())
.mode(SaveMode.Append)
.save(parmas.hudiUserBasePath)
batchDF.unpersist()
}
}).start()
.awaitTermination()
}
}