Spark Streaming消费kafka数据有两种方式,一种是基于接收器消费kafka数据,使用Kafka的高级API;另一种是直接消费方式,使用Kafka的低级API。

下面,详细说明、对比下这两种方式的优缺点。

一、Receiver-based Approach

这种方式,采用Kafka的高级API,使用接收器接收kafka的消息。接收器在接收到kafka数据后,把数据保存在Spark executor中,然后Spark Streaming任务再从中获取数据。

采用这种方式,默认配置情况下,当发生故障的时候,kafka数据可能会丢失。为了保证数据不丢吗,需要在Spark Streaming的处理过程中加入日志预写机制,就是将从kafka中获取的消息同步保存到日志中一份,比如分布式文件系统HDFS,当故障发生的时候,所有的数据都可以被恢复。

代码示例:

import org.apache.spark.streaming.kafka._

 val kafkaStream = KafkaUtils.createStream(streamingContext, 
     [ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])

二、Direct Approach

从spark1.3开始,推出这种无接收者机制的“直接”方式,以确保更健壮的端到端的保证。和接收器接收数据对比,这种方式每个批次周期性地获取kafka每个partition的指定范围的最新数据。

和方式一对比,方式二有如下几个有点:

1)简化并行度

不需要创建多个kafka数据流再合并多个输入流。Spark Streaming会创建和kafka partition个数相同的RDD partition数并行去消费kafka的数据,所以kafka和RDD的partition之间是一对一的映射关系,这样可以更便于理解和调优。

2)高效

为了保证数据不丢,在方式一中,需要采用日志预写机制Write Ahead Log,该日志对数据进行了复制。实际上这种方式是低效的,因为数据被消费了两次---一次是被kafka复制,另外一次是被预写机制复制。第二种方法消除了这种低效,因为没有接收机,不需要日志预写机制。只要保留在kafka中没有被清除的消息,就可以被消费。

3)精准一次语义

第一种传统方式采用kafka的高级API,将offset存储在Zookeeper中。这种方式虽然可以确保消息不会丢失,但是当故障发生时,由于SparkStreaming接收的数据和Zookeeper中保存的偏移量不一致,消息可能会被重复消费。在第二种方法中,使用kafka的简单API,不需要Zookeeper保存kafka的偏移量,这样就可以杜绝第一种方法中的隐患,即使故障出现,SparkStreaming也可以保证精确的一次性消费。

注意,这种方式不会更新Zookeeper中的偏移量,所以基于Zookeeper的kafka监视器无法展示kafka消息消费进展。这种情况下,只有手动去获取每个批次的偏移量,并手动更新到Zookeeper中。

代码示例:

import org.apache.spark.streaming.kafka._

 val directKafkaStream = KafkaUtils.createDirectStream[
     [key class], [value class], [key decoder class], [value decoder class] ](
     streamingContext, [map of Kafka parameters], [set of topics to consume])

另外,可以传递一个messageHandlertoDirectStream来访问messageAndMetadata,其中包括有关当前消息的元数据,并将其转化为任何所需要的类型。

示例代码:

import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer

import org.apache.spark.SparkConf
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka010._

/**
 * Consumes messages from one or more topics in Kafka and does wordcount.
 * Usage: DirectKafkaWordCount <brokers> <topics>
 *   <brokers> is a list of one or more Kafka brokers
 *   <groupId> is a consumer group name to consume from topics
 *   <topics> is a list of one or more kafka topics to consume from
 *
 * Example:
 *    $ bin/run-example streaming.DirectKafkaWordCount broker1-host:port,broker2-host:port \
 *    consumer-group topic1,topic2
 */
object DirectKafkaWordCount {
  def main(args: Array[String]): Unit = {
    if (args.length < 3) {
      System.err.println(s"""
        |Usage: DirectKafkaWordCount <brokers> <groupId> <topics>
        |  <brokers> is a list of one or more Kafka brokers
        |  <groupId> is a consumer group name to consume from topics
        |  <topics> is a list of one or more kafka topics to consume from
        |
        """.stripMargin)
      System.exit(1)
    }

    StreamingExamples.setStreamingLogLevels()

    val Array(brokers, groupId, topics) = args

    // Create context with 2 second batch interval
    val sparkConf = new SparkConf().setAppName("DirectKafkaWordCount")
    val ssc = new StreamingContext(sparkConf, Seconds(2))

    // Create direct kafka stream with brokers and topics
    val topicsSet = topics.split(",").toSet
    val kafkaParams = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
      ConsumerConfig.GROUP_ID_CONFIG -> groupId,
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer])
    val messages = KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topicsSet, kafkaParams))

    // Get the lines, split them into words, count the words and print
    val lines = messages.map(_.value)
    val words = lines.flatMap(_.split(" "))
    val wordCounts = words.map(x => (x, 1L)).reduceByKey(_ + _)
    wordCounts.print()

    // Start the computation
    ssc.start()
    ssc.awaitTermination()
  }
}

至此,SparkStreaming消费Kafka的两种方式介绍完毕!