Spark Streaming程序怎么才能做到不丢数据

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_42411818/article/details/99676331

Spark Streaming在接收数据的时候有两种模式,第一种是基于Receiver模式,第二种是Kafka Direct模式,两者不丢数据的处理方式不一样,下面我们就来聊聊这两种模式不丢数据的处理策略

基于Receiver模式

在这种模式下,我们可以使用checkpoint + WAL + ReliableReceiver的方式保证不丢失数据,就是说在driver端打开chechpoint,用于定期的保存driver端的状态信息到HDFS上,保证driver端的状态信息不会丢失;在接收数据Receiver所在的Executor上打开WAL,使得接收到的数据保存在HDFS中,保证接收到的数据不会丢失;因为我们使用的是ReliableReceiver,所以在Receiver挂掉的期间,是不会接收数据,当这个Receiver重启的时候,会从上次消费的地方开始消费。
所以Spark Streaming的checkpoint机制包括driver端元数据的checkpoint以及Executor端的数据的checkpoint(WAL以及updateStateByKey等也需要checkpint),Executor端的checkpoint机制除了保证数据写到HDFS之外,还有切断很长的RDD依赖的功效(因为RDD是一个很长的依赖链,如果有checkpoint机制,那么在某一个依赖链断的时候,就不会从头到尾的去再去计算一次了,只需要从checkpoint的位置重新计算即可)

Kafka Direct模式

这种模式下,因为数据源都是存储在Kafka中的,所以一般不会丢数据,但是有一种情况下可能会丢失数据,就是当Spark Streaming应用失败后或者升级重启的时候因为没有记住重启之前消费的topic的offset,使得重启后Spark Streaming从topic的最新的offset开始消费(这个是默认的行为),这样就导致Spark Streaming消费不到失败或者重启过程中Kafka接收到的消息,解决这个问题的办法有三个:

1、使用Spark Streaming自带的Driver端checkpoint机制,因为Driver端checkpoint机制会定期的保存Driver端的状态信息,当然也包括当前批次消费的Kafka中topic的offset信息,这样下次重启的时候就可以从checkpoint文件中直接读取上次消费到的offset信息,然后从这个offset开始消费。但是Driver端的checkpoint机制有一个很明显的缺陷,因为Driver端的checkpoint机制保存的Driver端的状态信息还包含DStreamGraph的状态信息,意思就是将Driver端的代码序列化到checkpoint文件中,这样的话,如果我们对代码做了很大的改动或者升级的话,那么升级后的代码和checkpoint文件中的代码不兼容,这样的话会导致重启失败,解决这个问题的方法就是每次升级的时候将checkpoint文件清除掉,但是这样做的话也清除了保存在checkpoint文件中上次消费到的offset信息,这个不是我们想要的,所以这种方式不可取。
2、我们可以在每一个批次开始之前将我们消费到的offset手动的保存到其他第三方存储系统中,可以是zookeeper或者Hbase,如下:

import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.kafka.common.TopicPartition
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.kafka010.HasOffsetRanges
import org.slf4j.LoggerFactory

class Stopwatch {
  // 记录当前时间和开始时间的时间差
  private val start = System.currentTimeMillis()

  override def toString() = (System.currentTimeMillis() - start) + " ms"

}

trait OffsetsStore {

  // 每一个分区消费的数据信息,从数据库中读取出来
  def readOffsets(topic: String): Option[Map[TopicPartition, Long]]

  // 把消费了的offset保存到数据库中去
  def saveOffsets(topic: String, rdd: RDD[_]): Unit

}

/**
  *  将Spark Streaming消费的kafka的offset信息保存到zookeeper中
  * @param zkHosts zookeeper的主机信息
  * @param zkPath offsets存储在zookeeper的路径
  */
class ZooKeeperOffsetsStore(zkHosts: String, zkPath: String) extends OffsetsStore with Serializable {
  private val logger = LoggerFactory.getLogger("ZooKeeperOffsetsStore")

  @transient   // transient意思是在写磁盘的时候会忽略掉这个属性
  private val client = CuratorFrameworkFactory.builder()  // 创建ZK的客户端
    .connectString(zkHosts)
    .connectionTimeoutMs(10000)   // 超时时间
    .sessionTimeoutMs(10000)   // 重试间隔时间
    .retryPolicy(new ExponentialBackoffRetry(1000, 3))
    .build()
  client.start()

  /**
    *  从zookeeper上读取Spark Streaming消费的指定topic的所有的partition的offset信息
    * @param topic
    * @return
    */
  override def readOffsets(topic: String): Option[Map[TopicPartition, Long]] = {
    logger.info("Reading offsets from ZooKeeper")
    val stopwatch = new Stopwatch()

    val offsetsRangesStrOpt = Some(new String(client.getData.forPath(zkPath)))  // 从zkpath去读消费情况的数据
    offsetsRangesStrOpt match {
      case Some(offsetsRangesStr) =>
        logger.info(s"Read offset ranges: ${offsetsRangesStr}")
        if (offsetsRangesStr.isEmpty) {
          None
        } else {  // 不是空的话就对数据进行解析
          val offsets = offsetsRangesStr.split(",")
            .map(s => s.split(":"))
            .map { case Array(partitionStr, offsetStr) => (new TopicPartition(topic, partitionStr.toInt) -> offsetStr.toLong) }
            .toMap

          logger.info("Done reading offsets from ZooKeeper. Took " + stopwatch)

          Some(offsets)
        }
      case _ =>
        logger.info("No offsets found in ZooKeeper. Took " + stopwatch)
        None
    }
  }

  /**
    *  将指定的topic的所有的partition的offset信息保存到zookeeper中
    * @param topic
    * @param rdd
    */
  override def saveOffsets(topic: String, rdd: RDD[_]): Unit = {
    logger.info("Saving offsets to ZooKeeper")
    val stopwatch = new Stopwatch()

    val offsetsRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
    offsetsRanges.foreach(offsetRange => logger.info(s"Using $offsetRange"))

    // 分区和offset进行拼接
    val offsetsRangesStr = offsetsRanges.map(offsetRange => s"${offsetRange.partition}:${offsetRange.fromOffset}")
      .mkString(",") //partition1:220,partition2:320,partition3:10000
    logger.info(s"Writing offsets to ZooKeeper: $offsetsRangesStr")
    client.setData().forPath(zkPath, offsetsRangesStr.getBytes())
    //ZkUtils.updatePersistentPath(zkClient, zkPath, offsetsRangesStr)

    logger.info("Done updating offsets in ZooKeeper. Took " + stopwatch)
  }
}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategy}

import scala.collection.JavaConversions._

object KafkaSource {
  def createDirectStream[K, V](ssc: StreamingContext, locationStrategy: LocationStrategy,
      kafkaParams: Map[String, Object], zkHosts: String, zkPath: String, topic: String
    ): DStream[ConsumerRecord[K, V]] = {
    val offsetsStore = new ZooKeeperOffsetsStore(zkHosts, zkPath)
    val storedOffsets = offsetsStore.readOffsets(topic)  // 读出数据
    val topicSet = Set(topic)
    val kafkaStream = storedOffsets match {
      case None =>  // 没有数据的话就是第一次启动
        KafkaUtils.createDirectStream[K, V](ssc, locationStrategy,
          ConsumerStrategies.Subscribe[K, V](topicSet, kafkaParams))
      case Some(fromOffsets) =>  // 如果有值的话就把之前的offset告诉程序
        KafkaUtils.createDirectStream[K, V](ssc, locationStrategy,
          ConsumerStrategies.Subscribe[K, V](util.Arrays.asList(topic), kafkaParams, fromOffsets))
    }
    // 保存消费数据,读出来了就直接保存
    kafkaStream.foreachRDD(rdd => offsetsStore.saveOffsets(topic, rdd))

    kafkaStream
  }
}

这样就是实现了手动的保存我们每一个批次消费到的topic的offset信息,数据也不会丢失了

3、也可以直接调用Kafka中高级的API,将消费的offset信息保存到zookeeper中,如下:
在这里插入图片描述
当重启Spark Streaming应用的时候,Spark Streaming会自动的从zookeeper中拿到上次消费的offset信息

关于spark streaming batch超时问题

在项目初期就应该按照需求设计,参考http://blog.cloudera.com/blog/2015/03/how-to-tune-your-apache-spark-jobs-part-2,比如:
num_executors取决于如下因素:
1:每一秒接收到的events,尤其是在高峰时间
2:数据源的缓冲能力
3:可以忍受的最大的滞后时间

executor_memory取决于如下因素:
1:每一个batch需要处理的数据的大小
2:transformations API的种类,如果使用的transformations(比如:groupByKey)需要shuffle的话,则需要的内存更大一点
3:使用状态Api的话(updateStateByKey,mapWithState,注意mapWithState虽然性能更好(因为有timeout 超时时间,不需要加更多的判断,会自动的清除超时数据),但是目前还处于试验版本,因此实际中更多的是用updateStateByKey),需要的内存更加大一点,因为需要内存缓存每一个key的状态

executor_cores:
每一个executor配置3到5个cores是比较好的,因为3到5个并发写HDFS是最优的

backpressure 方向调节接收速率:
receiver_max_rate=1000
receiver_initial_rate=30
–conf spark.streaming.kafka.maxRatePerPartition=${receiver_max_rate} #direct模式读取kafka每一个分区数据的最大速度

关于kafka的积压问题

参考http://xuyangyang.club/articles/2018/07/23/1532348839398.html

猜你喜欢

转载自blog.csdn.net/weixin_42411818/article/details/99676331