scala spark 数据对比_揭秘数据湖——长文详解Hudi从内核到实战(三)

点击上方蓝字 关注我吧 a071dfbd96cec56be96b78485bd914ff.gif

Hudi实战

1

Hudi名称概念

  • Time Line

Hudi的核心是维护不同时间对表执行的所有操作的事件表,这有助于提供表的即时视图,同时还有效地支持按到达顺序进行数据检索。Hudi包含以下组件:

(1)Instant action:在表上的操作类型

(2)Instant time:操作开始的一个时间戳,该时间戳会按照开始时间顺序单调递增

(3)state:即时状态

Hudi保证在时间轴上执行的操作都是原先性的,所有执行的操作包括:

(1)commits:原子的写入一张表的操作

(2)cleans:后台消除了表中的旧版本数据,即表中不在需要的数据

(3)delta_commit:增量提交,将一批数据原子写入到MergeOnRead表中,并且只记录到增量日志中

(4)compaction:后台协调Hudi中的差异数据

(5)rollback:回滚,删除在写入过程中的数据

(6)savepoint:将某些文件标记“已保存”,以便清理数据时不会删除它们,一般用于表的还原,可以将数据还原到某个时间点

任何操作都可以处于以下状态:

(1)Requested:表示已安排操作行为,但是尚未开始

(2)Inflight:表示正在执行当前操作

(3)Completed:表示已完成操作

  • File Managerment

Hudi将表组织成DFS上基本路径下的目录结构。表分位几个分区,与hive类似,每个分区均有唯一标示。

在每个分区内,有多个数据组,每个数据组包含几个文件片,其中文件片包含基本文件和日志文件。Hudi采用MVCC设计,其中压缩操作将日志文件和基本数据文件合并成新的文件片,而清楚操作则将未使用的文件片去除。

  • 索引

Hudi通过使用索引机制,生成hoodie密钥映射对应文件ID,从而提供高效upsert操作。

  • 表类型

1. Copy on Write:仅使用列式存储,例如parquet。仅更新版本号,通过写入过程中执行同步合并来重写文件。

2. Merge on Read:基于列式存储(parquet)和行式存储(arvo)结合的文件更始进行存储。更新记录到增量文件,压缩同步和异步生成新版本的文件。

以下是对比:

e8b09cd46a9a2ababf8b71944b2523a4.png

  • 查询类型

快照查询(Snapshot Queries):查询操作将查询最新快照的表数据。如果是Merge on Read类型的表,它将动态合并最新文件版本的基本数据和增量数据用于显示查询。如果是Copy On Write类型的表,它直接查询parquet表,同时提供upsert/delete操作。

增量查询(Incremental Queries):查询只能看到写入表的新数据。这有效的提供了changestreams来启用增量数据管道。

优化读查询(Read Optimized Queries):查询将查看给定提交/压缩操作表的最新快照。

以下是对比:

7abc4b97f3d8120e91c08c660545e161.png

2

Hudi操作

  • pom.xml

<dependencies>    <dependency>       <groupId>org.apache.hudigroupId>       <artifactId>hudi-clientartifactId>       <version>0.5.3version>    dependency>    <dependency>       <groupId>org.apache.hudigroupId>       <artifactId>hudi-hiveartifactId>       <version>0.5.3version>    dependency>    <dependency>       <groupId>org.apache.hudigroupId>       <artifactId>hudi-spark-bundle_2.11artifactId>       <version>0.5.3version>    dependency>    <dependency>       <groupId>org.apache.hudigroupId>       <artifactId>hudi-commonartifactId>       <version>0.5.3version>    dependency>    <dependency>       <groupId>org.apache.hudigroupId>       <artifactId>hudi-hadoop-mr-bundleartifactId>       <version>0.5.3version>    dependency>        <dependency>       <groupId>org.apache.sparkgroupId>       <artifactId>spark-core_2.11artifactId>               <version>2.4.5version>    dependency>    <dependency>        <groupId>org.apache.sparkgroupId>       <artifactId>spark-sql_2.11artifactId>               <version>2.4.5version>    dependency>    <dependency>       <groupId>org.apache.sparkgroupId>        <artifactId>spark-hive_2.11artifactId>               <version>2.4.5version>    dependency>    <dependency>       <groupId>org.apache.sparkgroupId>       <artifactId>spark-avro_2.11artifactId>        <version>2.4.5version>    dependency>    <dependency>       <groupId>org.scala-langgroupId>       <artifactId>scala-libraryartifactId>               <version>${scala.version}version>    dependency>    <dependency>       <groupId>org.apache.hadoopgroupId>       <artifactId>hadoop-clientartifactId>       <version>2.7.2version>    dependency>    <dependency>       <groupId>com.alibabagroupId>       <artifactId>fastjsonartifactId>        <version>1.2.47version>    dependency>    <dependency>       <groupId>org.apache.sparkgroupId>       <artifactId>spark-hive_2.11artifactId>               <version>2.4.5version>    dependency>    <dependency>       <groupId>org.spark-project.hivegroupId>       <artifactId>hive-jdbcartifactId>       <version>1.2.1.spark2version>    dependency>dependencies>
  • case class

packagecom.atguigu.beancase class DwsMember(                      uid: Int,                      ad_id: Int,                      var fullname:String,                      iconurl: String,                      dt: String,                      dn: String                    )
  • 配置文件

将集群配置文件复制到,项目resource源码包下,使本地环境可以访问hadoop集群。

047f7ac0b91fa30464fbad823ed8fae0.png

  • Hudi写入Hdfs

packagecom.atguigu.hudi.testimport com.atguigu.bean.DwsMemberimport com.atguigu.hudi.util.ParseJsonDataimport org.apache.spark.SparkConfimport org.apache.spark.sql.{SaveMode, SparkSession}object HoodieDataSourceExample {  def main(args: Array[String]): Unit = {   System.setProperty("HADOOP_USER_NAME", "root")    val sparkConf = newSparkConf().setAppName("dwd_member_import")      .set("spark.serializer","org.apache.spark.serializer.KryoSerializer")      .setMaster("local[*]")    val sparkSession =SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()    val ssc = sparkSession.sparkContext   ssc.hadoopConfiguration.set("fs.defaultFS","hdfs://mycluster")    ssc.hadoopConfiguration.set("dfs.nameservices","mycluster")//    insertData(sparkSession)        queryData(sparkSession)  }  /**    * 读取hdfs日志文件通过hudi写入hdfs    *    * @param sparkSession    */  def insertData(sparkSession:SparkSession) = {    import org.apache.spark.sql.functions._    import sparkSession.implicits._    val commitTime =System.currentTimeMillis().toString //生成提交时间    val df =sparkSession.read.text("/user/atguigu/ods/member.log")      .mapPartitions(partitions => {        partitions.map(item => {          val jsonObject =ParseJsonData.getJsonData(item.getString(0))         DwsMember(jsonObject.getIntValue("uid"),jsonObject.getIntValue("ad_id")            ,jsonObject.getString("fullname"),jsonObject.getString("iconurl")            , jsonObject.getString("dt"),jsonObject.getString("dn"))        })      })    val result =df.withColumn("ts", lit(commitTime)) //添加ts 时间戳列      .withColumn("uuid",col("uid")) //添加uuid列如果数据中uuid相同hudi会进行去重     .withColumn("hudipartition", concat_ws("/",col("dt"), col("dn"))) //增加hudi分区列   result.write.format("org.apache.hudi")      //     .options(org.apache.hudi.QuickstartUtils.getQuickstartWriteConfigs)     .option("hoodie.insert.shuffle.parallelism", 12)      .option("hoodie.upsert.shuffle.parallelism",12)     .option("PRECOMBINE_FIELD_OPT_KEY", "ts") //指定提交时间列     .option("RECORDKEY_FIELD_OPT_KEY", "uuid") //指定uuid唯一标示列     .option("hoodie.table.name", "testTable")      //     .option(DataSourceWriteOptions.DEFAULT_PARTITIONPATH_FIELD_OPT_VAL,"dt") //  发现api方式不起作用分区列     .option("hoodie.datasource.write.partitionpath.field","hudipartition") //分区列      .mode(SaveMode.Overwrite)     .save("/user/atguigu/hudi")  }

测试发现,Hudi不能指定多分区列,所以代码上分区列采用两列拼接成一列的方式,提交操作时必须指定ts和uuid。写入成功后查看hadoop路径上的文件。

4812d4b467250ea59eea08452935090a.png

df7cff7d7199dae2e2099bdadb7ddb6b.png

c5e1f9175a622eb864155cee0fb7d47d.png

  • 查询Hdfs上Hudi数据

/**  * 查询hdfs上的hudi数据  *  * @param sparkSession  */def queryData(sparkSession: SparkSession) = {  val df =sparkSession.read.format("org.apache.hudi")   .load("/user/atguigu/hudi/*/*")  df.show()}

显示结果为:

21f799157358a1b176fbe74d3614d753.png

  • 修改Hdfs上Hudi数据

def updateData(sparkSession: SparkSession) = {  import org.apache.spark.sql.functions._  import sparkSession.implicits._  val commitTime =System.currentTimeMillis().toString //生成提交时间  val df =sparkSession.read.text("/user/atguigu/ods/member.log")    .mapPartitions(partitions => {      partitions.map(item => {        val jsonObject =ParseJsonData.getJsonData(item.getString(0))       DwsMember(jsonObject.getIntValue("uid"),jsonObject.getIntValue("ad_id")          , jsonObject.getString("fullname"),jsonObject.getString("iconurl")          ,jsonObject.getString("dt"), jsonObject.getString("dn"))      })    })  val result =df.withColumn("ts", lit(commitTime)) //添加ts 时间戳列    .withColumn("uuid",col("uid")) //添加uuid列如果数据中uuid相同hudi会进行去重   .withColumn("hudipartition", concat_ws("/",col("dt"), col("dn"))) //增加hudi分区列 result.write.format("org.apache.hudi")    //     .options(org.apache.hudi.QuickstartUtils.getQuickstartWriteConfigs)    .option("hoodie.insert.shuffle.parallelism",12)   .option("hoodie.upsert.shuffle.parallelism", 12)   .option("PRECOMBINE_FIELD_OPT_KEY", "ts") //指定提交时间列   .option("RECORDKEY_FIELD_OPT_KEY", "uuid") //指定uuid唯一标示列   .option("hoodie.table.name", "testTable")    //      .option(DataSourceWriteOptions.DEFAULT_PARTITIONPATH_FIELD_OPT_VAL,"dt") //  发现api方式不起作用分区列   .option("hoodie.datasource.write.partitionpath.field","hudipartition") //分区列    .mode(SaveMode.Append)    .save("/user/atguigu/hudi")}

虽然代码操作和新增一样只是修改了插入模式为append,但是hudi会根据uid判断进行更新数据,操作完毕后,生成一份最新的修改后的数据。同时hdfs路径上写入一份数据。

d0fab3a3cac337c5e1c73256fece0b51.png

提交时间发生了变化

20be2811b146b7e203ff7d47d6298065.png

数据条数为94175

10e52ea16552b5f163e512eb0663d7e9.png

  • 增量查询

def incrementalQuery(sparkSession: SparkSession) = {  val beginTime = 20200703130000l  val df =sparkSession.read.format("org.apache.hudi")    .option(DataSourceReadOptions.QUERY_TYPE_OPT_KEY,DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL) //指定模式为增量查询   .option(DataSourceReadOptions.BEGIN_INSTANTTIME_OPT_KEY, beginTime) //设置开始查询的时间戳  不需要设置结束时间戳    .load("/user/atguigu/hudi")  df.show()  println(df.count())}

8e6d285c91abe9ae93dadbc043d2da90.png

根据haoodie_commit_time,时间进行查询,查询增量修改数据,注意参数begintime是和hadoop_commit_time对比而不是跟ts对比。如果beginitime填了比haoodie_commit_time大则会过滤所有数据。

d61fceb5cc2ec87eafb665f55e5168f5.png

8ce10b6ebffd011994aa3a7355d9a9ff.png

  • 指定特定时间查询

def updateData(sparkSession: SparkSession) = {  import org.apache.spark.sql.functions._  import sparkSession.implicits._  val commitTime =System.currentTimeMillis().toString //生成提交时间  val df =sparkSession.read.text("/user/atguigu/ods/member.log")    .mapPartitions(partitions => {      partitions.map(item => {        val jsonObject =ParseJsonData.getJsonData(item.getString(0))       DwsMember(jsonObject.getIntValue("uid"),jsonObject.getIntValue("ad_id")          ,jsonObject.getString("fullname"),jsonObject.getString("iconurl")          , jsonObject.getString("dt"),jsonObject.getString("dn"))      })    }).limit(100)  val result =df.withColumn("ts", lit(commitTime)) //添加ts 时间戳列    .withColumn("uuid",col("uid")) //添加uuid列如果数据中uuid相同hudi会进行去重   .withColumn("hudipartition", concat_ws("/",col("dt"), col("dn"))) //增加hudi分区列 result.write.format("org.apache.hudi")    //     .options(org.apache.hudi.QuickstartUtils.getQuickstartWriteConfigs)   .option("hoodie.insert.shuffle.parallelism", 12)   .option("hoodie.upsert.shuffle.parallelism", 12)    .option("PRECOMBINE_FIELD_OPT_KEY","ts") //指定提交时间列   .option("RECORDKEY_FIELD_OPT_KEY", "uuid") //指定uuid唯一标示列   .option("hoodie.table.name", "testTable")    //     .option(DataSourceWriteOptions.DEFAULT_PARTITIONPATH_FIELD_OPT_VAL,"dt") //  发现api方式不起作用分区列    .option("hoodie.datasource.write.partitionpath.field","hudipartition") //分区列    .mode(SaveMode.Append)    .save("/user/atguigu/hudi")}
def pointInTimeQuery(sparkSession: SparkSession) = {  val beginTime = 20200703150000l  val endTime = 20200703160000l  val df =sparkSession.read.format("org.apache.hudi")   .option(DataSourceReadOptions.QUERY_TYPE_OPT_KEY,DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL) //指定模式为增量查询   .option(DataSourceReadOptions.BEGIN_INSTANTTIME_OPT_KEY, beginTime) //设置开始查询的时间戳   .option(DataSourceReadOptions.END_INSTANTTIME_OPT_KEY, endTime)    .load("/user/atguigu/hudi")  df.show()  println(df.count())}

演示,update时limit只修改100条数据,然后根据时间戳进行查询,只会查询出进行修改的100条符合时间的数据.

95d3027c1caa8e50ab4834341d9013fd.png

3b12c1742fb8fe377172cca6ffae9069.png

0152bf1ad375c7a0136fbdc9e10589e3.gif

●揭秘数据湖——长文详解Hudi从内核到实战(一)

●揭秘数据湖——长文详解Hudi从内核到实战(二)

2d69c26dbc7240186df08a3f1793cab2.gif 想获取更多更全资料,扫码加好友入群

0833aab50432d110d24293c24b75040d.png

该公众号开源为大家解决大数据企业级遇到的各种问题,也欢迎各位大佬积极加入开源共享(共同面对大数据领域各种老大难问题)

来稿请投邮箱:miaochuanhai@126.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值