大数据课程 滴滴订单数据 MYSQL 转KAFKA 实时存储HBASE

采用如下架构将数据实时同步到hbase中:

后续场景为KAFKA 和 SPARK STREAM 以及 HBASE的交互在这里插入图片描述
针对 代码 的整体 浏览 。

首先 是 SPARK ENGINE 的抽象 类。

object SparkEngine {
  def getSparkConf():SparkConf = {

    val sparkConf: SparkConf = new SparkConf()
      .set("spark.worker.timeout" , GlobalConfigUtils.getProp("spark.worker.timeout"))
      .set("spark.cores.max" , GlobalConfigUtils.getProp("spark.cores.max"))
      .set("spark.rpc.askTimeout" , GlobalConfigUtils.getProp("spark.rpc.askTimeout"))
      .set("spark.task.maxFailures" , GlobalConfigUtils.getProp("spark.task.maxFailures"))
      .set("spark.driver.allowMutilpleContext" , GlobalConfigUtils.getProp("spark.driver.allowMutilpleContext"))
      .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .set("spark.sql.adaptive.enabled" , "true")
      .set("spark.streaming.kafka.maxRatePerPartition" , GlobalConfigUtils.getProp("spark.streaming.kafka.maxRatePerPartition"))
      .set("spark.streaming.backpressure.enabled" , GlobalConfigUtils.getProp("spark.streaming.backpressure.enabled"))
      .set("spark.streaming.backpressure.initialRate" , GlobalConfigUtils.getProp("spark.streaming.backpressure.initialRate"))
      .set("spark.streaming.backpressure.pid.minRate","10")
      .set("enableSendEmailOnTaskFail", "true")
      .set("spark.buffer.pageSize" , "16m")
//      .set("spark.streaming.concurrentJobs" , "5")
      .set("spark.driver.host", "localhost")
      .setMaster("local[*]")
      .setAppName("query")
    sparkConf.set("spark.speculation", "true")
    sparkConf.set("spark.speculation.interval", "300s")
    sparkConf.set("spark.speculation.quantile","0.9")
    sparkConf.set("spark.streaming.backpressure.initialRate" , "500")
    sparkConf.set("spark.streaming.backpressure.enabled" , "true")
    sparkConf.set("spark.streaming.kafka.maxRatePerPartition" , "5000")
<<<<<<<<<<<<<<<<<<<<cutting>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    sparkConf
  }

在 StreamApp 里面, 开始 正式 的 执行开始。 、、
对 KAFKA MANAGER 对象 的创建。

KAFKA MANAGER 的 设计 。

第一步, 定义 kafka manager , KafkaManager.scala

class KafkaManager(zkHost:String , kafkaParams:Map[String , Object]) extends Serializable

在 KAFKAMANAGER的构造函数里面 ,
创建 Kafka 客户端,初始化 ZKUTILS

val (zkClient,zkConnection) = ZkUtils.createZkClientAndConnection(zkHost , 10000 , 10000)
val zkUtils = new ZkUtils(zkClient,zkConnection , false)

随后定义 KAFKA MANAGER 创建 DirectStream的函数。

 def createDirectStream[K:ClassTag , V:ClassTag](ssc:StreamingContext , topics:Seq[String]):InputDStream[ConsumerRecord[K, V]]

里面包含 2个 动作, ReadOffset 和利用KafkaUtil 创建 stream.

读取 OFFSET 函数,针对 具体TOPIC。

def createDirectStream[K:ClassTag , V:ClassTag](ssc:StreamingContext , topics:Seq[String]):
  InputDStream[ConsumerRecord[K, V]]readoffset(topics, groudId).

其 内部函数, 进行 OFFSET的 自我 管理。
最根本的技术来源如下 博客
https://blog.cloudera.com/offset-management-for-apache-kafka-with-apache-spark-streaming/

private def readOffset(topics:Seq[String] , groupId:String):Map[TopicPartition , Long]{
//参考 如下 博客地址 https://www.cnblogs.com/small-k/p/8909942.html

//1. 首先 得到 TOPIC 分区信息。调用 ZKUTILS 包 的 API
     val topicAndPartitionMaps:
 mutable.Map[String,Seq[Int]]=  zkUtils.getPartitionsForTopics(topics)

//2 .针对 拿到的某个topic 对应的 分区 进行 循环处理。
topicAndPartitionMaps.foreach(topicPartitions =>{
// 2.1 获取 TOPIC 的目录
val zkGroupTopicsDirs: ZKGroupTopicDirs = new ZKGroupTopicDirs(groupId , topicPartitions._1)
// 对 某具体 TOPIC 的 分区对应的OFFSET 进行迭代
topicPartitions._2.foreach(partition =>{
// 获取到 TOPIC对应的每个分区 的 OFFSET 路径
val offsetPath = s"${zkGroupTopicsDirs.consumerOffsetDir}/${partition}"

//进行 读取 具体 路径数据的尝试,并将 读取到的数据 保存进 MAP 结构的topicPartitionMap里面
val  tryGetTopicPartition = Try{
          //String --->offset
          val offsetTuples: (String, Stat) = zkUtils.readData(offsetPath)
          if(offsetTuples != null){
            topicPartitionMap.put(new TopicPartition(topicPartitions._1 , Integer.valueOf(partition)) , offsetTuples._1.toLong)
          }
        }
// 若 读取 失败 , 用CONSUMER 去拉取 OFFSET的最初开始的地方。
if(tryGetTopicPartition.isFailure){
          val consumer = new KafkaConsumer[String , Object](kafkaParams)
          val topicCollection = List(new TopicPartition(topicPartitions._1 , partition))
          consumer.assign(topicCollection)

          val avaliableOffset: Long = consumer.beginningOffsets(topicCollection).values().head

          consumer.close()
          topicPartitionMap.put(new TopicPartition(topicPartitions._1 , Integer.valueOf(partition)) , avaliableOffset)

        }
// 有时候 因为各种原因,比如 某些消费 几天 出现 异常 没有 消费,但是没有注意,会造成 ZK里面的OFFSET和 实际 KAFKA里面 当前保存的OFFSET,因为 BROKER会定期更新 OFFSET,导致ZK OFFSET 过于太旧或者过于太新。
    val earliestOffsets = getEarliestOffsets(kafkaParams , topics)
    val latestOffsets = getLatestOffsets(kafkaParams , topics)
    for((k,v) <- topicPartitionMap){
      val current = v
      val earliest = earliestOffsets.get(k).get
      val latest = latestOffsets.get(k).get
      if(current < earliest || current > latest){
        topicPartitionMap.put(k , earliest)
      }
}

最终 KAFKA MANAGER 利用 ZK 对 OFFSET的管理, 然后

调用 createDirectStream 的ZKUTIL API,对外提供TOPIC PARTITIONS的整体管理。

KafkaUtil 创建 stream

val stream: InputDStream[ConsumerRecord[K, V]] = KafkaUtils.createDirectStream[K, V](
      ssc,
      PreferConsistent,
      ConsumerStrategies.Subscribe[K, V](topics, kafkaParams, **topicPartition**)
    )
  // 针对 整个函数 def createDirectStream[K:ClassTag , V:ClassTag]的返回值为
   stream 
   )

后续 开始 对于 整体 数据 利用 KAFKA MANAGER 进行 迭代 处理 和保存。

val inputDStream: InputDStream[ConsumerRecord[String, String]] = kafkaManager.createDirectStream(ssc, topics)


后面 是 利用 读取 到 的 具体 TOPIC 的 PARTITION 以及 OFFSET 开始进行 迭代 的FOREACH 处理。

inputDStream.foreachRDD(rdd =>{
      if(!rdd.isEmpty()){
// 利用offsetRanges 将 和 KAFKA MANAGER 创建的 DirectStream对应的CONSUMER [K,V] 绑定起来进行迭代 处理
        val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        ranges.foreach(line =>{
          println(s" 当前读取的topic:${line.topic} , partition:${line.partition} , fromOffset:${line.fromOffset} , untilOffset: ${line.untilOffset}")
        })
// 开始 对 DSTREAM  所代表 的 PARTITION 迭代 属性 进行 统计,并对 HBASE进行 实际存储操作。
val doElse = Try{
          val data: RDD[String] = rdd.map(line => line.value())
          data.foreachPartition(partition =>{
            //构建连接
            val conn = HbaseConnections.getHbaseConn
            })
            //写业务,这里 有个 内部 定义好的 STRUCTINTERPRETER的操作,后续介绍这个
            partition.foreach(d =>{
              val parse: (String  , Any) = JsonParse.parse(d)
              StructInterpreter.interpreter(parse._1 , parse , conn)
            })

            //注销连接
            HbaseConnections.closeConn(conn)
    
    // 这里是最终 提交 到 ZK 进行 保存 的地方
           if(doElse.isSuccess){
          //提交偏移量
          kafkaManager.persistOffset(rdd)

对于 HBASE 创建 表 ,一定要注意 创建 的时候 要自定义 Region来 Split, 否则容易造成 写 数据 造成 某些 Region 过于热点。Column 的列簇 名称 一定要简介, 不要太长,太长容易 浪费 内存。

关注重点 ,
createTable

public void createTable(HTableDescriptor desc,
byte[][] splitKeys)
throws IOException

Creates a new table with an initial set of empty regions defined by the specified split keys. The total number of regions created will be the number of split keys plus one. Synchronous operation. Note : Avoid passing empty split key.

构建 一个 SplitKeysBuilder 类 来 构造 统一 的前缀。

KAFKA 简介
https://www.cnblogs.com/zhaiyf/p/8651457.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值