近两年,KUDU 在大数据平台的应用越来越广泛。在阿里、小米、网易等公司的大数据架构中,KUDU 都有着不可替代的地位。本文通过分析 KUDU 的设计, 试图解释为什么 KUDU 会被广泛应用于大数据领域,因为还没有研究过 KUDU 的代码,下面的介绍是根据 KUDU 的论文和网上的一些资料学习自己理解所得,如有不实之处,劳请指正。
在 KUDU 之前,大数据主要以两种方式存储:
那为什么HBase不适合做分析呢?
因为分析需要批量获取数据,而HBase本身的设计并不适合批量获取数据
1)HBase是列式数据库,其实从底层存储的角度来说它并不是列式的,获取指定列数据时是会读到其他列数据的。相对而言Parquet格式针对分析场景就做了很多优化。
2)HBase是LSM-Tree架构的数据库,这导致了HBase读取数据路径比较长,从内存到磁盘,可能还需要读多个HFile文件做版本合并。
LSM 的中心思想就是将随机写转换为顺序写来大幅提高写入操作的性能,但是牺牲了部分读的性能。随机读写是指对任意一个位置的读和写,磁盘随机读写慢是因为需要寻道,倒带才可以访问到指定存储点,而内存不需要,可以任意指定存储点。
从上面分析可知,这两种数据在存储方式上完全不同,进而导致使用场景完全不同,但在真实的场景中,边界可能没有那么清晰,面对既需要随机读写,又需要批量分析的大数据场景,该如何选择呢?这个场景中,单种存储引擎无法满足业务需求,我们需要通过多种大数据工具组合来满足这一需求,一个常见的方案是:
满足数据更新+批量分析的大数据架构
为了让数据平台同时具备随机读写和批量分析能力,传统的做法是采用混合架构(hybrid architecture),也就是我们常说的T+1的方式,数据实时更新在HBase,第二天凌晨同步到HDFS做离线分析。
如上图所示,数据实时写入 HBase,实时的数据更新也在 HBase 完成,为了应对 OLAP 需求,我们定时(通常是 T+1 或者 T+H)将 HBase 数据写成静态的文件(如:Parquet)导入到 OLAP 引擎(如:HDFS),第二天凌晨同步到HDFS做离线分析。这一架构能满足既需要随机读写,又可以支持 OLAP 分析的场景,但他有如下缺点:
这样的缺点很明显,时效性差,数据链路长,过程复杂,开发成本高。
其实第二种方式已经能够满足多维度看数据的需求了,只是基于Hive跑小时级任务实在太消耗资源,而且有延迟的风险,那么有没有一种存储技术具有高吞吐量连续读取数据的能力,而且也支持低延迟的随机读写呢?一般来说这2个要求是互斥的,但是Kudu却在这2者之间做了很好的平衡。
为了解决上述架构的这些问题,KUDU 应运而生。KUDU 的定位是 「Fast Analytics on Fast Data」,是一个既支持随机读写、又支持 OLAP 分析的大数据存储引擎
KUDU 定位
从上图可以看出,KUDU 是一个「折中」的产品,在 HDFS 和 HBase 这两个偏科生中平衡了随机读写和批量分析的性能。从 KUDU 的诞生可以说明一个观点:底层的技术发展很多时候都是上层的业务推动的,脱离业务的技术很可能是「空中楼阁」。
KUDU 的数据模型与传统的关系型数据库类似,有表结构,需定义Schema信息,一个 KUDU 集群由多个表组成,每个表由多个字段组成,一个表必须指定一个由若干个(>=1)字段组成的主键,如下图:
KUDU 数据模型
KUDU 表中的每个字段是强类型的,而不是 HBase 那样所有字段都认为是 bytes。这样做的好处是可以对不同类型数据进行不同的编码,节省空间。需在写入数据前定义好每一列的类型,(方便做类似于parquet的列式存储)。同时,因为 KUDU 的使用场景是 OLAP 分析,有一个数据类型对下游的分析工具也更加友好。
KUDU 的对外 API 主要分为写跟读两部分。其中写包括:Insert、Update、Delete,所有写操作都必须指定主键;读 KUDU 对外只提供了 Scan 操作,Scan 时用户可以指定一个或多个过滤器,用于过滤数据。
跟大多数关系型数据库一样,KUDU 也是通过 MVCC(Multi-Version Concurrency Control)来实现内部的事务隔离。KUDU 默认的一致性模型是 Snapshot Consistency,即客户端可以一致的访问到某个时间点的一个快照。
如果有更高的外部一致性(external consistency)需求,KUDU 目前还没有实现,不过 KUDU 提供了一些设计方案。这里先介绍下外部一致性,它是指:多个事务并发执行达到串行效果,并且保证修改时间戳严格按照事务发生先后顺序,即如果有先后两个事务 A、B, A 发生在 B 之前,那么对于客户端来说,要么看到 A,要么看到 A、B,不会只看到 B 而看不到 A。KUDU 提供了两个实现外部一致性的方案:
这里我们衍生介绍下 Google Spanner 是如何实现分布式事务的外部一致性的。首先我们先明确下分布式事务外部一致性这个问题的由来。首先,在数据库中,我们出于性能考虑,一般我们对读不加排他锁,只对写进行加排他锁,这就会带来一个问题,数据在读取的时候可能正在被修改,导致同一事务中多次读取到的数据可能不一致,为了解决这个问题,我们引入了 MVCC。在单机系统中,通过 MVCC 就能解决外部一致性问题,因为每个事务都有一个在本机生成的一个时间戳,根据事务的时间戳先后,我们就能判断出事务发生的先后顺序。但是在分布式系统中,要实现外部一致性就没有那么简单了,核心问题是事务在不同的机器上执行,而不同机器的本地时钟是有误差的,因此就算是真实发生的事务顺序是 A->B,但是在事务持久化的时候记录的时间戳可能是 B < A,这时如果一个事务 C 来读取数据,可能只读到 B 而没有读到 A。从上面的分析我们可以发现,分布式系统中保证事务的外部一致性的核心是一个精确的事务版本(时间戳),而最大的难点也在这里,计算机上的时钟不是一个绝对精确的时间,它跟标准时间是有一定的随机的误差的,导致分布式系统中不同机器之间的时间有偏差。Google Spanner 的解决思路是把不同机器的误差时间控制在一个很小的确定的范围内,再配合 commit-wait 机制来实现外部一致性。
控制时间误差的方案称为 TrueTime,它通过硬件(GPS 和原子钟)和软件结合,保证获取到的时间在较小误差(±4ms)内绝对正确,具体的实现这里就不展开了,有兴趣的同学可以自行找资料研究。TrueTime 对外只提供 3 个 API,如下:TrueTime API
这里最主要的 API 是 TT.now(),它范围当前绝对精确时间的上下界,表示当前绝对精确时间在 TT.now().earliest 和 TT.now().latest 之间。
有了一个有界误差的 TrueTime 后,就可以通过 commit-wait 机制来实现外部一致性了,具体的方案如下:
commit-wait 过程
如上图所示,在一个事务开始获取锁执行后,生成事务的时间版本 s=TT.now().latest,然后开始执行事务的具体操作,但是一个事务的结束并不只由事务本身的时间消耗决定,它还要保证后续的事务时间版本不会早于自己,因此,事务需要等待直到 TT.now().earliest > s 后,才算真正结束。根据整个 commit-wait 过程我们可以知道,整个事务提交过程需要等待 2 倍的平均误差时间(ε),TrueTime 的平均误差时间是 4 ms,因此一次 commit-wait 需要至少 8 ms。
之前我们提到,KUDU 也借鉴 Spanner 使用 commit-wait 机制实现外部一致性,但是 commit-wait 强依赖于 TrueTime,而 TrueTime 需要各种昂贵的硬件设备支持,目前 KUDU 通过纯软件算法的方式来实现时钟算法,为 HybridTime,但这个方案时间误差较大,考虑到 commit-wait 需要等待 2ε 时间,因此误差一大实际场景使用限制就很多了。
KUDU 中存在两个角色
为了实现分区容错性,跟其他大数据产品一样,对于每个角色,在 KUDU 中都可以设置特定数量(一般是 3 或 5)的副本。各副本间通过 Raft 协议来保证数据一致性。Raft 协议与 ZAB 类似,都是 Paxos 协议的工程简化版本
KUDU Client 在与服务端交互时,先从 Master Server 获取元数据信息,然后去 Tablet Server 读写数据
与大多数大数据存储引擎类似,KUDU 对表进行横向分区,KUDU 表会被横向切分存储在多个 tablets 中。不过相比与其他存储引擎,KUDU 提供了更加丰富灵活的数据分区策略。
一般数据分区策略主要有两种,
一、一种是 Range Partitioning,按照字段值范围进行分区,HBase 就采用了这种方式,如下图:
Range Partitioning 的优势是在数据进行批量读的时候,可以把大部分的读变成同一个 tablet 中的顺序读,能够提升数据读取的吞吐量。并且按照范围进行分区,我们可以很方便的进行分区扩展。其劣势是同一个范围内的数据写入都会落在单个 tablet 上,写的压力大,速度慢。
二、另一种分区策略是 Hash Partitioning,按照字段的 Hash 值进行分区,Cassandra 采用了这个方式,见下图:
Hash Partitioning
与 Range Partitioning 相反,由于是 Hash 分区,数据的写入会被均匀的分散到各个 tablet 中,写入速度快。但是对于顺序读的场景这一策略就不太适用了,因为数据分散,一次顺序读需要将各个 tablet 中的数据分别读取并组合,吞吐量低。并且 Hash 分区无法应对分区扩展的情况。
各种分区策略的优劣对比见下图:
各种分区策略的优劣
既然各分区策略各有优劣,能否将不同分区策略进行组合,取长补短呢?这也是 KUDU 的思路,KUDU 支持用户对一个表指定一个范围分区规则和多个 Hash 分区规则,如下图:
组合分区策略
KUDU 是一个列式存储的存储引擎,其数据存储方式如下:
KUDU 存储
列式存储的数据库很适合于 OLAP 场景,其特点如下:
一张表table会分成若干个tablet,每个tablet包括MetaData元信息及若干个RowSet,RowSet包含一个MemRowSet及若干个DiskRowSet,DiskRowSet中包含一个BloomFile、Ad_hoc Index、BaseData、DeltaMem及若干个RedoFile和UndoFile(UndoFile一般情况下只有一个)。
• MemRowSet:用于新数据insert及已在MemRowSet中的数据的更新,一个MemRowSet写满后会将数据刷到磁盘形成若干个DiskRowSet。每次到达32M生成一个DiskRowSet。
• DiskRowSet:用于老数据的变更(mutation),后台定期对DiskRowSet做compaction,以删除没用的数据及合并历史数据,减少查询过程中的IO开销。
• BloomFile:根据一个DiskRowSet中的key生成一个bloom filter,用于快速模糊定位某个key是否在DiskRowSet中存在。
• Ad_hocIndex:是主键的索引,用于定位key在DiskRowSet中的具体哪个偏移位置。
• BaseData是MemRowSet flush下来的数据,按列存储,按主键有序。
• UndoFile是基于BaseData之前时间的历史数据,通过在BaseData上apply UndoFile中的记录,可以获得历史数据。
• RedoFile是基于BaseData之后时间的变更(mutation)记录,通过在BaseData上apply RedoFile中的记录,可获得较新的数据。
• DeltaMem用于DiskRowSet中数据的变更mutation,先写到内存中,写满后flush到磁盘形成RedoFile。
kudu自己存储数据不依赖与HDFS存储;不依赖于zookeeper,将它的功能集成进了自身的TMaster;
与其他大数据存储引擎类似,KUDU 的存储也是通过 LSM 树(Log-Structured Merge Tree)来实现的。LSM 的中心思想就是将随机写转换为顺序写来大幅提高写入操作的性能,但是牺牲了部分读的性能。
KUDU 的最小存储单元是 RowSets,KUDU 中存在两种 RowSets:MemRowSets、DiskRowSets,数据先写内存中的 MemRowSet,MemRowSet 满了后刷到磁盘成为一个 DiskRowSet,DiskRowSet 一经写入,就无法修改了。见下图:
当然上面只是最粗粒度的一个写入过程,为了解释 KUDU 的为什么既能支持随机读写,又能支持大数据量的 OLAP 分析,我们需要更进一步进行解剖分析。我们需求探究的主要两个问题是:
应对数据变更
首先上面我们讲了,DiskRowSet 是不可修改了,那么 KUDU 要如何应对数据的更新呢?在 KUDU 中,把 DiskRowSet 分为了两部分:base data、delta stores。base data 负责存储基础数据,delta stores负责存储 base data 中的变更数据。整个数据更新方案如下:
应对数据更新
如上图所示,数据从 MemRowSet 刷到磁盘后就形成了一份 DiskRowSet(只包含 base data),每份 DiskRowSet 在内存中都会有一个对应的 DeltaMemStore,负责记录此 DiskRowSet 后续的数据变更(更新、删除)。DeltaMemStore 内部维护一个 B-树索引,映射到每个 row_offset 对应的数据变更。DeltaMemStore 数据增长到一定程度后转化成二进制文件存储到磁盘,形成一个 DeltaFile,随着 base data 对应数据的不断变更,DeltaFile 逐渐增长。
首先我们从 KUDU 的 DiskRowSet 数据结构上分析:
DiskRowSet 数据结构
从上图可知,在具体的数据(列数据、变更记录)上,KUDU 都做了 B- 树索引,以提高随机读写的性能。在 base data 中,KUDU 还针对主键做了好几类索引(实际上由于 delta store 只记录变更数据,base data 中对主键的索引即本 DiskRowSet 中全局的主键索引):
随着时间的推移,KUDU 中的小文件会越来越多,主要包括各个 DiskRowSet 中的 base data,还有每个 base data 对应的若干份 DeltaFile。小文件的增多会影响 KUDU 的性能,特别是 DeltaFile 中还有很多重复的数据。为了提高性能,KUDU 会进行定期 compaction,compaction 主要包括两部分:
当用户的查询存在列的过滤条件时,KUDU 还可以在查询时进行 延迟物化(Lazy Materialization )来提升性能。举例说明,现在我们有这样一张表:
用户的 SQL 是这样的:
SELECT * FROM tb WHERE sex=‘男’ ADN age > 20
KUDU 中数据查询过程是这样的:
上述查询中,KUDU 真正需要去物理读取的数据只有 id=3 这一行,这样就减少了 IO 数量。
数据写过程
如上图,当 Client 请求写数据时,先根据主键从 Mater Server 中获取要访问的目标 Tablets,然后到依次对应的 Tablet 获取数据。因为 KUDU 表存在主键约束,所以需要进行主键是否已经存在的判断,这里就涉及到之前说的索引结构对读写的优化了。一个 Tablet 中存在很多个 RowSets,为了提升性能,我们要尽可能地减少要扫描的 RowSets 数量。首先,我们先通过每个 RowSet 中记录的主键的(最大最小)范围,过滤掉一批不存在目标主键的 RowSets,然后在根据 RowSet 中的布隆过滤器,过滤掉确定不存在目标主键的 RowSets,最后再通过 RowSets 中的 B-树索引,精确定位目标主键是否存在。如果主键已经存在,则报错(主键重复),否则就进行写数据(写 MemRowSet)。
数据更新过程
数据更新的核心是定位到待更新数据的位置,这块与写入的时候类似,就不展开了,等定位到具体位置后,然后将变更写到对应的 delta store 中。
数据读过程
如上图,数据读取过程大致如下:先根据要扫描数据的主键范围,定位到目标的
Tablets,然后读取 Tablets 中的 RowSets。在读取每个 RowSet 时,先根据主键过滤要 scan 范围,然后加载范围内的 base data,再找到对应的 delta stores,应用所有变更,最后 union 上 MenRowSet 中的内容,返回数据给 Client。
这里介绍一个小米使用 KUDU 的案例。具体的业务场景是这样的:
收集手机App和后台服务发送的 RPC 跟踪事件数据,然后构建一个服务监控和问题诊断的工具。
- 高写入吞吐:每天大于200亿条记录
- 为了能够尽快定位和解决问题,要求系统能够查询最新的数据并能快速返回结果
- 为了方便问题诊断,要求系统能够查询/搜索明细数据(而不只是统计信息)
在使用 KUDU 前,小米的架构是这样的:
一部分源系统数据是通过Scribe(日志聚合系统)把数据写到HDFS,另一部分源系统数据直接写入HBase。然后通过Hive/MR/Spark作业把两部分数据合并,给离线数仓和 OLAP 分析。
在使用 KUDU 后,架构简化成了:
从上图我们可以看到,所有的数据存储都集中到的 KUDU 一个上,减少了整体的架构复杂度,同时,也大大提升了实时性。
链接:https://www.jianshu.com/p/93c602b637a4
spark-shell --jar kudu-spark.jar XXX
scala> import org.apache.kudu.spark.kudu._
================== Read ====================
scala> val df = spark.read.options(Map("kudu.master" -> "centos00:7051", "kudu.table" -> "my_kudu_table")).kudu
scala> df.show(3,false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|9 |Richard|1800 |
|2 |Tony |1000 |
|3 |David |2000 |
+---+-------+------+
only showing top 3 rows
scala> df.select("id","name").show(3,false)
+---+-------+
|id |name |
+---+-------+
|9 |Richard|
|2 |Tony |
|3 |David |
+---+-------+
only showing top 3 rows
scala> df.select("id","name","salary").filter("salary < 1500")show(3,false)
+---+----+------+
|id |name|salary|
+---+----+------+
|2 |Tony|1000 |
|6 |Alex|1400 |
+---+----+------+
// 创建临时表
scala> df.registerTempTable("t")
scala> spark.sql("select id, name, salary from t where id = 1").show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|1 |Phoenix|2000 |
+---+-------+------+
// 创建临时视图
scala> df.createOrReplaceTempView("v")
scala> spark.sql("select id, name, salary from v where id = 1").show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|1 |Phoenix|2000 |
+---+-------+------+
================ insertRows ===================
scala> import org.apache.kudu.spark.kudu.KuduContext
scala> val kc = new KuduContext("centos00:7051",spark.sparkContext)
scala> df.schema
res0: org.apache.spark.sql.types.StructType = StructType(StructField(id,LongType,false), StructField(name,StringType,true), StructField(salary,StringType,true))
scala> df.schema.printTreeString
root
|-- id: long (nullable = false)
|-- name: string (nullable = true)
|-- salary: string (nullable = true)
scala> import org.apache.kudu.client._
scala> import collection.JavaConverters._
scala> kc.createTable("mykudu", df.schema, Seq("id"), new CreateTableOptions().setNumReplicas(1).addHashPartitions(List("id").asJava,3))
res1: org.apache.kudu.client.KuduTable = org.apache.kudu.client.KuduTable@ed6d97f
scala> val tmp = df.filter("id < 6")
tmp: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [id: bigint, name: string ... 1 more field]
scala> tmp.show
+---+-------+------+
| id| name|salary|
+---+-------+------+
| 2| Tony| 1000|
| 3| David| 2000|
| 1|Phoenix| 2000|
| 5| Jimy| 1900|
| 4| Mike| 1500|
+---+-------+------+
scala> kc.insertRows(tmp, "mykudu")
scala> val m = spark.read.options(Map("kudu.master" -> "centos00:7051", "kudu.table" -> "mykudu")).kudu
m: org.apache.spark.sql.DataFrame = [id: bigint, name: string ... 1 more field]
scala> m.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|1 |Phoenix|2000 |
|5 |Jimy |1900 |
|2 |Tony |1000 |
|3 |David |2000 |
|4 |Mike |1500 |
+---+-------+------+
========== deleteRows ==========
scala> df.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|9 |Richard|1800 |
|2 |Tony |1000 |
|3 |David |2000 |
|10 |Phoniex|null |
|1 |Phoenix|2000 |
|8 |Kevin |8000 |
|5 |Jimy |1900 |
|6 |Alex |1400 |
|7 |Bob |1600 |
|4 |Mike |1500 |
+---+-------+------+
scala> tmp.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|2 |Tony |1000 |
|3 |David |2000 |
|1 |Phoenix|2000 |
|5 |Jimy |1900 |
|4 |Mike |1500 |
+---+-------+------+
scala> kc.deleteRows(tmp.select("id"), "my_kudu_table")
scala> df.show
+---+-------+------+
| id| name|salary|
+---+-------+------+
| 9|Richard| 1800|
| 10|Phoniex| null|
| 8| Kevin| 8000|
| 6| Alex| 1400|
| 7| Bob| 1600|
+---+-------+------+
================ upsertRows ================
scala> df.orderBy("id").show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|6 |Alex |1400 |
|7 |Bob |1600 |
|8 |Kevin |8000 |
|9 |Richard|1800 |
|10 |Phoniex|null |
+---+-------+------+
scala> df.filter("8 < id and id < 10").show
+---+-------+------+
| id| name|salary|
+---+-------+------+
| 9|Richard| 1800|
+---+-------+------+
scala> kc.upsertRows(df.filter("8 < id and id < 10"), "mykudu")
scala> m.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|9 |Richard|1800 |
+---+-------+------+
================ updateRows ================
scala> val t = spark.read.options(Map("kudu.master" -> "centos00:7051", "kudu.table" -> "kudutable")).kudu
t: org.apache.spark.sql.DataFrame = [id: string, name: string ... 1 more field]
scala> t.show(false)
+---+------+------+
|id |name |salary|
+---+------+------+
|1 |Jordon|2500 |
+---+------+------+
scala> val d = sc.makeRDD(Seq(("1", "Lincoln", "3000"))).toDF("id", "name", "salary")
d: org.apache.spark.sql.DataFrame = [id: string, name: string ... 1 more field]
scala> d.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|1 |Lincoln|3000 |
+---+-------+------+
scala> kc.updateRows(d, "kudutable")
scala> t.show(false)
+---+-------+------+
|id |name |salary|
+---+-------+------+
|1 |Lincoln|3000 |
+---+-------+------+
定义kudu的表需要分成5个步骤:
1:提供表名
2:提供schema
3:提供主键
4:定义重要选项;例如:定义分区的schema
5:调用create Table api
import org.apache.kudu.client.CreateTableOptions
import org.apache.kudu.spark.kudu._
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
import collection.JavaConverters._
object CURD {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("AcctfileProcess")
//设置Master_IP并设置spark参数
.setMaster("local")
.set("spark.worker.timeout", "500")
.set("spark.cores.max", "10")
.set("spark.rpc.askTimeout", "600s")
.set("spark.network.timeout", "600s")
.set("spark.task.maxFailures", "1")
.set("spark.speculationfalse", "false")
.set("spark.driver.allowMultipleContexts", "true")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val sparkContext = SparkContext.getOrCreate(sparkConf)
val sqlContext = SparkSession.builder().config(sparkConf).getOrCreate().sqlContext
//使用spark创建kudu表
val kuduContext = new KuduContext("hadoop01:7051,hadoop02:7051,hadoop03:7051", sqlContext.sparkContext)
//TODO 1:定义表名
val kuduTableName = "spark_kudu_tbl"
//TODO 2:定义schema
val schema = StructType(
StructField("CompanyId", StringType, false) ::
StructField("name", StringType, false) ::
StructField("sex", StringType, true) ::
StructField("age", IntegerType, true) :: Nil
)
TODO 3:定义表的主键
val kuduTablePrimaryKey = Seq("CompanyId")
//TODO 4:定义分区的schema
val kuduTableOptions = new CreateTableOptions()
kuduTableOptions.
setRangePartitionColumns(List("name").asJava).
setNumReplicas(3)
//TODO 5:调用create Table api
kuduContext.createTable(
kuduTableName,schema,kuduTablePrimaryKey, kuduTableOptions)
}
}
object DropTable {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("AcctfileProcess")
//设置Master_IP并设置spark参数
.setMaster("local")
.set("spark.worker.timeout", "500")
.set("spark.cores.max", "10")
.set("spark.rpc.askTimeout", "600s")
.set("spark.network.timeout", "600s")
.set("spark.task.maxFailures", "1")
.set("spark.speculationfalse", "false")
.set("spark.driver.allowMultipleContexts", "true")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
val sparkContext = SparkContext.getOrCreate(sparkConf)
val sqlContext = SparkSession.builder().config(sparkConf).getOrCreate().sqlContext
//使用spark创建kudu表
val kuduContext = new KuduContext("hadoop01:7051,hadoop02:7051,hadoop03:7051", sqlContext.sparkContext)
// TODO 指定要删除的表名称
var kuduTableName = "spark_kudu_tbl"
// TODO 检查表如果存在,那么删除表
if (kuduContext.tableExists(kuduTableName)) {
kuduContext.deleteTable(kuduTableName)
}
}
}
建表语句:
CREATE TABLE pk_inline
(
col1 BIGINT PRIMARY KEY,
col2 STRING,
col3 BOOLEAN
) PARTITION BY HASH(col1) PARTITIONS 2 STORED AS KUDU
TBLPROPERTIES ( 'kudu.num_tablet_replicas' = '1');
CREATE TABLE kaka_first
(
id BIGINT,
name STRING
)
DISTRIBUTE BY HASH INTO 16 BUCKETS
TBLPROPERTIES(
'storage_handler' = 'com.cloudera.kudu.hive.KuduStorageHandler',
'kudu.table_name' = 'kaka_first',
'kudu.master_addresses' = '10.10.245.129:7051',
'kudu.key_columns' = 'id'
);
建表语句中,默认第一个就是Primary Key,是个not null列,在后面的kudu.key_columns
中列出,这边至少写一个。
com.cloudera.kudu.hive.KuduStorageHandler
插入数据
INSERT INTO kaka_first VALUES (1, "john"), (2, "jane"), (3, "jim");
Impala默认一次同时最多插入1024条记录,作为一个batch
更新数据
UPDATE kaka_first SET name="bob" where id = 3;
删除数据
DELETE FROM kaka_first WHERE id < 3;
修改表属性
ALTER TABLE kaka_first RENAME TO employee;
//重命名
ALTER TABLE employee
SET TBLPROPERTIES('kudu.master_addresses' = '10.10.245.135:7051');
//更改kudu master address
ALTER TABLE employee SET TBLPROPERTIES('EXTERNAL' = 'TRUE');
//将内部表变为外部表
kudu的表具有类似于传统RDBMS中的表的数据结构。schema设计对于实现Kudu的最佳性能和操作稳定性至关重要。业务场景的多变,对于table来说并不存在一种最好的schema设计。大部分情况下,创建kudu的表需要考虑三个问题:
比较好的Schema设计应该满足一下要求:
从kudu的官方文档中可以看到spark 如何集成kudu的。从文档中的demo可以看到,kudu表的创建只能调用NOSQL API 来实现,无法通过spark sql直接创建一张kudu表。spark sql查询kudu表也是先注册一张临时表后,才能用sql 语句查询的。
那么有没有方法在spark sql上直接提交一个Create DDL语句来创建一张关联kudu的表呢?
答案是:可以,通过 spark sql的USING OPTIONS语法实现,该语法是spark sql用来创建外部数据源的表的,我们可以通过该语法创建kudu数据源的表。
假设我们已经通过api 创建了一张kudu表kudu_test,impala建kudu表
impala > CREATE TABLE kudu_test(id1 int, id2 int, id3 string,PRIMARY KEY (id1))
PARTITION BY HASH PARTITIONS 10
STORED AS KUDU ;
接下来我们要通过spark sql 去创建一张关联表,spark_kudu_test。
spark.sql("""
CREATE TABLE spark_kudu_test(id1 int, id2 int, id3 string)
USING org.apache.kudu.spark.kudu
OPTIONS("kudu.master" "node1:7051,node2:7051,node3:7051","kudu.table" "kudu_test");
""")
这样我们就能够通过spark sql去操作kudu的数据了。
从MySql导出数据到本地txt
select * from DAYCACHETBL into outfile '/tmp/DAYCACHETBL.txt'
fields terminated by '\t'
lines terminated by '\n';
保存到hdfs中/data
目录下
hdfs dfs -mkdir /data
hdfs dfs -put /tmp/DAYCACHETBL.txt /data
在hive shell中创建hive表
create table DAYCACHETBL (
METERID string,
SOURCEID int,
VB double,
DELTA double,
DTIME string,
UPGUID string,
UPBATCH string,
level string,
YEAR string,
MONTH string,
QUARTER string,
WEEK string,
D_DELTA double
)
ROW FORMAT DELIMITED
fields terminated by '\t'
lines terminated by '\n'
stored as textfile
location '/data';
在impala-shell下创建kudu表
create table DAYCACHETBL2 (
METERID string,
SOURCEID int,
VB double,
DELTA double,
DTIME string,
UPGUID string,
UPBATCH string,
level string,
YEAR string,
MONTH string,
QUARTER string,
WEEK string,
D_DELTA double
)
DISTRIBUTE BY HASH INTO 16 BUCKETS
TBLPROPERTIES(
'storage_handler' = 'com.cloudera.kudu.hive.KuduStorageHandler',
'kudu.table_name' = 'DAYCACHETBL2',
'kudu.master_addresses' = 'kudu1:7051,kudu2:7051,kudu3:7051',
'kudu.key_columns' = 'METERID'
);
将hive表中的内容插入kudu表
insert into DAYCACHETBL2 select * from DAYCACHETBL;