Kudu学习笔记——介绍、接口和使用

介绍

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
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值