上一节内容介绍了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()
}
大概也就如此了。