Spark Streaming exactly once原理及编程示例

来源:互联网 发布:最佳睡眠时间知乎 编辑:程序博客网 时间:2024/05/17 06:56

上一节内容介绍了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.sparkimport org.I0Itec.zkclient.ZkClientimport org.I0Itec.zkclient.ZkConnectionimport kafka.message.MessageAndMetadataimport kafka.common.TopicAndPartitionimport kafka.utils.{ ZKGroupTopicDirs, ZkUtils }import org.apache.spark.streaming.kafka.OffsetRangeimport org.apache.spark.streaming.kafka.KafkaClusterimport org.apache.log4j._import kafka.serializer.Decoderimport kafka.serializer.StringDecoderimport kafka.message._import org.apache.spark._import org.apache.spark.streaming._import org.apache.spark.streaming.StreamingContext._import java.util.HashMapimport org.apache.kafka.clients.producer.{ KafkaProducer, ProducerConfig, ProducerRecord }import org.apache.spark.streaming.kafka._import org.apache.spark.streaming.dstream.InputDStreamobject 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 0
原创粉丝点击