【大数据开发】Hudi——Hudi快速入门

Hudi快速入门
Hudi中文文档
Hudi介绍(含有测试代码,见博客github)
spark2.4版本以上才能整合Hudi

一、设计原则

流式读/写

  • Hudi应用于大型数据集记录的输入和输出
  • Hudi通过其特殊列添加并跟踪记录级的元数据

自管理

  • 如果任务失败会自动回滚,并且再次尝试

万物皆日志

  • 内部存储是以日志的形式保存

键-值数据模型

  • Hudi表被建模为键值对数据集

二、表类型

  • Copy On Write表(COW)——写时复制

COW表写的时候数据直接写入basefile,(parquet)不写log文件。所以COW表的文件片只包含basefile(一个parquet文件构成一个文件片)

关键目标是是使用partitioner将tagged Hudi记录RDD(所谓的tagged是指已经通过索引查询,标记每条输入记录在表中的位置)分成一些列的updatesinserts.为了维护文件大小,我们先对输入进行采样,获得一个工作负载profile,这个profile记录了输入记录的insert和update、以及在分区中的分布等信息。把数据从新打包,这样:

1)对于updates, 该文件ID的最新版本都将被重写一次,并对所有已更改的记录使用新值
2)对于inserts,记录首先打包到每个分区路径中的最小文件中,直到达到配置的最大大小。

之后的所有剩余记录将再次打包到新的文件组,新的文件组也会满足最大文件大小要求。
在这里插入图片描述

  • Merge On Read表 (MOR)——读时合并

MOR表写数据时,记录首先会被快速的写进日志文件,稍后会使用时间轴上的压缩操作将其与基础文件合并。
根据查询是读取日志中的合并快照流还是变更流,还是仅读取未合并的基础文件,MOR表支持多种查询类型。在高层次上,MOR writer在读取数据时会经历与COW writer相同的阶段。这些更新将追加到最新文件篇的最新日志文件中,而不会合并。对于insert,Hudi支持两种模式:

1)插入到日志文件:有可索引日志文件的表会执行此操作(HBase索引);
2)插入parquet文件:没有索引文件的表(例如布隆索引)与写时复制(COW)一样,对已标记位置的输入记录进行分区,
以便将所有发往相同文件id的upsert分到一组。

这批upsert会作为一个或多个日志块写入日志文件。Hudi允许客户端控制日志文件大小。对于写时复制(COW)读时合并(MOR)writer来说,Hudi的WriteClient是相同的。几轮数据的写入将会累积一个或多个日志文件。这些日志文件与基本的parquet文件(如有)一起构成一个文件片,而这个文件片代表该文件的一个完整版本。这种表是用途最广、最高级的表。为写(可以指定不同的压缩策略,吸收突发写流量)和查询(例如权衡数据的新鲜度和查询性能)提供了很大的灵活性。同时它包含一个学习曲线,以便在操作上掌控他。

在这里插入图片描述

三、kafka数据落地Hudi

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()
  }
}

  • 3
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值