大数据开发之Spark篇----Spark和HBase的结合(1)

Spark处理,存储到HBase

版本
Scala 2.11.8
Spark 2.4.0
HBase 1.2.0-cdh5.7.0
mysql 5.1.27
maven依赖
org.apache.spark:spark-core_2.11:${spark.version}
org.apache.hbase:hbase-client:${hbase.version}
org.apache.hbase:hbase-server:${hbase.version}

今天首先要介绍的是最最最为传统的一种写入方式:Put方式
这里我写贴上代码在进行解释

ackage com.doudou.www.etl

import java.util.zip.CRC32

import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{ConnectionFactory, Put}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
import org.apache.log4j.{Level, Logger}

/**
  * 通过RDD将数据写入到HBase,这里使用put的方法
  *
  * 工作分为三个部分:
  * -a 读取数据进行etl过程,并转换格式为(ImmutableBytesWritable,Put)这种格式
  * -b 配置HBase上的信息以及创建要写入的表格
  * -c 使用saveAsNewAPIHadoopFile这个API来写入数据,写完后可以手动flush
  *
  */
object ProcessData {

  val logger:Logger = Logger.getLogger(ProcessData.getClass)

  def main(args: Array[String]): Unit = {

    Logger.getRootLogger.setLevel(Level.WARN)

    /**
      * 构建spark的入口点,使用spark来读取数据
      */
    val conf = new SparkConf().setAppName("etl").setMaster("local[3]").set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    conf.registerKryoClasses(Array(classOf[ImmutableBytesWritable]))
    val sc = new SparkContext(conf)

    /**
      * 使用textFile来读取原始数据,使用case class转换RDD中每个元素的数据类型
      */
    val InfoRdd = sc.textFile(ETLConstantUtils.DATA_DIR,2)
    val personRDD = InfoRdd.mapPartitions(iter => {
      iter.map(x => {
        val info = x.split(",")
        info.size
        Person(info(0).toInt,info(1),info(2).toInt,info(3),info(4).toInt,info(5).toInt)
      })
    })

    /**
      * 将数据类型转换为:
      * (ImmutableBytesWritable,put)的类型
      * 每一个put对应一条信息,有rowkey和一种的column组成,并统一使用一个列簇
      * 重点是怎么将一条记录计算出一个rowkey来
      */
    val putRDD = personRDD.mapPartitions(iter => {
      iter.map(person => {
        val rowKey = createRowKey(person)
        val put = new Put(Bytes.toBytes(rowKey))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("id"),Bytes.toBytes(person.id.toString))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("name"),Bytes.toBytes(person.name))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("age"),Bytes.toBytes(person.age.toString))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("gender"),Bytes.toBytes(person.gender))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("sameLevel"),Bytes.toBytes(person.sameLevel.toString))
        put.addColumn(Bytes.toBytes(ETLConstantUtils.HCOLUMNS),Bytes.toBytes("isWorked"),Bytes.toBytes(person.isWorked.toString))
        (new ImmutableBytesWritable(put.getRow),put)
      })
    })

    /**
      * 设置HBaseConfiguration
      */
    //    println(putRDD.take(1))
    val hconf = HBaseConfiguration.create()
    hconf.set("hbase.zookeeper.quorum","doudou")
    hconf.set("hbase.zookeeper.property.clientPort","2181")
    hconf.set("hbase.rootdir","hdfs://doudou:8020/hbase")
    hconf.set(TableOutputFormat.OUTPUT_TABLE,ETLConstantUtils.HBASE_TABLE)

    /**
      * 创建出对HBae的connection和admin
      */
    val con = ConnectionFactory.createConnection(hconf)
    val admin = con.getAdmin

    /**
      * 要写入某张表里面,需要先创建
      */
    if(admin.tableExists(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))){
      admin.disableTable(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      admin.deleteTable(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val htd = new HTableDescriptor(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val hcd = new HColumnDescriptor("info")
      htd.addFamily(hcd)
      admin.createTable(htd)
    }else{
      val htd = new HTableDescriptor(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val hcd = new HColumnDescriptor(ETLConstantUtils.HCOLUMNS)
      htd.addFamily(hcd)
      admin.createTable(htd)
    }


    /**
      * 使用spark的saveAsNewAPIHadoopFile这个API来将数据写入
      * 需要定义一个临时的HDFS的文件目录,使用这种方法的时候,这个目录没什么用,当使用HFile是,这个就是保存文件的目录了
      * 需要定义key的类型:classOf[ImmutableBytesWritable]
      * 需要定义value的类型:classOf[Put]
      * 定义表的写入类型:classOf[TableOutputFormat[ImmutableBytesWritable]\]
      * 以及HBAse的配置信息
      */
    putRDD.saveAsNewAPIHadoopFile("/hbase/data/default/tmp"
      ,classOf[ImmutableBytesWritable],classOf[Put],classOf[TableOutputFormat[ImmutableBytesWritable]],hconf)

    /**
      * 手动将写缓存的数据flush到HDFS磁盘上
      */
    admin.flush(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))

    admin.close()
    con.close()
    sc.stop()
  }

  def createRowKey(person:Person):String = {

    val sBuilder = new StringBuilder()
    sBuilder.append(person.id)
    val crc32 = new CRC32()
    crc32.reset()
    if(person.name.length > 0){
      crc32.update(Bytes.toBytes(person.name))
    }
    if(person.age != 0){
      crc32.update(Bytes.toBytes(person.age.toString))
    }
    if(person.gender.length > 0){
      crc32.update(Bytes.toBytes(person.gender))
    }
    sBuilder.append(crc32.getValue)
    sBuilder.toString()
  }

}

这里我将刚读取到的rdd要String类型转换成了Person类型,而这个Person类型是一个case class,读者在复现的时候可以自行定义。

首先我们的数据是一张和人信息有关的数据表,里面有的字段如下:

case class Person(id:Int,name:String,age:Int,gender:String,sameLevel:Int,isWorked:Int)

一开始,我们先创建一个Spark的入口点SparkContext,然后使用textFile这个api将数据读取进来。
接着,我们将数据进行类型的转换,转成了Person类型,这样做只是方便我后面的操作,在生产当中,读者可以根据自己的需求来决定是否要这样操作。
第三步,这也是重点的一步。我们要将每一条记录(就是RDD中的每一个元素)的类型转换成(ImmutableBytesWritable,Put)这样的类型,而Put需要我们定义好这条记录的rowKey和相关的信息。这代码的末端我定义了生成rowKey的方法,我们使用时间戳加CRC32编码的的字段来构建rowKey,这样我们就可以根据时间前缀来定义我们在HBase上的表的预分区了。(具体的操作请看代码)
然后,我们使用saveAsNewAPIHadoopFile这个API,将我们的数据写入到HBase上面了。了解Spark写入HDFS的过程的同学都知道,写入到HDFS上的文件需要定义输出格式和key以及我们的value,现在这里的输出格式,key,value为:TableOutputFormat,ImmutableBytesWritable,Put。(具体每个API的操作和传入的参数,读者可以自行了解)

上面的代码中,我是手动地将数据从写缓存中flush到磁盘文件上的,读者可以自行决定是否使用这种方法,这种方法可以减轻HBase的内存压力,同时提高HBase的可靠性。

这里在给出两个优化点:
1)就是我们可以设置put不走WAL,这样就可以减轻集群的文件的压力,同时加速了etl的过程,当然这样也面临一定的风险。所以我们一般是在etl过程中使用这个机制而已,业务端可能不实用。
2)将rowkey定义为时间戳作为前缀使得我们可以根据一天的业务数据量来定义HBase表的预分区,当然了这种架构一般是etl作业一天只跑一次的时候使用的,我司现在已经放弃了这种架构了,因为要写这篇博文的内容我才这么写的。
3)写入前可以对RDD进行一次coalesce(1)以降低输出的文件数,这也是减缓小文件的一种途径吧,小文件的处理我将在后面的博客中与大家分享。

发布了118 篇原创文章 · 获赞 16 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_39702831/article/details/89161478