Spark Streaming exactly once原理及编程示例

上一节内容介绍了spark介绍了at least once以及at most once的实现原理,这里再重复一次,毕竟这些概念非常重要。 任何涉及到消息队列的服务,都会出现3个层面的问题,一个是获取数据,一个是处理数据,一个是存储数据 . 因此在谈论at least once/exactly once也要分3个阶段(这一点storm和spark是不同的,因为storm是等待处理完数据发送ACK的方式,而spark分3阶段保证)。

对于处理数据层,spark RDD天生就能保证exactly once,所以不做讨论。

通过预先写入日志WAL,让spark保存了足够的信息来恢复,不管是worker 出现问题,还是driver出现问题,都能够通过checkpoint的wal来恢复,也因此保证了at least once, 大家首先要看清楚,这里的at least once是指receiver层面。 

存储层,实际就是写入数据的时候,如果此时worker或者driver 失败了,怎没保证exactly once . spark并不提供任何保护措施,需要用户自己实现。

说道这里我要说一下目前开源软件的文档问题,storm, spark这些文章极其粗糙,和原来的文档相差太多,很多东西随便略过,让用户自己去想,好歹你给个例子。


接下来继续 spark实现exactly once的问题,之前采用的是createstream,因为有2个问题,一个是wal会影响性能,另外就是并发问题,总之就是性能不行,我也不知道为什么。所以在1.3 之后的版本出现了createdirectstream. 

createDirectStream默认会根据你的kafka partition个数来分配task,一个partition一个task,效率会高很多,另外spark自己会处理offset,而不是保存在zk(实际上我没觉得保存在 zk有啥问题)。那么不保存在ZK,那保存在哪里? 内存里。 那如果 spark任务出现问题怎么恢复到之前的offset?  答案是没有办法, 需要你自己处理。 


因为新版api的问题,我们有几个东西需要自己看考虑,自己来做了,spark不会为你做了。

1)  由于默认不保存ZK,那么我们自己需要考虑保存在哪里? 下一次从保存的offset处理数据。假设我们仍然手动保存在ZK。

2)  JOB启动的时候先判断ZK是否有保存的offsets,如果有,调用

 KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](ssc, kafkaParams, fromOffsets, messageHandler)
    如果没有保存,有可能是第一次处理,调用
 KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicsSet)

因此我们的整个程序和之前createstream会有比较多的区别,整个程序如下:


package com.isesol.spark

import org.I0Itec.zkclient.ZkClient
import org.I0Itec.zkclient.ZkConnection
import kafka.message.MessageAndMetadata
import kafka.common.TopicAndPartition
import kafka.utils.{ ZKGroupTopicDirs, ZkUtils }
import org.apache.spark.streaming.kafka.OffsetRange
import org.apache.spark.streaming.kafka.KafkaCluster
import org.apache.log4j._
import kafka.serializer.Decoder
import kafka.serializer.StringDecoder
import kafka.message._
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
import java.util.HashMap
import org.apache.kafka.clients.producer.{ KafkaProducer, ProducerConfig, ProducerRecord }
import org.apache.spark.streaming.kafka._
import org.apache.spark.streaming.dstream.InputDStream

object low_streaming {
  def main(args: Array[String]) {

    Logger.getLogger("org").setLevel(Level.WARN)
    val conf = new SparkConf().setMaster("yarn-cluster").setAppName("this is the first spark streaming program!")
    val ssc = new StreamingContext(conf, Seconds(5))
    val zk = "datanode01.isesol.com:2181,datanode02.isesol.com:2181,datanode03.isesol.com:2181,datanode04.isesol.com:2181,cmserver.isesol.com:2181"
    val brokers = "namenode02.isesol.com:9092,namenode01.isesol.com:9092,datanode04.isesol.com:9092,datanode03.isesol.com:9092"
    val group = "low_api1"
    val topics = "2001"
    //val topicsSet = topics.split(",").toSet
    val topicsSet = Set(topics)
    val numThreads = 2
    var offsetRanges = Array[OffsetRange]()
    val kafkaParams = Map[String, String]("metadata.broker.list" -> brokers, "zookeeper.connect" -> "datanode01.isesol.com:2181,datanode02.isesol.com:2181,datanode03.isesol.com:2181,datanode04.isesol.com:2181,cmserver.isesol.com:2181", "group.id" -> group, "auto.offset.reset" -> "largest")
    var kafkaStream: InputDStream[(String, String)] = null
    var fromOffsets: Map[TopicAndPartition, Long] = Map()
    val zk_topic = new ZKGroupTopicDirs("2001_manual", topics)
    val zkClient = new ZkClient(zk)
    val child = zkClient.countChildren(s"${zk_topic.consumerOffsetDir}")
    
    /*  判断ZK是否包含offset信息,如果包含,则通过fromoffsets来开始获取数据, 如果不包含则根据kafkaparams的规则获取数据,随后会跟心获取到的offset到ZK,
     *  fromoffsets是一个map结构,因为一个kafka topic可能不止一个partition */
    
    if (child > 0) {
      for (i <- 0 until child) {
        val partitionOffset = zkClient.readData[String](s"${zk_topic.consumerOffsetDir}/${i}")
        val topicandpartition = TopicAndPartition(topics, i)
        fromOffsets += (topicandpartition -> partitionOffset.toLong)
      }

      println("zk contains topic offsets, then goes to messagehandler part!")
      
      /*messagehandler会把kafka数据归并为(topic_name, message)的tuple方式, 因此在取数据的时候只取 _.2 */
      
      val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.topic, mmd.message())

      kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](ssc, kafkaParams, fromOffsets, messageHandler)
    } else {

      println("zk doesn't contains topic offsets, then goes to messagehandler part!")
      kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicsSet)
    }

    kafkaStream.transform {
      rdd =>
        offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        rdd
    }.foreachRDD { rdd =>
      for (offsets <- offsetRanges) {
        println(s"${offsets.topic} ${offsets.partition} ${offsets.fromOffset} ${offsets.untilOffset}")
      
        /*  更新获取到的offsets,更新到ZK实际不是原子型,因为仍然不能保证exactly once,这里仅仅是一个示例,如果通过MySQL之类的事务型数据库,是能保证的 */
        
        val zkPath = s"${zk_topic.consumerOffsetDir}/${offsets.partition}"
        val zkconnection = new ZkConnection(zk)
        val zkUtils = new ZkUtils(zkClient, zkconnection, false)       
        zkUtils.updatePersistentPath(zkPath, offsets.fromOffset.toString)
      }
      val line = rdd.map(x => x._2)
      println(line.count())
    }

    ssc.start()
    ssc.awaitTermination()
  }
}


上面的代码实际还是比较容易理解的,通过if(child > 0) 来判断是否ZK包含了数据,如果不包含则根据kafkaparam的设置来获取数据,如果包含了,说明之前已经处理了数据,那么从fromoffsets开始获取数据。 另外通过messagehandler把数据归并成tuple格式(topic_name, message) , 因此在实际处理RDD的时候,我们只需要处理 _.2的 message即可。

其他好像没有太大区别,仅仅是在读取offsets时候自己需要手动处理。 网上有很多文章极其杂乱,不知道从哪里抄袭的,连抄袭都抄不好,太让人失望。


提交jar包:

spark-submit --class com.isesol.spark.low_streaming  --master yarn --deploy-mode cluster --jars spark-streaming-kafka_2.10-1.6.0-cdh5.9.0.jar --driver-memory 1g --executor-memory 1G    --num-executors 5  low_streaming.jar  

然后观察数据输出,以及查看ZK的offsets是否再实时变化。 另外生产环境应该是会保存到HBASE或者HDFS文件,保存在HBASE大家可以根据上一篇文章的做法即可。这里也贴一下大概代码:

  rdd.foreachPartition { x =>
        val hbaseconf = HBaseConfiguration.create()
        hbaseconf.set("hbase.zookeeper.quorum", "datanode01.isesol.com,datanode02.isesol.com,datanode03.isesol.com,datanode04.isesol.com,cmserver.isesol.com")
        hbaseconf.set("hbase.zookeeper.property.clientPort", "2181")
        val myTable = new HTable(hbaseconf, TableName.valueOf("test"))
        //myTable.setAutoFlush(false)
        myTable.setWriteBufferSize(3 * 1024 * 1024)
        x.foreach { y =>
          {
            println(y)
            val p = new Put(Bytes.toBytes(System.currentTimeMillis().toString()))
            p.add(Bytes.toBytes("cf"), Bytes.toBytes("message"), Bytes.toBytes(y.toString()))
            myTable.put(p)
          }
        }
        myTable.close()
      }

大概也就如此了。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tom_fans

谢谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值