介绍
Kudu是基于Hadoop平台的列式存储系统。
Kudu官方文档
Kudu使用场景
- 适用于既有随机访问,也有批量数据扫描的复合场景
- 适用于高计算量的场景
- 充分利用高性能存储设备
- 支持数据更新,避免数据反复迁移
- 支持跨地域的实时数据备份和查询
Kudu的优势
- 支持update和upsert操作
- 结构化数据模型
- 与imapla或spark集成后,可通过sql操作,使用方便
- 一个table由多个tablet组成,对分区查看、扩容和数据高可用支持非常好
Kudu和HBase、Hive对比
- 在字段较多时,HBase保存一行数据存储会膨胀很多倍,在大数据量下,HBase对存储的消耗是难以接受的,而Kudu独特的列编码和列压缩会极大节省存储空间
- 在批量读取和批量写入速度上(宽表,字段90+),Kudu都比HBase要快很多。有博客说HBase的写性能要优于Kudu,但根据我的测试,写入一小时数据,HBase使用bulk load写空表都需要4分钟,而Kudu不超过2分钟(可能集群性能上有差异,但应该不是主要因素)。HBase读HFile的方式读取一小时数据需要10分钟,而Kudu读一天的数据10分钟
- 相比Hive,Kudu支持动态增删列和数据更新,随机读取效率也高(但不如HBase),适合有更新需求,或回溯补全历史数据的情况
- Kudu实质上是HBase和Hive的折中,兼顾了批量读写与随机读写的效率
性能测试
- 天级别读:源数据2.8亿,541.7 G,耗时10min,平均46.6w条/s
- 小时级别写:源数据2000W行,38G,耗时1.4min,平均23w条/s
- 天级别更新列:源数据4.74亿行,10个新列,耗时17min,平均46.5w条/s (其中包括读800G的text格式hive表的时间,真实插入时间更少)
Spark操作Kudu接口
注意事项
- kudu引擎暂不支持sparkSQL中 <、>、or 这些谓词下推,支持like,但仅限于“201907%”这种形式,不支持“201907%02”这种形式
- 使用SparkSQL读取Kudu表时,必须使用between或者in来指定range分区字段的范围,否则会变成全表扫描,效率极低!!!
- 插入数据、更新数据和删除行的df必须包含所有主键,主键不可为空
- 删除kudu分区会同时删除分区内的数据
Spark-Kudu接口
目前官网提供的kuduContext接口极为简单,无法满足需求,因此整合了一个功能更全面的接口。
GitHub地址:
https://github.com/FeichenYe/KuduHandle
以下仅给出创建kudu表、给kudu表增加列、给kudu表加range分区、删除kudu表range分区的例子,全部的api详见git。
创建带编码格式和列压缩的kudu表:
/**
*新建kudu表。这里range分区字段强制为一个字段
* @param kuduTableName 新建kudu表名
* @param rangeKeyColumn range分区字段 (kudu表的range分区字段可以由多个主键字段组成,但这里强制指定唯一range字段)
* @param hashKeyColumns hash分区字段,由多个主键组成,逗号分隔
* @param simpleBaseColumns kudu表非主键字段,逗号分隔
*/
def createTable(kuduTableName: String,
rangeKeyColumn: String,
hashKeyColumns: String,
simpleBaseColumns: String): Unit ={
//TODO:定义列schema
val kuduCols = new util.ArrayList[ColumnSchema]()
val rangeCol = new ColumnSchema.ColumnSchemaBuilder(rangeKeyColumn, Type.STRING).key(true).nullable(false)
.encoding(Encoding.DICT_ENCODING).compressionAlgorithm(CompressionAlgorithm.LZ4)
kuduCols.add(rangeCol.build())
for(colName <- hashKeyColumns.split(",")){
if(colName.equalsIgnoreCase("key1") || colName.equalsIgnoreCase("key2")){
val col = new ColumnSchema.ColumnSchemaBuilder(colName, Type.STRING).key(true).nullable(false)
.encoding(Encoding.DICT_ENCODING).compressionAlgorithm(CompressionAlgorithm.LZ4)
kuduCols.add(col.build())
}else{
val col = new ColumnSchema.ColumnSchemaBuilder(colName, Type.STRING).key(true).nullable(false).
encoding(Encoding.PLAIN_ENCODING).compressionAlgorithm(CompressionAlgorithm.LZ4)
kuduCols.add(col.build())
}
}
for(colName <- simpleBaseColumns.split(",")){
val col = new ColumnSchema.ColumnSchemaBuilder(colName,Type.STRING).key(false).nullable(true)
.encoding(Encoding.PLAIN_ENCODING).compressionAlgorithm(CompressionAlgorithm.LZ4)
kuduCols.add(col.build())
}
val schema = new Schema(kuduCols)
//TODO:定义分区schema
val hashKeyArr = hashKeyColumns.split(",")
var hashKeyList = List(hashKeyArr(0))
for(i <- 1 until hashKeyArr.length){
hashKeyList = hashKeyList ++ List(hashKeyArr(i))
}
val hashKey = hashKeyList.asJava
val lower = schema.newPartialRow()
lower.addString(rangeKeyColumn,"2018010100")
val upper = schema.newPartialRow()
upper.addString(rangeKeyColumn,"2019010100")
val kuduTableOptions = new CreateTableOptions()
kuduTableOptions
.addHashPartitions(hashKey, 10)
.setRangePartitionColumns(List(rangeKeyColumn).asJava)
.addRangePartition(lower,upper)
.setNumReplicas(3)
//TODO:调用create Table api
kuduContext.createTable(kuduTableName, schema, kuduTableOptions)
}
给kudu表加列:
/**
* 给指定kudu表添加列,columnsString 字段可以包含多个列名,用逗号隔开 (此处添加的列皆为非主键列)
* @param kuduTableName 目标kudu表
* @param columnsString 待添加的列,可包含多个列名,用逗号隔开
*/
def addColumns(kuduTableName: String, columnsString: String): Unit ={
val ato = new AlterTableOptions()
for(colName <- columnsString.split(",")){
val col = new ColumnSchema.ColumnSchemaBuilder(colName,Type.STRING).key(false).nullable(true)
.encoding(Encoding.PLAIN_ENCODING).compressionAlgorithm(CompressionAlgorithm.SNAPPY)
ato.addColumn(col.build())
}
kuduClient.alterTable(kuduTableName, ato)
}
给kudu表加分区:
/**
* 添加range分区,分区的范围不能相互覆盖
* @param kuduTableName 目标kudu表
* @param rangeKey range分区字段
* @param lower 分区的下限
* @param upper 分区的上限
*/
def addRangePartition(kuduTableName: String, rangeKey: String, lower: String, upper: String): Unit ={
val table = kuduClient.openTable(kuduTableName)
val schema = table.getSchema
val lowerBound= schema.newPartialRow()
lowerBound.addString(rangeKey,lower)
val upperBound = schema.newPartialRow()
upperBound.addString(rangeKey,upper)
val ato = new AlterTableOptions
//add range partition
ato.addRangePartition(lowerBound,upperBound)
kuduClient.alterTable(kuduTableName,ato)
}
删除kudu表分区:
/**
* 删除kudu表range分区,会同时删除分区内的数据。慎用!!!
* @param kuduTableName 目标kudu表
* @param rangeKey range分区字段
* @param lower 分区的下限
* @param upper 分区的上限
*/
def dropRangePartition(kuduTableName: String, rangeKey: String, lower: String, upper: String): Unit ={
val table = kuduClient.openTable(kuduTableName)
val schema = table.getSchema
val lowerBound= schema.newPartialRow()
lowerBound.addString(rangeKey,lower)
val upperBound = schema.newPartialRow()
upperBound.addString(rangeKey,upper)
val ato = new AlterTableOptions
//drop range partition
ato.dropRangePartition(lowerBound,upperBound)
kuduClient.alterTable(kuduTableName,ato)
}
小知识
- create Kudu表时指定replicas数量为1,此时只有1份数据,而不是真实数据+1份副本=2份数据,因此建议生产环境下,replicas至少设置为3 (Kudu规定副本数必须为奇数)
- Kudu没有TTL功能,得自己写脚本定时删分区
- Kudu数据的存储暂时只支持本地文件系统,不支持s3