(Scala)sparkstreaming手动提交offset到zookeeper中,kafka版本0.10

kafka因为版本的不同可能会导致一下接口的差异还有功能等的区别,我用的是0.10版本的kafka,0.8版本的kafka将topic及其分区等元数据是默认保存在zookeeper中,新版本的kafka有一个自带的topic__consumer_offset以存储offset代替zookeeper的相应功能,即:offset会存到kafka中,我们处理kafka中的数据时,kafka有个参数enable.commit.auto,默认为true即kafka会根据默认的时间(好像是5秒)刷新一次offset值,即5秒自动提交一次。这种方式会因为程序等因无法抗拒的因素挂掉导致数据出现问题。一般都选择手动提交,在数据处理逻辑后提交更新后的offset。
可以直接将offset提交到kafka中,此种方式代码简单易实现,
还可以将offset存到zookeeper中,此种方式代码量相对较多。
还可以将offset存到外部数据库中,比如mysql、sqlserver等数据库。
除了这几种方式还可以使用checkpoint,设置检查点的方式去管理offset值。
本次我们就使用zookeeper管理offset值。有相对代码量较少的kafka管理offset,感兴趣的朋友可以去我博客里面看。
废话不多说,上我的代码。

package zookeeper_offset

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
  * spark-streaming读取kafka
  * 适用本版:spark-streaming-kafka-0-10
  * (0.10和0.8的API有较大的区别)
  */
object DirectKafkaManagerMeterData {

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

    val conf = new SparkConf().setMaster("local[4]").setAppName("DirectKafkaMeterData")
    val ssc = new StreamingContext(conf, Seconds(5))//流数据分批处理时间间隔
    //kafka节点
    val BROKER_LIST = "10.204.118.101:9092,10.204.118.102:9092,10.204.118.103:9092"
    val BOOTSTRAP_SERVER = "10.204.118.101:9092,10.204.118.102:9092,10.204.118.103:9092"
    val ZK_SERVERS = "10.204.118.101:2181,10.204.118.102:2181,10.204.118.103:2181"
    val GROUP_ID = "test_consumer" //消费者组
    val topics = Array("kafka010_test") //待消费topic
    val kafkaParams = Map[String,Object](
      "bootstrap.servers" -> BOOTSTRAP_SERVER,
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> GROUP_ID,
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> "false"
    )

    //采用zookeeper手动维护偏移量
    val zkManager = new KafkaOffsetZKManager(ZK_SERVERS)
    val fromOffsets = zkManager.getFromOffset(topics,GROUP_ID)
    //创建数据流
    var stream:InputDStream[ConsumerRecord[String, String]] = null
    if (fromOffsets.size > 0){
      stream = KafkaUtils.createDirectStream[String, String](
        ssc,
        PreferConsistent,
        Subscribe[String, String](topics, kafkaParams, fromOffsets)
      )
    }else{
      stream = KafkaUtils.createDirectStream[String, String](
        ssc,
        PreferConsistent,
        Subscribe[String, String](topics, kafkaParams)
      )
      println("第一次消费 Topic:" + topics)
    }

    //处理流数据
    stream.foreachRDD { rdd =>
      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      val rs = rdd.map(record => (record.offset(), record.partition(), record.value())).collect()
      for(item <- rs) println(item)
      rdd.foreachPartition(partitions =>{
        partitions.foreach(line =>{
          //处理数据逻辑
          //等等逻辑
        })
      })
      // 处理数据存储到HDFS或Hbase等
      // 存储代码(略)
      // 处理完数据保存/更新偏移量
      zkManager.storeOffsets(offsetRanges,GROUP_ID)
    }

    ssc.start()
    ssc.awaitTermination()
  }

}

然后另创一个类用来维护zookeeper中的offset

package zookeeper_offset

import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange

/**
  * kafka偏移量zookeeper维护类
  * 适用本版:spark-streaming-kafka-0-10
  *
  * @param zkServers zookeeper server
  */
class KafkaOffsetZKManager(zkServers : String) {

  //创建zookeeper连接客户端
  val zkClient = {
    val client = CuratorFrameworkFactory
      .builder
      .connectString(zkServers)
      .retryPolicy(new ExponentialBackoffRetry(1000, 3))
      //                .namespace("kafka")//创建包含隔离命名空间的会话
      .build()
    client.start()
    client
  }
  val _base_path_of_kafka_offset = "/kafka/offsets" //offset 路径起始位置
  /**
    * 获取消费者组topic已消费偏移量(即本次起始偏移量)
    * @param topics topic集合
    * @param groupName 消费者组
    * @return
    */
  def getFromOffset(topics: Array[String], groupName:String):Map[TopicPartition, Long] = {
    // Kafka 0.8和0.10的版本差别:0.10->TopicPartition ,0.8->TopicAndPartition
    var fromOffset: Map[TopicPartition, Long] = Map()
    for(topic <- topics){
      val topic = topics(0).toString
      println(topic+"'''''''''''''''''''''''''''''''''''")
      // 读取ZK中保存的Offset,作为Dstrem的起始位置。如果没有则创建该路径,并从 0 开始Dstream
      val zkTopicPath = s"${_base_path_of_kafka_offset}/${groupName}/${topic}"
      // 检查路径是否存在
      checkZKPathExists(zkTopicPath)
      // 获取topic的子节点,即 分区
      val childrens = zkClient.getChildren().forPath(zkTopicPath)
      // 遍历分区
      import scala.collection.JavaConversions._
      for (p <- childrens){
        // 遍历读取子节点中的数据:即 offset
        val offsetData = zkClient.getData().forPath(s"${zkTopicPath}/${p}")
        println(offsetData.toString+"----------------------------------")
        // 将offset转为Long
        val offset = java.lang.Long.valueOf(new String(offsetData)).toLong
        fromOffset += (new TopicPartition(topic, Integer.parseInt(p)) -> offset)
      }
    }
    println(fromOffset+"+++++++++++++++++++++++++++++++++++++")
    fromOffset
  }

  /**
    * 检查ZK中路径存在,不存在则创建该路径
    * @param path
    * @return
    */
  def checkZKPathExists(path: String)={
    if (zkClient.checkExists().forPath(path) == null) {
      zkClient.create().creatingParentsIfNeeded().forPath(path)
    }
  }

  /**
    * 保存或更新偏移量
    * @param offsetRange
    * @param groupName
    */
  def storeOffsets(offsetRange: Array[OffsetRange], groupName:String) = {
    for (o <- offsetRange){
      val zkPath = s"${_base_path_of_kafka_offset}/${groupName}/${o.topic}/${o.partition}"
      // 检查路径是否存在
      checkZKPathExists(zkPath)
      // 向对应分区第一次写入或者更新Offset 信息
      println("---Offset写入ZK------\nTopic:" + o.topic +", Partition:" + o.partition + ", Offset:" + o.untilOffset)
      zkClient.setData().forPath(zkPath, o.untilOffset.toString.getBytes())
    }
  }
}

在启动程序处理数据的时候,会先从zookeeper得到对应的offset值
(Scala)sparkstreaming手动提交offset到zookeeper中,kafka版本0.10_第1张图片
(Scala)sparkstreaming手动提交offset到zookeeper中,kafka版本0.10_第2张图片
本人也是刚入坑的大白,大佬们不喜勿喷,有要一块学习的大佬们可以留下联系方式咱们一块努力,有问题可以互相探讨探讨。。。

你可能感兴趣的:(sparkstreaming)