Spark Streaming 中使用kafka低级api+zookeeper 保存 offset 并重用 以及 相关代码整合

来源:互联网 发布:英国博士 知乎 编辑:程序博客网 时间:2024/05/23 19:11

转载自

http://www.klion26.com/spark-streaming-save-offset-to-zookeeper.html

http://www.klion26.com/spark-streaming-saving-offset-in-zookeeper-2.html


在 Spark Streaming 中消费 Kafka 数据的时候,有两种方式分别是 1)基于 Receiver-based 的 createStream 方法和 2)Direct Approach (No Receivers) 方式的 createDirectStream 方法,详细的可以参考 Spark Streaming + Kafka Integration Guide,但是第二种使用方式中  kafka 的 offset 是保存在 checkpoint 中的,如果程序重启的话,会丢失一部分数据,可以参考  Spark & Kafka - Achieving zero data-loss。

本文主要讲在使用第二种消费方式(Direct Approach)的情况下,如何将 kafka 中的 offset 保存到 zookeeper 中,以及如何从 zookeeper 中读取已存在的 offset。

大致思想就是,在初始化 kafka stream 的时候,查看 zookeeper 中是否保存有 offset,有就从该 offset 进行读取,没有就从最新/旧进行读取。在消费 kafka 数据的同时,将每个 partition 的 offset 保存到 zookeeper 中进行备份,具体实现参考下面代码

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. val topic : String = "topic_name" //消费的 topic 名字  
  2.     val topics : Set[String] = Set(topic) //创建 stream 时使用的 topic 名字集合  
  3.     val topicDirs = new ZKGroupTopicDirs("test_spark_streaming_group", topic)  //创建一个 ZKGroupTopicDirs 对象,对保存  
  4.     val zkTopicPath = s"${topicDirs.consumerOffsetDir}" //获取 zookeeper 中的路径,这里会变成 /consumers/test_spark_streaming_group/offsets/topic_name  
  5.     val zkClient = new ZkClient("10.4.232.77:2181"//zookeeper 的host 和 ip,创建一个 client  
  6.     val children = zkClient.countChildren(s"${topicDirs.consumerOffsetDir}"//查询该路径下是否字节点(默认有字节点为我们自己保存不同 partition 时生成的)  
  7.     var kafkaStream : InputDStream[(String, String)] = null     
  8.     var fromOffsets: Map[TopicAndPartition, Long] = Map()   //如果 zookeeper 中有保存 offset,我们会利用这个 offset 作为 kafkaStream 的起始位置  
  9.     if (children > 0) {   //如果保存过 offset,这里更好的做法,还应该和  kafka 上最小的 offset 做对比,不然会报 OutOfRange 的错误  
  10.         for (i <- 0 until children) {  
  11.           val partitionOffset = zkClient.readData[String](s"${topicDirs.consumerOffsetDir}/${i}")  
  12.           val tp = TopicAndPartition(topic, i)  
  13.           fromOffsets += (tp -> partitionOffset.toLong)  //将不同 partition 对应的 offset 增加到 fromOffsets 中  
  14.           logInfo("@@@@@@ topic[" + topic + "] partition[" + i + "] offset[" + partitionOffset + "] @@@@@@")  
  15.         }  
  16.         val messageHandler = (mmd : MessageAndMetadata[String, String]) => (mmd.topic, mmd.message())  //这个会将 kafka 的消息进行 transform,最终 kafka 的数据都会变成 (topic_name, message) 这样的 tuple  
  17.         kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](ssc, kafkaParam, fromOffsets, messageHandler)  
  18.     }  
  19.     else {  
  20.         kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParam, topics) //如果未保存,根据 kafkaParam 的配置使用最新或者最旧的 offset  
  21.     }  
  22.     var offsetRanges = Array[OffsetRange]()  
  23.     kafkaStream.transform{ rdd =>  
  24.       offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges //得到该 rdd 对应 kafka 的消息的 offset  
  25.       rdd  
  26.     }.map(msg => msg._2).foreachRDD { rdd =>       
  27.       for (o <- offsetRanges) {  
  28.         val zkPath = s"${topicDirs.consumerOffsetDir}/${o.partition}"  
  29.         ZkUtils.updatePersistentPath(zkClient, zkPath, o.fromOffset.toString)  //将该 partition 的 offset 保存到 zookeeper  
  30.         logInfo(s"@@@@@@ topic  ${o.topic}  partition ${o.partition}  fromoffset ${o.fromOffset}  untiloffset ${o.untilOffset} #######")  
  31.       }  
  32.       rdd.foreachPartition(  
  33.         message => {  
  34.           while(message.hasNext) {  
  35.             logInfo(s"@^_^@   [" + message.next() + "] @^_^@")  
  36.           }  
  37.         }  
  38.       )  
  39.     }  

使用上面的代码,我们可以做到 Spark Streaming 程序从 Kafka 中读取数据是不丢失

以上部分我们讲了如何在将 offset 保存在 zk 中,以及进行重用,但是程序中有个小问题“如果程序停了很长很长一段后再启动,zk 中保存的 offset 已经过期了,那会怎样呢?”本文将解决这个问题

如果 kafka 上的 offset 已经过期,那么就会报 OffsetOutOfRange 的异常,因为之前保存在 zk 的 offset 已经 topic 中找不到了。所以我们需要在 从 zk 找到 offset 的这种情况下增加一个判断条件,如果 zk 中保存的 offset 小于当前 kafka topic 中最小的 offset,则设置为 kafka topic 中最小的 offset。假设我们上次保存在 zk 中的 offset 值为 123(某一个 partition),然后程序停了一周,现在 kafka topic 的最小 offset 变成了 200,那么用前文的代码,就会得到 OffsetOutOfRange 的异常,因为 123 对应的数据已经找不到了。下面我们给出,如何获取 <topic, parition> 的最小 offset,这样我们就可以进行对比了

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. val partitionOffset = zkClient.readData[String](s"${topicDirs.consumerOffsetDir}/${i}")  
  2. val tp = TopicAndPartition(topic, i)  
  3. val requestMin = OffsetRequest(Map(tp -> PartitionOffsetRequestInfo(OffsetRequest.EarliestTime, 1)))  
  4. val consumerMin = new SimpleConsumer("broker_host"90921000010000"getMinOffset")  //注意这里的 broker_host,因为这里会导致查询不到,解决方法在下面  
  5. val curOffsets = consumerMin.getOffsetsBefore(requestMin).partitionErrorAndOffsets(tp).offsets  
  6. var nextOffset = partitionOffset.toLong  
  7. if (curOffsets.length > 0 && nextOffset < curOffsets.head) {  // 通过比较从 kafka 上该 partition 的最小 offset 和 zk 上保存的 offset,进行选择  
  8.   nextOffset = curOffsets.head  
  9. }  
  10. fromOffsets += (tp -> nextOffset) //设置正确的 offset,这里将 nextOffset 设置为 0(0 只是一个特殊值),可以观察到 offset 过期的现象  


但是上面的代码有一定的问题,因为我们从 kafka 上获取 offset 的时候,需要寻找对应的 leader,从 leader 来获取 offset,而不是 broker,不然可能得到的 curOffsets 会是空的(表示获取不到)。下面的代码就是获取不同 partition 的 leader 相关代码

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. val topic_name = "topic_name"     //topic_name 表示我们希望获取的 topic 名字  
  2. val topic2 = List(topic_name)         
  3. val req = new TopicMetadataRequest(topic2, 0)  
  4. val getLeaderConsumer = new SimpleConsumer("broker_host"90921000010000"OffsetLookup")  // 第一个参数是 kafka broker 的host,第二个是 port  
  5. val res = getLeaderConsumer.send(req)  
  6. val topicMetaOption = res.topicsMetadata.headOption  
  7. val partitions = topicMetaOption match {  
  8.   case Some(tm) =>  
  9.     tm.partitionsMetadata.map(pm => (pm.partitionId, pm.leader.get.host)).toMap[Int, String]  // 将结果转化为 partition -> leader 的映射关系  
  10.   case None =>  
  11.     Map[Int, String]()  
  12. }  

上面的代码能够得到所有 partition 的 leader 地址,然后将 leader 地址替换掉上面第一份代码中的 broker_list 即可。

到此,在 spark streaming 中将 kafka 的 offset 保存到 zk,并重用的大部分情况都覆盖到了


以上为转载,以下为自己做的代码整合,造了个轮子。

可以配合spark streaming的checkpoint,暂时没有打开,因为spark streaming的checkpoint会保存spark运行的一些状态信息,如果程序作了修改,要从checkpoint启动可能会出错。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. package com.test.streaming  
  2.   
  3. import kafka.api.{TopicMetadataRequest, PartitionOffsetRequestInfo, OffsetRequest}  
  4. import kafka.consumer.SimpleConsumer  
  5. import kafka.message.MessageAndMetadata  
  6. import kafka.serializer.StringDecoder  
  7. import kafka.utils.{ZkUtils, ZKGroupTopicDirs}  
  8. import org.I0Itec.zkclient.ZkClient  
  9. import org.apache.spark.streaming.dstream.InputDStream  
  10. import org.apache.spark.{rdd, SparkConf}  
  11. import org.apache.spark.streaming.kafka.{OffsetRange, HasOffsetRanges, KafkaUtils}  
  12. import org.apache.spark.streaming.{Seconds, StreamingContext}  
  13. import kafka.common.TopicAndPartition  
  14.   
  15. object KafkaTest {  
  16.   def createContext(checkpointDirectory: String) = {  
  17.   
  18.     println("create spark")  
  19.     val topics = "test_tpoics"  
  20.     val group = "test-kafka"  
  21.     val zkQuorum ="10.16.10.191:2181"  
  22.     val brokerList = "10.10.10.196:8092,10.10.10.196:8092"  
  23.     //    val Array(topics, group, zkQuorum,brokerList) = args  
  24.     val sparkConf = new SparkConf().setAppName("Test-SparkDemo-kafka").setMaster("local[3]")  
  25.     sparkConf.set("spark.streaming.kafka.maxRatePerPartition","1")  
  26.     val ssc = new StreamingContext(sparkConf, Seconds(2))  
  27.     //    ssc.checkpoint(checkpointDirectory)  
  28.     val topicsSet = topics.split(",").toSet  
  29.     val kafkaParams = Map[String, String](  
  30.       "metadata.broker.list" -> brokerList,  
  31.       "group.id" -> group,  
  32.       "zookeeper.connect"->zkQuorum,  
  33.       "auto.offset.reset" -> kafka.api.OffsetRequest.SmallestTimeString  
  34.     )  
  35.     val topicDirs = new ZKGroupTopicDirs("test_spark_streaming_group",topics)  
  36.     val zkTopicPath = s"${topicDirs.consumerOffsetDir}"  
  37.     val hostAndPort = "10.16.10.191:2181"  
  38.     val zkClient = new ZkClient(hostAndPort)  
  39.     val children = zkClient.countChildren(zkTopicPath)  
  40.     var kafkaStream :InputDStream[(String,String)] = null  
  41.     var fromOffsets: Map[TopicAndPartition, Long] = Map()  
  42.     if (children > 0) {  
  43.       //---get partition leader begin----  
  44.       val topicList = List(topics)  
  45.       val req = new TopicMetadataRequest(topicList,0)  //得到该topic的一些信息,比如broker,partition分布情况  
  46.       val getLeaderConsumer = new SimpleConsumer("10.16.10.196",8092,10000,10000,"OffsetLookup"// low level api interface  
  47.       val res = getLeaderConsumer.send(req)  //TopicMetadataRequest   topic broker partition 的一些信息  
  48.       val topicMetaOption = res.topicsMetadata.headOption  
  49.       val partitions = topicMetaOption match{  
  50.         case Some(tm) =>  
  51.           tm.partitionsMetadata.map(pm=>(pm.partitionId,pm.leader.get.host)).toMap[Int,String]  
  52.         case None =>  
  53.           Map[Int,String]()  
  54.       }  
  55.       //--get partition leader  end----  
  56.       for (i <- 0 until children) {  
  57.         val partitionOffset = zkClient.readData[String](s"${topicDirs.consumerOffsetDir}/${i}")  
  58.         val tp = TopicAndPartition(topics, i)  
  59.         //---additional begin-----  
  60.         val requestMin = OffsetRequest(Map(tp -> PartitionOffsetRequestInfo(OffsetRequest.EarliestTime,1)))  // -2,1  
  61.         val consumerMin = new SimpleConsumer(partitions(i),8092,10000,10000,"getMinOffset")  
  62.         val curOffsets = consumerMin.getOffsetsBefore(requestMin).partitionErrorAndOffsets(tp).offsets  
  63.         var nextOffset = partitionOffset.toLong  
  64.         if(curOffsets.length >0 && nextOffset < curOffsets.head){  //如果下一个offset小于当前的offset  
  65.           nextOffset = curOffsets.head  
  66.         }  
  67.         //---additional end-----  
  68.         fromOffsets += (tp -> nextOffset)   
  69.       }  
  70.       val messageHandler = (mmd : MessageAndMetadata[String, String]) => (mmd.topic, mmd.message())   
  71.       kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](ssc, kafkaParams, fromOffsets, messageHandler)  
  72.     }else{  
  73.       println("create")  
  74.       kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicsSet)  
  75.     }  
  76.     var offsetRanges = Array[OffsetRange]()  
  77.     kafkaStream.transform{  
  78.       rdd=>offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges  
  79.         rdd  
  80.     }.map(msg=>msg._2).foreachRDD{rdd=>  
  81.       for(offset <- offsetRanges ){  
  82.         val zkPath = s"${topicDirs.consumerOffsetDir}/${offset.partition}"  
  83.         ZkUtils.updatePersistentPath(zkClient,zkPath,offset.fromOffset.toString)  
  84.       }  
  85.       rdd.foreachPartition(  
  86.         message=>{  
  87.           while(message.hasNext){  
  88.             println(message.next())  
  89.           }  
  90.         })  
  91.     }  
  92.     ssc  
  93.   }  
  94.   
  95.   def main(args: Array[String]) {  
  96.   
  97.     val checkpointDirectory = "kafka-checkpoint2"  
  98.     System.setProperty("hadoop.home.dir","D:\\Program Files\\hadoop-2.2.0")  
  99.     val ssc = StreamingContext.getOrCreate(checkpointDirectory,  
  100.       () => {  
  101.         createContext(checkpointDirectory)  
  102.       })  
  103.     ssc.start()  
  104.     ssc.awaitTermination()  
  105.   }  
  106. }  


0 0
原创粉丝点击