Spark Streaming与Kafka的整合

SparkStreaming与Kafka整合

  • receiver模式
    • 基于receiver方式的图解
    • 数据丢失问题
    • 数据吞吐量问题
    • receiver方式存在的其余问题
    • receiver方式的注意点
    • receiver方式的总结
  • direct模式
  • 基于direct的整合[代码]
    • LocationStrategies的选择
    • 管理offset的三种方式
      • checkpoint
      • kafka itself
      • 借助三方存储

官网:http://spark.apache.org/docs/latest/streaming-kafka-integration.html
从官网的介绍当中我们也可以发现,Spark Streaming有2种方式去接收kafka的数据:

  • 使用receiver去接收数据,同时使用的是kafka的high level api
  • 另外一种方式是没有使用receiver的,即direct方式,是从Spark 1.3开始

本文将针对这两种方式进行详解,其中在direct模式中还会涉及对offset的管理与维护

receiver模式

这种方式使用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

基于receiver方式的图解

Spark Streaming与Kafka的整合_第1张图片
从上图中我们可以发现:

  • receiver是跑在executor中的(只在一个executor中运行),在executor中会有一个专门的task来运行接收数据,且是常驻的
  • 这种方式,默认的Storage level为MEMORY_AND_DISK_SER_2
    由于是该策略,因此会将数据存放在2个地方,然后会有一堆task来进行处理数据
  • 使用的是kafka高阶api,kafka对应的offset信息是存储在ZK中的
  • 数据存在哪个executor上会和Driver进行汇报
    后面Driver可以根据数据存在的地方,来发放task到对应节点来进行运行

数据丢失问题

基于上述的实现会存在一个问题:
task接收数据,并存储offset到zk中去,那么当driver挂了,由于driver挂了,相应的executor也会被随之干掉,那么在executor中的数据也就丢失了(当这部分数据还没处理完的时候)

解决方案:
为了保证数据不丢失,就需要额外开启WAL机制,该机制在Spark 1.2版本中出来的
WAL机制即先将日志记录下来,会将所有接受到的数据都写到日志中去(存储到HDFS上)
这也driver端恢复之后,可以通过HDFS上的WAL进行数据的恢复
WAL参数:spark.streaming.receiver.writeAheadLog.enable

思考几个问题:

  • 为什么Storage level是MEMORY_AND_DISK_SER_2?
    设置为2,就是考虑到数据会丢失的场景;但是从上述的案例中我们可以发现,即使是2也很难保证数据不丢失
  • 开启了WAL机制后,还有必要设置为2么?
    答案是没有必要的;如果开启了WAL机制,接收的数据就已经被写入到HDFS上去了,HDFS本身就有副本,因此这个Storagelevel直接用MEMORY_AND_DISK_SER就行了

数据吞吐量问题

在开启了WAL机制后,的确是解决了数据丢失的问题,但是随之带来了个问题:

  • 写完HDFS的WAL之后,再去更新zk中的offset,这样整体的吞吐量肯定是下降
  • 当中引入了HDFS,整体的链路拉长了,吞吐量会急剧下降

receiver方式存在的其余问题

  • 在程序失败恢复时,有可能出现数据部分落地,但是程序失败,未更新offset的情况(即数据写入成功,offset没有保存成功),这样就可能会导致数据重复消费
  • 由于这里的offset是kafka自己维护在zk中的,因此如果想要保证精准一次的消费语义,就需要保证数据写入操作和offset保存操作的原子性,要么一起成功要么一起失败;或者保证数据写入操作的幂等性

receiver方式的注意点

  • Kafka的topic是有partition的;假设1个topic对应了3个partition,那么RDD的并行度不是3的,kafka与rdd的partition是不等于的;增加topic的partition,只会多增加几个线程去处理接收数据,但是并不会提高Spark处理数据时候的并行度
  • 多个Kafka的input DStream被创建,即有多个不同的group和topic,这样做目的是为了增加receiver的数量,进而提高接收数据的并行度

receiver方式的总结

  • 会存在数据丢失的情况,可以用WAL机制解决
  • WAL机制导致了数据延迟现象的产生
  • 好处在于offset,我们不需要关注,使用的高阶api,直接kafka自己维护就搞定了

direct模式

Spark Streaming与kafka的对接采用direct方式,有如下几个特点:

  • 不需要Receiver
  • topic的partition和RDD的partition是1:1
  • 自己手工维护offset

基于direct的整合[代码]

LocationStrategies的选择

新的Kafka Consumer API会提前fetch消息放入buffer中,之后将会分发数据信息到可用的executor上面;在很多场景下会选择使用PreferConsistent:

  • 如果executor和kafka的broker在同一个节点上,应该采取PreferBrokers(可能性不大)
  • 否则则选用PreferConsistent,数据尽量均匀的分布到各个executor上面去

管理offset的三种方式

checkpoint

生产上不建议使用这种方式

kafka itself

使用Kafka提供的api来进行维护offset,核心提交代码就一行:
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)

注意点:

  • 使用Kafka自身维护offset,对应的offset信息存在__consumer_offsets这个topic中
  • 必须在业务逻辑处理完成之后,再进行提交offset信息,因为kafka并不是事务型的,所以我们的输出必须保证幂等性

代码示例:

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()
  }
}

你可能感兴趣的:(Spark)