本文将针对这两种方式进行详解,其中在direct模式中还会涉及对offset的管理与维护
这种方式使用receiver来接收kafka的数据,receiver是通过kafka的high-level consumer api来进行实现的;通过receiver从kafka接收数据并存储到Spark executors,然后Spark Streaming job开始处理数据
官网相关介绍:
http://spark.apache.org/docs/latest/streaming-kafka-0-8-integration.html#approach-1-receiver-based-approach
基于上述的实现会存在一个问题:
task接收数据,并存储offset到zk中去,那么当driver挂了,由于driver挂了,相应的executor也会被随之干掉,那么在executor中的数据也就丢失了(当这部分数据还没处理完的时候)
解决方案:
为了保证数据不丢失,就需要额外开启WAL机制,该机制在Spark 1.2版本中出来的
WAL机制即先将日志记录下来,会将所有接受到的数据都写到日志中去(存储到HDFS上)
这也driver端恢复之后,可以通过HDFS上的WAL进行数据的恢复
WAL参数:spark.streaming.receiver.writeAheadLog.enable
思考几个问题:
在开启了WAL机制后,的确是解决了数据丢失的问题,但是随之带来了个问题:
Spark Streaming与kafka的对接采用direct方式,有如下几个特点:
新的Kafka Consumer API会提前fetch消息放入buffer中,之后将会分发数据信息到可用的executor上面;在很多场景下会选择使用PreferConsistent:
生产上不建议使用这种方式
使用Kafka提供的api来进行维护offset,核心提交代码就一行:
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
注意点:
代码示例:
object StreamingKafkaDirectV2 {
def main(args: Array[String]): Unit = {
val ssc = ContextUtils.getStreamingContext(this.getClass.getSimpleName, 5)
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "localhost:9093,localhost:9094,localhost:9095",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "use_a_separate_group_id_for_each_stream",
"auto.offset.reset" -> "earliest", //每次从头开始消费数据
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics = Array("huhu_offset")
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent, //数据尽量均匀的分布到各个executor上面去
Subscribe[String, String](topics, kafkaParams)
)
stream.foreachRDD(rdd => {
// 获取当前批次的offset数据
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges.foreach(x => {
println(s"${x.topic} ${x.partition} ${x.fromOffset} ${x.untilOffset}")
})
// 使用Kafka的api来维护offset
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
})
ssc.start()
ssc.awaitTermination()
}
}
借助redis来存储offset数据信息,代码示例:
object StreamingKafkaDirectV3 {
def main(args: Array[String]): Unit = {
val ssc = ContextUtils.getStreamingContext(this.getClass.getSimpleName, 5)
val groupId = "huhu_group"
val topic = "huhu_offset"
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "localhost:9093,localhost:9094,localhost:9095",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> groupId,
"auto.offset.reset" -> "earliest", //每次从头开始消费数据
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics = Array(topic)
// 从保存offset的地方去获取已经提交的offset记录信息
val jedis = RedisUtils.getJedis
val offsets = jedis.hgetAll(topics(0) + "_" + groupId)
var fromOffsets = Map[TopicPartition, Long]()
import scala.collection.JavaConversions._
offsets.map(x => {
fromOffsets += new TopicPartition(topics(0), x._1.toInt) -> x._2.toLong
})
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent, // 数据尽量均匀的分布到各个executor上面去
Subscribe[String, String](topics, kafkaParams, fromOffsets) // 需要增加每次开始消费的offset参数
)
stream.foreachRDD(rdd => {
if (!rdd.isEmpty()) {
println(rdd.partitions.size)
// 获取当前批次的offset数据
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges.foreach(x => {
println(s"${x.topic} ${x.partition} ${x.fromOffset} ${x.untilOffset}")
})
// TODO.. 处理具体的业务逻辑
// 提交offset到redis
val jedis = RedisUtils.getJedis
offsetRanges.foreach(x => {
val topicGroupId = x.topic + "_" + groupId
jedis.hset(topicGroupId, x.partition+"", x.untilOffset+"")
})
jedis.close()
} else {
println("当前批次没有数据")
}
})
ssc.start()
ssc.awaitTermination()
}
}