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)以降低输出的文件数,这也是减缓小文件的一种途径吧,小文件的处理我将在后面的博客中与大家分享。