Day72_Spark-streaming(二)

(三)、SparkStreaming算子

1、常见的算子操作

由于Streaming底层是基于Core来实现的,所以其很多算子相似于RDD,如下图1-11所示。

 这里我们主要学习三个算子,transform,updateByKey,window函数。

2、Transform

(1)概述

transform是一个transformation算子,转换算子。

怎么去理解呢?DStream上述提供的所有的transformation操作,都是DStream-2-DStream操作,没有一个DStream和RDD的直接操作,而DStream本质上是一系列RDD,所以RDD-2-RDD操作是显然被需要的,所以此时官方api中提供了一个为了达成此操作的算子——transform操作。

​也就是说transform主要就是用来自定义官方api没有提供的一些操作。

(2)需求简介——动态排名

 热点排行榜是专门为网民提供的每天热点排行榜平台,让网民可以每天看到实时热点,历史热点,明星热点等排行榜,下面简单给大家介绍如何针对热点进行排名操作。

(3)代码实现

/*
    transform,是一个transformation操作
        transform(p:(RDD[A]) => RDD[B]):DStream[B]
     类似的操作
        foreachRDD(p: (RDD[A]) => Unit)
     transform的一个非常重要的一个操作,就是来构建DStream中没有的操作,DStream的大多数操作都可以用transform来模拟
     比如map(p: (A) => B) ---> transform(rdd => rdd.map(p: (A) => B))
 */

import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
object Advertising_ranking {
    def main(args: Array[String]): Unit = {
        //创建程序入口
        val conf: SparkConf = new SparkConf().setAppName("advertising").setMaster("local[*]")
        val sc = new SparkContext(conf)
        val ssc = new StreamingContext(sc,Seconds(5))
        //设置日志级别
        sc.setLogLevel("WARN")
        //接收数据
        val data: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
        //切分数据
        val spliData: DStream[String] = data.flatMap(_.split(" "))
        //每条点击流日志记为1次
        val pageAndOne: DStream[(String, Int)] = spliData.map((_,1))
        //聚合相同的点击流
        val pageAndCount: DStream[(String, Int)] = pageAndOne.reduceByKey(_+_)
        //遍历DStream中封装的RDD进行操作
        val resultSorted: DStream[(String, Int)] = pageAndCount.transform(rdd => {
            //对RDD当中的数据进行倒叙排名
            val sorted: RDD[(String, Int)] = rdd.sortBy(_._2, false)
            //从排名数据中取top3
            val topThree: Array[(String, Int)] = sorted.take(3)
            //打印输出
            topThree.foreach(println)
            //因为transform需要返回值
            sorted
        })
        //打印整体排名情况
        resultSorted.print()
        //启动sparkStreaming
        ssc.start()
        //让其一直启动,等待程序关闭
        ssc.awaitTermination()
    }
}

3.updateStateByKey

(1)概述

updateStateByKey(func)根据于key的前置状态和key的新值,对key进行更新,返回一个新状态的Dstream。

简单理解就是:统计截止到目前为止key的状态。

通过分析,我们需要清楚:在这个操作中需要两个数据,一个是key的前置状态,一个是key的新增(当前批次的数据);还有历史数据(前置状态)得需要存储在磁盘,不应该保存在内存中。

同时key的前置状态可能有可能没有。

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

object UpdateStateByKey_Demo {
  def updateFunc(currentValue:Seq[Int],historyValue:Option[Int]): Option[Int] = {
    val result: Int = currentValue.sum+historyValue.getOrElse(0)
    Some(result)
  }
  def main(args: Array[String]): Unit = {
    //创建sparkStreaming程序入口
    val conf: SparkConf = new SparkConf().setAppName("demo").setMaster("local[*]")
    val sc = new SparkContext(conf)
    val ssc = new StreamingContext(sc,Seconds(5))
    //设置日志级别
    sc.setLogLevel("WARN")
    //设置检查点,用来保存历史状态
    ssc.checkpoint("./999")
    //接收数据
    val file: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
    //切分
    val spliFile: DStream[String] = file.flatMap(_.split(" "))
    //每个单词记为1次
    val wordAndOne: DStream[(String, Int)] = spliFile.map((_,1))
    //进行有状态转化操作
    val wordAndCount: DStream[(String, Int)] = wordAndOne.updateStateByKey(updateFunc)
    //打印输出
    wordAndCount.print()
    //开启sparkStreaming
    ssc.start()
    //让其一直开启,等待关闭
    ssc.awaitTermination()
  }
}

4.Window

(1)概述

    window操作就是窗口函数。Spark Streaming提供了滑动窗口操作的支持,从而让我们可以对一个滑动窗口内的数据执行计算操作。每次掉落在窗口内的RDD的数据,会被聚合起来执行计算操作,然后生成的RDD,会作为window DStream的一个RDD。比如下图中,就是对每三秒钟的数据执行一次滑动窗口计算,这3秒内的3个RDD会被聚合起来进行处理,然后过了两秒钟,又会对最近三秒内的数据执行滑动窗口计算。所以每个滑动窗口操作,都必须指定两个参数,窗口长度以及滑动间隔,而且这两个参数值都必须是batch间隔的整数倍。Window算子执行如下图1-13所示

 

1. 红色的矩形就是一个窗口,窗口hold的是一段时间内的数据流。

2. 这里面每一个time都是时间单元,在官方的例子中,每个window size是3 time 批次, 而且每隔2个单位时间,窗口会slide一次。

   所以基于窗口的操作,需要指定2个参数:

    window length - The duration of the window (3 in the figure)

    slide interval - The interval at which the window-based operation is performed (2 in the figure). 

3. 窗口大小,是一段时间内数据的容器。

4. 滑动间隔,就是我们多长时间滑动一次。

/**
 * window窗口操作
 *    流式无界,所以我们要向进行全局的统计肯定是行不通的,那么我们可以对这个无界的数据集进行切分,
 *    被切分的这每一个小的区间,我们可以理解为window,
 *    而sparkstreaming是准实时流计算,微小的批次操作,可以理解为是一个特殊的window窗口操作。
 *
 * 理论上,对这个窗口window的划分,有两种情况,一种就是按照数据的条数,另外一种就是按照时间。
 * 但是在sparkstreaming中目前仅支持后者,也就是说仅支持基于时间的窗口,需要提供两个参数
 * 一个参数是窗口的长度:window_length
 * 另外一个参数是窗口的计算频率:sliding_interval ,每隔多长时间计算一次window操作
 *
 * streaming程序中还有一个interval是batchInterval,那这两个interval有什么关系?
 * batchInterval,每隔多长时间启动一个spark作业,而是每隔多长时间为程序提交一批数据
 *
 * 特别需要注意的是:
 *  window_length和sliding_interval都必须是batchInterval的整数倍。
 *
 *  总结:
 *      window操作
 *          每隔M长的时间,去统计N长时间内产生的数据
 *          M被称之sliding_interval,窗口的滑动频率
 *          N被称之window_length,窗口的长度
 *     该window窗口是一个滑动的窗口。
 *
 *   当sliding_interval > window_length的时候,会出现窗口的空隙
 *   当sliding_interval < window_length的时候,会出现窗口的重合
 *   当sliding_interval = window_length的时候,两个窗口是严丝合缝的
 *
 *   batchInterval=2s
 *   sliding_interval=4s
 *   window_length=6s
 */
object  WindowOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setMaster("local[*]")
            .setAppName("WindowOps")
        //两次流式计算之间的时间间隔,batchInterval
        val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
        val ssc = new StreamingContext(conf, batchDuration)
        val lines = ssc.socketTextStream("node01", 9999)
        val words = lines.flatMap(line => line.split("\\s+"))
        val pairs = words.map(word => (word, 1))
        val ret = pairs.reduceByKeyAndWindow(_+_, windowDuration = Seconds(6), slideDuration = Seconds(4))
        ret.print
        ssc.start()
        ssc.awaitTermination()
    }
}

 

四、SparkStreaming高可用及其优化建议

(一)、SparkStreaming缓存操作

SparkStreaming的缓存,说白了就是DStream的缓存,DStream的缓存就只有一个方面,DStream对应的RDD的缓存,RDD如何缓存?rdd.persist(),所以DStream的缓存说白了就是RDD的缓存,使用persist()指定,及其需要指定持久化策略,大多算子默认情况下,持久化策略为MEMORY_ONLY_SER。

(二)、SparkStreaming的checkpoint机制

1、概述

  1. 每一个Spark Streaming应用,正常来说,都是要7*24小时运转的,这就是实时计算程序的特点。因为要持续不断的对数据进行计算。因此,对实时计算应用的要求,应该是必须要能够对与应用程序逻辑无关的失败,进行容错。
  2. 如果要实现这个目标,Spark Streaming程序就必须将足够的信息checkpoint到容错的存储系统上,从而让它能够从失败中进行恢复。有两种数据需要被进行checkpoint:
  • 元数据checkpoint——将定义了流式计算逻辑的信息,保存到容错的存储系统上,比如HDFS。当运行Spark Streaming应用程序的Driver进程所在节点失败时,该信息可以用于进行恢复。元数据信息包括了:
  1. 配置信息——创建Spark Streaming应用程序的配置信息,比如SparkConf中的信息。
  2. DStream的操作信息——定义了Spark Streaming应用程序的计算逻辑的DStream操作信息。
  3. 未处理的batch信息——那些job正在排队,还没处理的batch信息。
  • 数据checkpoint——将实时计算过程中产生的RDD的数据保存到可靠的存储系统中。
  1. 对于一些将多个batch的数据进行聚合的,有状态的transformation操作,这是非常有用的。在这种transformation操作中,生成的RDD是依赖于之前的batch的RDD的,这会导致随着时间的推移,RDD的依赖链条变得越来越长。
  2. 要避免由于依赖链条越来越长,导致的一起变得越来越长的失败恢复时间,有状态的transformation操作执行过程中间产生的RDD,会定期地被checkpoint到可靠的存储系统上,比如HDFS。从而削减RDD的依赖链条,进而缩短失败恢复时,RDD的恢复时间。

   总结,元数据checkpoint主要是为了从driver失败中进行恢复;而RDD checkpoint主要是为了,使用到有状态的transformation操作时,能够在其生产出的数据丢失时,进行快速的失败恢复

2、启动checkpoint

启动方式一

  • 使用了有状态的transformation操作——比如updateStateByKey,或者reduceByKeyAndWindow操作,被使用了,那么checkpoint目录要求是必须提供的,也就是必须开启checkpoint机制,从而进行周期性的RDD checkpoint。
  • 要保证可以从Driver失败中进行恢复——元数据checkpoint需要启用,来进行这种情况的恢复。
  • 要注意的是,并不是说,所有的Spark Streaming应用程序,都要启用checkpoint机制,如果即不强制要求从Driver失败中自动进行恢复,又没使用有状态的transformation操作,那么就不需要启用checkpoint。事实上,这么做反而是有助于提升性能的。
  • 启动方式二
  • 对于有状态的transformation操作,启用checkpoint机制,定期将其生产的RDD数据checkpoint,是比较简单的。可以通过配置一个容错的、可靠的文件系统(比如HDFS)的目录,来启用checkpoint机制,checkpoint数据就会写入该目录。使用StreamingContext的checkpoint()方法即可。然后,你就可以放心使用有状态的transformation操作了。
  • 如果为了要从Driver失败中进行恢复,那么启用checkpoint机制,是比较复杂的。需要改写Spark Streaming应用程序。

当应用程序第一次启动的时候,需要创建一个新的StreamingContext,并且调用其start()方法,进行启动。当Driver从失败中恢复过来时,需要从checkpoint目录中记录的元数据中,恢复出来一个StreamingContext。

​这里针对第二点(重新修改代码)做一说明:

def createFuc():StreamingContext = {
    val ssc = new StreamingContext(conf, batchInterval)
    ssc.checkpoint(checkpoint)
    //业务逻辑
   	.....
    ssc
}
val ssc = StreamingContext.getOrCreate(checkpoint, createFunc)

比如如下代码:

object CheckpointWithKafkaDirectOps {
    def main(args: Array[String]): Unit = {
        Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
        Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
        Logger.getLogger("org.spark_project").setLevel(Level.WARN)
        val conf = new SparkConf()
            .setAppName("CheckpointWithKafkaDirectOps")
            .setMaster("local")
        val duration = Seconds(2)
        val checkpoint = "file:///E:/data/monitored/chk"
        def createFunc():StreamingContext = {
            val ssc = new StreamingContext(conf, duration)
            ssc.checkpoint(checkpoint)
            val kafkaParams = Map[String, Object](
                "bootstrap.servers" -> "node01:9092,node02:9092,node03:9092",
            "key.deserializer" -> classOf[StringDeserializer],
                "value.deserializer" -> classOf[StringDeserializer],
                "group.id" -> "spark-kafka-grou-0817",
                "auto.offset.reset" -> "earliest",
                "enable.auto.commit" -> "false"
            )
            val topics = "spark".split(",").toSet
            val messages:InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc, LocationStrategies.PreferConsistent,
                ConsumerStrategies.Subscribe(topics, kafkaParams))
            messages.foreachRDD((rdd, bTime) => {
                if(!rdd.isEmpty()) {
                    println("num: " + rdd.getNumPartitions)
                    val offsetRDD = rdd.asInstanceOf[HasOffsetRanges]
                    val offsetRanges = offsetRDD.offsetRanges
                    for(offsetRange <- offsetRanges) {
                        val topic = offsetRange.topic
                        val partition = offsetRange.partition
                        val fromOffset = offsetRange.fromOffset
                        val untilOffset = offsetRange.untilOffset                        println(s"topic:${topic}\tpartition:${partition}\tstart:${fromOffset}\tend:${untilOffset}")
                    }
                    rdd.count()
                }
            })
            ssc
        }
        //创建或者恢复出来一个StreamingContext
        val ssc = StreamingContext.getOrCreate(checkpoint, createFunc)
        ssc.start()
        ssc.awaitTermination()

当程序对应的driver失败进行恢复的时候,上述的修改,只是完成了第一步,还有第二步,第三步要走。

第二步,修改spark-submit脚本中的参数:--deploy-mode cluster

第三步,修改spark-submit脚本中的参数:--supervise

3.DriverHA

(1)DriverHA的原理

由于流计算系统是长期运行、且不断有数据流入,因此其Spark守护进程(Driver)的可靠性至关重要,它决定了Streaming程序能否一直正确地运行下去。

 

Driver实现HA的解决方案就是将元数据持久化,以便重启后的状态恢复。如图一所示,Driver持久化的元数据包括:

1:蓝色的箭头表示接收的数据,接收器把数据流打包成块,存储在executor的内存中,如果开启了WAL,将会把数据写入到存在容错文件系统的日志文件中

2:青色的箭头表示提醒driver, 接收到的数据块的元信息发送给driver中的StreamingContext, 这些元数据包括:executor内存中数据块的引用ID和日志文件中数据块的偏移信息

3:红色箭头表示处理数据,每一个批处理间隔,StreamingContext使用块信息用来生成RDD和jobs.SparkContext执行这些job用于处理executor内存中的数据块

4:黄色箭头表示checkpoint这些计算,以便于恢复。流式处理会周期的被checkpoint到文件中

   恢复计算(图1-17中的橙色箭头):使用Checkpoint数据重启driver,重新构造上下文并重启接收器。恢复元数据块(图2中的绿色箭头):恢复Block元数据。

通过如上的数据备份和恢复机制,Driver实现了故障后重启、依然能恢复Streaming任务而不丢失数据,因此提供了系统级的数据高可靠

(2)DriverHA的配置

#!/bin/sh

SPARK_HOME=/export/servers/spark

$SPARK_HOME/bin/spark-submit \

--master spark://node01:7077 \

--deploy-mode cluster \

--class chapter4.Advertising_ranking \

--executor-memory 600M \

--executor-cores 2 \

--driver-cores 1 \

--supervise \

--total-executor-cores 3 \

hdfs://node01:8020/wordcount/input/wc.jar \

2 node01 9999 \

hdfs://node01:8020/wordcount/input/word1.txt

(3)DriverHA的实现

object SparkStreamingDriverHAOps {

    def main(args: Array[String]): Unit = {

        Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)

        Logger.getLogger("org.apache.spark").setLevel(Level.WARN)

        Logger.getLogger("org.spark_project").setLevel(Level.WARN)

        if(args == null || args.length < 4) {

            System.err.println(

                """

                  |Parameter Errors! Usage: <batchInterval> <host> <port> <checkpoint>

                """.stripMargin)

            System.exit(-1)

        }

        val Array(batchInterval, host, port, checkpoint) = args

        val conf = new SparkConf()

            .setAppName("SparkStreamingDriverHA")

            .setMaster("local[*]")

        def createFunc():StreamingContext = {

            val ssc = new StreamingContext(conf, Seconds(batchInterval.toLong))

            ssc.checkpoint(checkpoint)

            val lines:DStream[String] = ssc.socketTextStream(host, port.toInt)

            val pairs:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1))

            val usb:DStream[(String, Int)] = pairs

                .updateStateByKey((seq, option) => Option(seq.sum + option.getOrElse(0)))

            usb.print()

            ssc

        }

        val ssc = StreamingContext.getOrCreate(checkpoint, createFunc)

        ssc.start()

        ssc.awaitTermination()

    }

}

(三)、SparkStreaming程序的部署、升级与维护

1、Spark程序的部署

1、我们之前讲过的部署方式,一般都是直接往Spark standalone集群、yarn集群和mesos集群部署应用。

2、为executor配置充足的内存,因为Receiver接受到的数据,默认是要存储在Executor的内存中的,所以Executor必须配置足够的内存来保存接收到的数据。要注意的是,如果你要执行窗口长度为30分钟的窗口操作,那么Executor的内存资源就必须足够保存30分钟内的数据,因此内存的资源要求是取决于你执行的操作的。

3、配置checkpoint,如果你的应用程序要求checkpoint操作,那么就必须配置一个Hadoop兼容的文件系统(比如HDFS)的目录作为checkpoint目录.

4、配置driver的HA自动恢复,如果要让driver能够在失败时自动恢复,之前已经讲过,一方面,要重写driver程序,一方面,要在spark-submit中添加参数。

Driver HA机制两种方式 1) 程序重写HA 2)借助Spark集群进行维护

2、WAL(预写日志)

预写日志机制,简写为WAL,全称为Write Ahead Log。从Spark 1.2版本开始,就引入了基于容错的文件系统的WAL机制。如果启用该机制,Receiver接收到的所有数据都会被写入配置的checkpoint目录中的预写日志。这种机制可以让driver在恢复的时候,避免数据丢失,并且可以确保整个实时计算过程中,零数据丢失。

配置方式:

1)StreamingContext 设置 checkpoint() 一个checkpoint目录。

2)spark.streaming.receiver.writeAheadLog.enable参数设置为true。

优化方式:

然而,这种极强的可靠性机制,会导致Receiver的吞吐量大幅度下降,因为单位时间内,有相当一部分时间需要将数据写入预写日志。如果又希望开启预写日志机制,确保数据零损失,又不希望影响系统的吞吐量,那么可以创建多个输入DStream,启动多个Receiver。

建议,在启用了预写日志机制之后,推荐将复制持久化机制禁用掉,因为所有数据已经保存在容错的文件系统中了,不需要在用复制机制进行持久化,保存一份副本了。只要将输入DStream的持久化机制设置一下即可,persist(StorageLevel.MEMORY_AND_DISK_SER)。

3、Receiver

1、spark.streaming.receiver.maxRate

和spark.streaming.kafka.maxRatePerPartition参数可以用来设置,前者设置普通Receiver,后者是Kafka Direct方式。

2、Spark 1.5中,对于Kafka Direct方式,引入了backpressure机制,从而不需要设置Receiver的限速,Spark可以自动估计Receiver最合理的接收速度,并根据情况动态调整。只要将spark.streaming.backpressure.enabled设置为true即可。

3、企业级内部使用场景一般都是Kafka Direct方式,优点:

1)、不用receiver,不会独占集群的一个cpu core;

2)、有backpressure自动调节接收速率的机制;

4、升级SparkStreaming应用程序在线上

大家知道,线上的Spark Streaming应用程序都是7 * 24 * 30小时运行的。因此如果需要对正在运行的应用程序,进行代码的升级,那么有两种方式可以实现:

1、并行: 也就是升级后的Spark应用程序与旧的Spark应用程序并行,当新的应用程序没有问题时,才可以将旧的替换掉。这种方式适合于客户单独拉取自己的数据。

2、必须有缓存系统保存数据才可以,启动新的应用程序。

Checkpoint目录不能共享

注意:配置了driver自动恢复机制时,如果想要根据旧的应用程序的checkpoint信息,启动新的应用程序,是不可行的。需要让新的应用程序针对新的checkpoint目录启动,或者删除之前的checkpoint目录

5、监控Spark应用程序

Spark Web UI会显示一个独立的streaming tab,会显示Receiver的信息,比如是否活跃,接收到了多少数据,是否有异常等;还会显示完成的batch的信息,batch的处理时间、队列延迟等。这些信息可以用于监控spark streaming应用的进度。

Spark UI中,以下两个统计指标格外重要:

1、处理时间——每个batch的数据的处理耗时—直监控每个batch是否延迟。

2、调度延迟——一个batch在队列中阻塞住,等待上一个batch完成处理的时间

如果batch的处理时间,比batch的间隔要长的话,而且调度延迟时间持续增长,应用程序不足以使用当前设定的速率来处理接收到的数据,此时,可以考虑增加batch的间隔时间。

(四)、SparkStreaming优化建议

1、设置合理的CPU

很多情况下Streaming程序需要的内存不是很多,但是需要的CPU要很多。在Streaming程序中,CPU资源的使用可以分为两大类:

(1)、用于接收数据;

(2)、用于处理数据。我们需要设置足够的CPU资源,使得有足够的CPU资源用于接收和处理数据,这样才能及时高效地处理数据。

2、关于接收数据的调优说明

1、通过网络接收数据时(比如Kafka、Flume、ZMQ、RocketMQ、RabbitMQ和ActiveMQ等),会将数据反序列化,并存储在Spark的内存中。

2、如果数据接收成为系统的瓶颈,那么可以考虑并行化数据接收。每一个输入DStream都会在某个Worker的Executor上启动一个Receiver,该Receiver接收一个数据流。因此可以通过创建多个输入DStream,并且配置它们接收数据源不同的分区数据,达到接收多个数据流的效果。

3、举例说明:一个接收4个Kafka Topic的输入DStream,可以被拆分为两个输入DStream,每个分别接收二个topic的数据。这样就会创建两个Receiver,从而并行地接收数据,进而提升吞吐量。多个DStream可以使用union算子进行聚合,从而形成一个DStream。然后后续的transformation算子操作都针对该一个聚合后的DStream即可。

4、使用inputStream.repartition(<number of partitions>)即可。这样就可以将接收到的batch,分布到指定数量的机器上,然后再进行进一步的操作。

5、数据接收并行度调优,除了创建更多输入DStream和Receiver以外,还可以考虑调节block interval。通过参数,spark.streaming.blockInterval,可以设置block interval,默认是200ms。对于大多数Receiver来说,在将接收到的数据保存到Spark的BlockManager之前,都会将数据切分为一个一个的block。而每个batch中的block数量,则决定了该batch对应的RDD的partition的数量,以及针对该RDD执行transformation操作时,创建的task的数量。每个batch对应的task数量是大约估计的,即batch interval / block interval

举个例子

1)、batch interval为3s,block interval为150ms,会创建20个task。如果你认为每个batch的task数量太少,即低于每台机器的cpu core数量,那么就说明batch的task数量是不够的,因为所有的cpu资源无法完全被利用起来。要为batch增加block的数量,那么就减小block interval

2)、推荐的block interval最小值是50ms,如果低于这个数值,那么大量task的启动时间,可能会变成一个性能开销点。

3、设置合理的并行度

如果在计算的任何stage中使用的并行task的数量没有足够多,那么集群资源是无法被充分利用的。举例来说,对于分布式的reduce操作,比如reduceByKey和reduceByKeyAndWindow,默认的并行task的数量是由spark.default.parallelism参数决定的。你可以在reduceByKey等操作中,传入第二个参数,手动指定该操作的并行度,也可以调节全局的spark.default.parallelism参数

该参数说的是,对于那些shuffle的父RDD的最大的分区数据。对于parallelize或者textFile这些输入算子,因为没有父RDD,所以依赖于ClusterManager的配置。如果是local模式,该默认值是local[x]中的x;如果是mesos的细粒度模式,该值为8,其它模式就是Math.max(2, 所有的excutor上的所有的core的总数)。

4、序列化调优说明

数据序列化造成的系统开销可以由序列化格式的优化来减小。在流式计算的场景下,有两种类型的数据需要序列化。

1、输入数据:默认情况下,接收到的输入数据,是存储在Executor的内存中的,使用的持久化级别是StorageLevel.MEMORY_AND_DISK_SER_2。这意味着,数据被序列化为字节从而减小GC开销,并且会复制以进行executor失败的容错。因此,数据首先会存储在内存中,然后在内存不足时会溢写到磁盘上,从而为流式计算来保存所有需要的数据。这里的序列化有明显的性能开销——Receiver必须反序列化从网络接收到的数据,然后再使用Spark的序列化格式序列化数据。

2、流式计算操作生成的持久化RDD:流式计算操作生成的持久化RDD,可能会持久化到内存中。例如,窗口操作默认就会将数据持久化在内存中,因为这些数据后面可能会在多个窗口中被使用,并被处理多次。然而,不像Spark Core的默认持久化级别,StorageLevel.MEMORY_ONLY,流式计算操作生成的RDD的默认持久化级别是StorageLevel.MEMORY_ONLY_SER ,默认就会减小GC开销。

在上述的场景中,使用Kryo序列化类库可以减小CPU和内存的性能开销。使用Kryo时,一定要考虑注册自定义的类,并且禁用对应引用的tracking(spark.kryo.referenceTracking)。

5、batchInterval

 如果想让一个运行在集群上的Spark Streaming应用程序可以稳定,它就必须尽可能快地处理接收到的数据。换句话说,batch应该在生成之后,就尽可能快地处理掉。对于一个应用来说,这个是不是一个问题,可以通过观察Spark UI上的batch处理时间来定。batch处理时间必须小于batch interval时间。

在构建StreamingContext的时候,需要我们传进一个参数,用于设置Spark Streaming批处理的时间间隔。Spark会每隔batchDuration时间去提交一次Job,如果你的Job处理的时间超过了batchDuration的设置,那么会导致后面的作业无法按时提交,随着时间的推移,越来越多的作业被拖延,最后导致整个Streaming作业被阻塞,这就间接地导致无法实时处理数据,这肯定不是我们想要的。

另外,虽然batchDuration的单位可以达到毫秒级别的,但是经验告诉我们,如果这个值过小将会导致因频繁提交作业从而给整个Streaming带来负担,所以请尽量不要将这个值设置为小于500ms。在很多情况下,设置为500ms性能就很不错了。

那么,如何设置一个好的值呢?我们可以先将这个值设置为比较大的值(比如10S),如果我们发现作业很快被提交完成,我们可以进一步减小这个值,直到Streaming作业刚好能够及时处理完上一个批处理的数据,那么这个值就是我们要的最优值。

6、内存调优

 内存调优的另外一个方面是垃圾回收。对于流式应用来说,如果要获得低延迟,肯定不想因为JVM垃圾回收导致的长时间延迟。有很多参数可以帮助降低内存使用和GC开销:

 1、DStream的持久化:正如在“数据序列化调优”一节中提到的,输入数据和某些操作生产的中间RDD,默认持久化时都会序列化为字节。与非序列化的方式相比,这会降低内存和GC开销。使用Kryo序列化机制可以进一步减少内存使用和GC开销。进一步降低内存使用率,可以对数据进行压缩,由spark.rdd.compress参数控制(默认false)。

 2、清理旧数据:默认情况下,所有输入数据和通过DStream transformation操作生成的持久化RDD,会自动被清理。Spark Streaming会决定何时清理这些数据,取决于transformation操作类型。例如,你在使用窗口长度为10分钟内的window操作,Spark会保持10分钟以内的数据,时间过了以后就会清理旧数据。但是在某些特殊场景下,比如Spark SQL和Spark Streaming整合使用时,在异步开启的线程中,使用Spark SQL针对batch RDD进行执行查询。那么就需要让Spark保存更长时间的数据,直到Spark SQL查询结束。可以使用streamingContext.remember()方法来实现。

3、CMS垃圾回收器:使用并行的mark-sweep垃圾回收机制,被推荐使用,用来保持GC低开销。虽然并行的GC会降低吞吐量,但是还是建议使用它,来减少batch的处理时间(降低处理过程中的gc开销)。如果要使用,那么要在driver端和executor端都开启。在spark-submit中使用--driver-java-options设置;使用spark.executor.extraJavaOptions参数设置。-XX:+UseConcMarkSweepGC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值