如何实现SparkStreaming程序的高可用与断点续传

解决7*24小时稳定运行

首先,为了保证实时计算程序可以7*24小时能稳定运行,则第一个考虑就是要实现SparkStreaming对接Kafka实时计算程序HA高可用。

  1. 保证master高可用,worker节点的失败是具有容错性的(迄今为止,Spark自身而言对于丢失部分计算工作是有容错性的,它会将丢失的计算工作迁移到其他worker节点上执行)。然而,调度器是依托于master进程来做出调度决策的,这就会造成单点故障:如果master挂掉了,就没法提交新的应用程序。所以为了解决这个问题,spark有一个高可用方案,基于zookeeper的HA方案。

    • 使用zookeeper来提供leader选举以及一些状态存储,你可以在集群中启动多个master进程,让它们连接到zookeeper实例。其中一个master进程会被选举为leader,其他的master会被指定为standby模式。如果当前的leader master进程挂掉了,其他的standby master会被选举,从而恢复旧master的状态。并且恢复作业调度。整个恢复过程(从leader master挂掉开始计算)大概会花费1~2分钟。要注意的是,这只会推迟调度新的应用程序,master挂掉之前就运行的应用程序是不被影响的。

    在spark-env.sh文件中,设置SPARK_DAEMON_JAVA_OPTS选项:

spark.deploy.recoveryMode		# 设置为ZOOKEEPER来启用standby master恢复模式(默认为NONE)
spark.deploy.zookeeper.url		# zookeeper集群url(举例来说,192.168.75.101:2181,192.168.75.102:2181)
spark.deploy.zookeeper.dir		# zookeeper中用来存储恢复状态的目录(默认是/spark)

备注:如果在集群中启动了多个master节点,但是没有正确配置master去使用zookeeper,master在挂掉进行恢复时是会失败的,因为没法发现其他master,并且都会认为自己是leader。这会导致集群的状态不是健康的,因为所有master都会自顾自地去调度。

1. 在启动一个zookeeper集群之后,简单地在多个节点上启动多个master进程,并且给它们相同的zookeeper配置(zookeeper url和目录),就完成了配置HA高可用。master就可以被动态加入master集群,并可以在任何时间被移除掉。
2. 为了调度新的应用程序或者向集群中添加worker节点,它们需要知道当前leader master的ip地址。这可以通过传递一个master列表来完成。举例来说,我们可以将我们的SparkContext连接的地址指向spark://host1:port1,host2:port2。这就会导致你的SparkContext尝试去注册所有的master,如果host1挂掉了,那么配置还是正确的,因为会找到新的leader master,也就是host2。
3. 对于注册一个master和普通的操作,这是一个重要的区别。当一个应用程序启动的时候,或者worker需要被找到并且注册到当前的leader master的时候。一旦它成功注册了,就被保存在zookeeper中了。如果故障发生了,new leader master会去联系所有的之前注册过的应用程序和worker,并且通知它们master的改变。这样的话,它们甚至在启动的时候都不需要知道new master的存在。正是由于这个属性,new master可以在任何时间被创建,并且我们唯一需要担心的一件事情就是新的应用程序和worker可以找到并且注册到master。一旦注册上去之后,我们就不用担心它了。
  1. 保证Driver高可用性,因为在第一次创建和启动StreamingContext的时候,那么将持续不断地产生实时计算的元数据并写入检查点,如果driver节点挂掉,那么可以让Spark集群自动重启集群(必须使用yarn cluster模式,spark-submit --deploy-mode cluster --supervise …)。
package cn.piesat.spark

import org.apache.kafka.clients.consumer.{ConsumerRecord}
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.sql.SparkSession
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreamingKafka {
  private val brokers = "hadoop01:9092"
  private val topics = "lj01"
  private val checkPointPath = "hdfs://hadoop01:9000/sparkStreaming/kafka6"

  def main(args: Array[String]): Unit = {
    val spark = getSparkSession()
    val streamingContext = StreamingContext.getOrCreate(checkPointPath, () => {
      val ssc = new StreamingContext(spark.sparkContext, Seconds(5))
      ssc.checkpoint(checkPointPath)
      val kafkaInputStream = getKafkaInputStream(ssc)
      val result = kafkaInputStream.map(x => x.value()).flatMap(x => {
        x.split(" ").map(x => {
          (x, 1)
        })
      }).reduceByKey(_ + _)
      result.print()
      ssc
    })
    streamingContext.start()
    streamingContext.awaitTermination()
  }

  def getSparkSession(): SparkSession = {
    SparkSession.builder()
      .appName("kafka_test")
      .master("local[4]")
      .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .getOrCreate()
  }

  def getKafkaInputStream(ssc: StreamingContext): InputDStream[ConsumerRecord[String, String]] = {
    val topicArray = topics.split(",").toList
    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> brokers,
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "lj00",
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )
    KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topicArray, kafkaParams)
    )
  }

}

注意:对streaming的操作逻辑必须写在StreamingContext.getOrCreate()方法里,因为若是第二次恢复时则执行方法里的逻辑!!!
  1. 实现RDD的高可用,设置启动WAL预写日志机制。
    sparkStreaming从原理上说,是通过receiver来进行数据接收的,接收到的数据,会被划分成一个个的block,block会被组合成batch,针对一个batch,会创建一个Rdd,启动一个job来执行定义的算子操作。receiver主要接收到数据,那么就会立即将数据写入一份到时容错文件系统(比如hdfs)上的checkpoint目录中的,一份磁盘文件中去,作为数据的冗余副本。
SparkConf conf = new SparkConf()
    .setMaster("local[2]")
  .setAppName("AdClickRealTimeStatSpark")
  .set("spark.streaming.receiver.writeAheadLog.enable","true");
  1. 保证启用checkpoint,需在updateStateByKey、mapWithState、基于window等有状态的操作时,设置自动保存checkpoint,即必须设置cheakpoint目录,其一般是保存在HDFS上。

解决在程序升级的条件下,实现断点续传

断点续传是指数据同步任务在运行过程中因各种原因导致任务失败,不需要重头同步数据,只需要从上次失败的位置继续同步即可,类似于下载文件时因网络原因失败,不需要重新下载文件,只需要继续下载就行,可以大大节省时间和计算资源。保证能断点续传,则需要结合任务的出错重试机制才能完成。当任务运行失败,会在Engine###里进行重试,重试的时候会接着上次失败时读取的位置继续读取数据,直到任务运行成功为止。

前置条件

  1. 数据源(这里特指关系数据库)中必须包含一个升序的字段,比如主键或者日期类型的字段,同步过程中会使用checkpoint机制记录这个字段的值,任务恢复运行时使用这个字段构造查询条件过滤已经同步过的数据,如果这个字段的值不是升序的,那么任务恢复时过滤的数据就是错误的,最终导致数据的缺失或重复;
  2. 数据源必须支持数据过滤,如果不支持的话,任务就无法从断点处恢复运行,会导致数据重复;
  3. 目标数据源必须支持事务,比如关系数据库,文件类型的数据源也可以通过临时文件的方式支持;

如果任务异常终止,任务如果异常结束,假设任务结束时最后一个checkpoint记录的状态为异常结束的offset,那么任务恢复的时候就会把各个通道记录的状态赋值给offset,再次读取数据时构造sql会加上where条件。

以上,就能满足7*24小时稳定运行,且解决在程序升级的条件下,实现断点续传。

猜你喜欢

转载自blog.csdn.net/weixin_42526352/article/details/105623515