Apache Hudi

Apache Hudi 在基于 HDFS/S3 数据存储之上,提供了两种流原语:

  1. 插入更新
  2. 增量拉取

一般来说,我们会将大量数据存储到HDFS/S3,新数据增量写入,而旧数据鲜有改动,特别是在经过数据清洗,放入数据仓库的场景。而且在数据仓库如 hive中,对于update的支持非常有限,计算昂贵。另一方面,若是有仅对某段时间内新增数据进行分析的场景,则hive、presto、hbase等也未提供原生方式,而是需要根据时间戳进行过滤分析。

在此需求下,Hudi可以提供这两种需求的实现。第一个是对record级别的更新,另一个是仅对增量数据的查询。且Hudi提供了对Hive、presto、Spark的支持,可以直接使用这些组件对Hudi管理的数据进行查询。

存储类型

我们看一下 Hudi 的两种存储类型:

  1. 写时复制(copy on write):仅使用列式文件(parquet)存储数据。在写入/更新数据时,直接同步合并原文件,生成新版本的基文件(需要重写整个列数据文件,即使只有一个字节的新数据被提交)。此存储类型下,写入数据非常昂贵,而读取的成本没有增加,所以适合频繁读的工作负载,因为数据集的最新版本在列式文件中始终可用,以进行高效的查询。
  2. 读时合并(merge on read):使用列式(parquet)与行式(avro)文件组合,进行数据存储。在更新记录时,更新到增量文件中(avro),然后进行异步(或同步)的compaction,创建列式文件(parquet)的新版本。此存储类型适合频繁写的工作负载,因为新记录是以appending 的模式写入增量文件中。但是在读取数据集时,需要将增量文件与旧文件进行合并,生成列式文件。

视图

在了解这两种存储类型后,我们再看一下Hudi支持的存储数据的视图(也就是查询模式):

  1. 读优化视图(Read Optimized view):直接query 基文件(数据集的最新快照),也就是列式文件(如parquet)。相较于非Hudi列式数据集,有相同的列式查询性能
  2. 增量视图(Incremental View):仅query新写入数据集的文件,也就是指定一个commit/compaction,query此之后的新数据。
  3. 实时视图(Real-time View):query最新基文件与增量文件。此视图通过将最新的基文件(parquet)与增量文件(avro)进行动态合并,然后进行query。可以提供近实时的数据(会有几分钟的延迟)

在以上3种视图中,“读优化视图”与“增量视图”均可在“写时复制”与“读时合并”的存储类型下使用。而“实时视图“仅能在”读时合并“模式下使用。

存储类型

支持的视图

写时复制

读优化 + 增量

读时合并

读优化 + 增量 + 近实时

时间轴

最后介绍一下 Hudi 的核心 —— 时间轴。Hudi 会维护一个时间轴,在每次执行操作时(如写入、删除、合并等),均会带有一个时间戳。通过时间轴,可以实现在仅查询某个时间点之后成功提交的数据,或是仅查询某个时间点之前的数据。这样可以避免扫描更大的时间范围,并非常高效地只消费更改过的文件(例如在某个时间点提交了更改操作后,仅query某个时间点之前的数据,则仍可以query修改前的数据)。

使用案例

下面我们尝试使用Hudi API 进行读写。

写入数据

首先准备数据集,部分条目为:

1535908|Big Run|Stream|WV|38.6370428|-80.8595469|794

875609|Constable Hook|Cape|NJ|40.657881|-74.0990309|7

1217998|Gooseberry Island|Island|RI|41.4534361|-71.3253284|10

26603|Boone Moore Spring|Spring|AZ|34.0895692|-111.410065|3681

1506738|Missouri Flat|Flat|WA|46.7634987|-117.0346113|2605

启动spark-shell,并指定hudi jar包:

spark-shell --conf "spark.serializer=org.apache.spark.serializer.KryoSerializer" --conf "spark.sql.hive.convertMetastoreParquet=false" --jars /usr/lib/hudi/hudi-spark-bundle.jar,/usr/lib/spark/external/lib/spark-avro.jar

加载指定包:

import scala.collection.JavaConversions._
import org.apache.spark.sql.SaveMode._
import org.apache.hudi.DataSourceReadOptions._
import org.apache.hudi.DataSourceWriteOptions._
import org.apache.hudi.config.HoodieWriteConfig._
import org.apache.hudi.hive.MultiPartKeysValueExtractor

指定创建的Hudi表名与路径:

val tableName = "hudi_table"
val basePath = "s3://xxxx/xxx"

构造 DataFrame:

val lineRDD = sc.textFile("features.txt").map(_.split("\\|")).filter(_.length > 6)
case class Record(id:Int, name:String, c_class:String, state:String, latitude:Float, longitude:String, elevation:Int)
val RecordRDD = lineRDD.map(x=>Record(x(0).toInt, x(1), x(2), x(3), x(4).toFloat, x(5), x(6).toInt))
val featureDF=RecordRDD.toDF

插入数据到 Hudi(以及Hive):

featureDF.write.format("org.apache.hudi").
option(RECORDKEY_FIELD_OPT_KEY, "c_class").
option(PARTITIONPATH_FIELD_OPT_KEY, "state").
option(PRECOMBINE_FIELD_OPT_KEY, "id").
option(TABLE_NAME, tableName).
option(HIVE_SYNC_ENABLED_OPT_KEY, "true").
option(HIVE_TABLE_OPT_KEY, "hivehudi").
option(HIVE_PARTITION_FIELDS_OPT_KEY, "state").
option(HIVE_PARTITION_EXTRACTOR_CLASS_OPT_KEY, classOf[MultiPartKeysValueExtractor].getName).
mode(Overwrite).
save(basePath);

我们可以看到目录结构类似于 Hive:

hudi/hudi_table/AR/44bfae35-056b-4bcd-8970-5f1271c3845d-0_18-215-89206_20191121100011.parquet

hudi/hudi_table/CA/2a591ee9-afa4-48d9-bd16-63376a1b8e06-0_38-215-89226_20191121100011.parquet

hudi/hudi_table/CT/911510f9-0655-405f-afad-be9c15429e81-0_46-215-89234_20191121100011.parquet

表名为hudi_table,分区键为 state,真正存储数据的文件为parquet。

查询数据

首先载入数据格式:

val toViewDF = spark.read.format("org.apache.hudi").load(basePath + "/*/*")

我们在上面插入数据的时候,同时创建了 Hive 表,所以有以下两种方式做查询:

  1. 直接查询 Hive 表:

  spark.sql("select name from hivehudi where c_class='Summit'").show()

  +--------------------+

  |                name|

  +--------------------+

  |           High Knob|

  | White Rock Mountain|

  |      Open Mine Hill|

2. 使用临时表:

  roViewDF.registerTempTable("hudi_ro_table")

  spark.sql("select id,name from hudi_ro_table where c_class='Stream'").show()

  +-------+--------------------+

  |     id|                name|

  +-------+--------------------+

  | 539931|   Tiger Point Gully|

  | 871801|           Dry Brook|

  | 847407|      McClusky Creek|

  | 637687|          Shaw Drain|

  | 749747|        Duncan Creek|

  |1502779|         Brushy Lick|

  …

更新数据

首先我们看一条数据:

spark.sql("select id,name from hudi_ro_table where c_class='Stream' and id=539931").show()

+------+-----------------+

|    id|             name|

+------+-----------------+

|539931|Tiger Point Gully|

然后更新此数据(更新的数据存储在一个新的源文件中):

val updateRDD = sc.textFile("update.txt").map(_.split("\\|")).filter(_.length>6)
val updateDF = updateRDD.map(x=>Record(x(0).toInt, x(1), x(2), x(3), x(4).toFloat, x(5), x(6).toInt)).toDF
updateDF.write.format("org.apache.hudi").
option(RECORDKEY_FIELD_OPT_KEY, "c_class").
option(PARTITIONPATH_FIELD_OPT_KEY, "state").
option(PRECOMBINE_FIELD_OPT_KEY, "id").
option(TABLE_NAME, tableName).
option(HIVE_SYNC_ENABLED_OPT_KEY, "true").
option(HIVE_TABLE_OPT_KEY, "hivehudi").
option(HIVE_PARTITION_FIELDS_OPT_KEY, "state").
option(HIVE_PARTITION_EXTRACTOR_CLASS_OPT_KEY, classOf[MultiPartKeysValueExtractor].getName).
mode(Append).
save(basePath);

可以看到我们这里使用的模式由Overwrite 改为了 Append,也就是追加的模式,其余的基本不变。我们首先分别看一下 hive 表与 hudi 表中的数据变化。

Hive 表中:

spark.sql("select id,name from hivehudi where c_class='Stream' and id=539931").show()

+------+-----------------+

|    id|             name|

+------+-----------------+

|539931|Tiger Point Gully|

|539931|     Tiger-update|

+------+-----------------+

Hudi 表中:

val appViewDF = spark.read.format("org.apache.hudi").load(basePath + "/*/*")
appViewDF.registerTempTable("hudi_update_table")
spark.sql("select id,name from hudi_update_table where c_class='Stream' and id=539931").show()

+------+-----------------+

|    id|             name|

+------+-----------------+

|539931|Tiger Point Gully|

|539931|     Tiger-update|

+------+-----------------+

可以看到均可以查到更新后的数据。

对数据执行 select * 加上过滤条件:

可以看到表中有2个比较有意思的字段,分别为:_hoodie_commit_time, _hoodie_commit_seqno

上文我们提到过 Hudi 有一个核心为时间轴,每次执行一个commit时,都会生成一个时间戳。这里 _hoodie_commit_time 即记录了commit 的时间戳。进一步的,Hudi 便是基于此实现了增量查询。

下面我们尝试一下增量查询:

// 获取 commit 时间戳
val commits = spark.sql("select distinct(_hoodie_commit_time) as commitTime from hudi_update_table order by commitTime").map(k => k.getString(0)).take(3) // 设置起始时间戳为上次时间戳
val beginTime = commits(commits.length - 2) // 增量查询
val incViewDF = spark.
read.
format("org.apache.hudi").
option(VIEW_TYPE_OPT_KEY, VIEW_TYPE_INCREMENTAL_OPT_VAL).
option(BEGIN_INSTANTTIME_OPT_KEY, beginTime).
load(basePath);
incViewDF.registerTempTable("hudi_incr_table")
spark.sql("select * from hudi_incr_table where c_class='Stream' and id=539931").show()

这里我们使用增量查询的选项 VIEW_TYPE_INCREMENTAL_OPT_VAL,以及设置了时间戳的起始时间。查询结果为:

可以看到查询到的数据仅为上次commit 后的数据。

当然,我们也可以指定时间段内的数据进行查询,指定下面选项即可:

    option(BEGIN_INSTANTTIME_OPT_KEY, beginTime).
    option(END_INSTANTTIME_OPT_KEY, endTime).

Hudi CLI

最后我们看下一下 Hudi CLI

// 启动 hudi cli:

/usr/lib/hudi/cli/bin/hudi-cli.sh

// 连接hudi 数据表

connect --path s3://xxxx/hudi/hudi_table

接下来我们可以查看提交过的 commit:

甚至回滚 commit:

commit rollback --commit 20191122073858

回滚后再次对 hive 表执行查询:

spark.sql("select id,name from hivehudi where c_class='Stream' and id=539931").show()

+------+-----------------+

|    id|             name|

+------+-----------------+

|539931|Tiger Point Gully|

+------+-----------------+

可以看到之前更新的数据已经被删除。

在 Hudi  Cli 下,我们也可以创建表(create)、列出commit时文件级别的信息(commit showfiles)等。更多 Hudi cli 的用法,可以在 Hudi Cli 下输入 help 获取更多信息。

References:

Apache Hudi 官方介绍:https://hudi.apache.org/index.html

Apache Hudi Quick Start:https://hudi.apache.org/quickstart.html

Apache Hudi CLI: https://hudi.apache.org/admin_guide.html

Apache Hudi 介绍与应用的更多相关文章

  1. 重磅!Vertica集成Apache Hudi指南

    1. 摘要 本文演示了使用外部表集成 Vertica 和 Apache Hudi. 在演示中我们使用 Spark 上的 Apache Hudi 将数据摄取到 S3 中,并使用 Vertica 外部表访 ...

  2. 基于Apache Hudi构建数据湖的典型应用场景介绍

    1. 传统数据湖存在的问题与挑战 传统数据湖解决方案中,常用Hive来构建T+1级别的数据仓库,通过HDFS存储实现海量数据的存储与水平扩容,通过Hive实现元数据的管理以及数据操作的SQL化.虽然能 ...

  3. 写入Apache Hudi数据集

    这一节我们将介绍使用DeltaStreamer工具从外部源甚至其他Hudi数据集摄取新更改的方法, 以及通过使用Hudi数据源的upserts加快大型Spark作业的方法. 对于此类数据集,我们可以使 ...

  4. 直播 | Apache Kylin & Apache Hudi Meetup

    千呼万唤始出来,Meetup 直播终于来啦- 本次线上 Meetup 由 Apache Kylin 与 Apache Hudi 社区联合举办,将于 3 月 14 日晚进行直播,邀请到来自丁香园.腾讯. ...

  5. Apache Hudi 设计与架构最强解读

    感谢 Apache Hudi contributor:王祥虎 翻译&供稿. 欢迎关注微信公众号:ApacheHudi 本文将介绍Apache Hudi的基本概念.设计以及总体基础架构. 1.简 ...

  6. 使用Apache Spark和Apache Hudi构建分析数据湖

    1. 引入 大多数现代数据湖都是基于某种分布式文件系统(DFS),如HDFS或基于云的存储,如AWS S3构建的.遵循的基本原则之一是文件的"一次写入多次读取"访问模型.这对于处理 ...

  7. 使用Apache Hudi构建大规模、事务性数据湖

    一个近期由Hudi PMC & Uber Senior Engineering Manager Nishith Agarwal分享的Talk 关于Nishith Agarwal更详细的介绍,主 ...

  8. Apache Hudi重磅特性解读之存量表高效迁移机制

    1. 摘要 随着Apache Hudi变得越来越流行,一个挑战就是用户如何将存量的历史表迁移到Apache Hudi,Apache Hudi维护了记录级别的元数据以便提供upserts和增量拉取的核心 ...

  9. Apache Hudi表自动同步至阿里云数据湖分析DLA

    1. 引入 Hudi 0.6.0版本之前只支持将Hudi表同步到Hive或者兼容Hive的MetaStore中,对于云上其他使用与Hive不同SQL语法MetaStore则无法支持,为解决这个问题,近 ...

随机推荐

  1. 修改配置文件application.properties

    附录A.常用应用程序属性 可以在application.properties文件内部application.yml,文件内部或命令行开关中指定各种属性.本附录提供了常见Spring Boot属性的列表 ...

  2. Go 零基础 30 min 入门

        不知不觉用 Go 开发也两年多了. 筹备点经验汇总, 方便后面的同学能快速上手.  提纲     1. Go 安装     2. Go ide 搭建     3. Go modules 模块管 ...

  3. 微信企业号开发入门(回调模式)java

    最近在开发微信企业号,刚接触时云里雾里的,在摸索过程中终于清晰了一点. 刚开始我以为订阅号.服务号.企业号的接口差不多,就一直用订阅号的教程来入门,后来才发现差的挺多的. 首先,微信企业号不像订阅号和 ...

  4. vue —— Toast 内 加变量

    toast正常使用: 在页面内引入: import { Toast } from 'mint-ui' 使用的时候,简单到飞起: Toast('领取成功'); 而如果想在toast中加入变量,也很简单: ...

  5. python编程系列---Pycharm快捷键(更新中....)

    以下是我常用到的Pycharm快捷键(还有很多,只是我暂时用的最多的就这些): 在开发过程中,经常使用一些快捷键会大大提高开发效率,不要因为看这多而不用,常用的就那些,用得多就都记住了,脱离鼠标,逼格 ...

  6. 如何把应用程序移植到k8s

    程序部署环境的容器化已经是大势所趋,微服务为容器化提供了广阔的应用舞台,k8s已经把Docker纳入为它的底层支撑容器引擎,一统江湖,成为了容器技术事实上的标准.一般的应用程序是不能直接拿来部署到容器 ...

  7. 关于while和do while 的个人理解

    先上代码 int x=425; System.out.println("循环开始,我的初始值为:x="+x); //425 do { System.out.println(&quo ...

  8. 百万年薪python之路 -- 面向对象之所有属性及方法

    1.私有成员公有成员 1.1 类的私有属性 # class A: # # name = '周道镕' # __name = 'zdr' # 私有类的属性 # # def func(self): # pr ...

  9. lamda和匿名内部类

    匿名内部类 匿名内部类在日常编程中还是经常会使用的.比如 ArrayList<String> list=new ArrayList<>(); list.add(new Stri ...

  10. PHPStorm IntelliJ IDEA 代码缩进风格设置

    关于缩进风格,我还是觉得4空格比tab好 File -> Settings -> Editor -> Code Style: 如图,把Detect and use exsiting ...