定义

SparkStreaming应用程序中有很多能够优化的地方,这样的优化可以提高应用的运行效率。

减少批处理的时间

SparkStreaming的优化可以大大提高每个批次的处理时间,每个批次处理其实就是操作RDD,那么怎么样才能缩短操作RDD的时间呢,那我们就可以参考Spark Core(十九)Spark性能的调优来尽可能的减少批处理的时间。

增加接收数据的并行度

应用程序在处理网络传输过来的数据时候需要反序列化后存储在Spark内存中,如果数据的接受成为了应用程序性能的瓶颈,我们可以增加接受数据的并行性。默认情况下一个
只对应一个Receiver接收器,那么我们就可以为一个DStream增加多个Receiver接收器,因为一个接收器只会消费一个分区上得数据,这样我们提高并行性其实也就是同时消费了多个分区上的数据。

package com.lyz.streaming.datasource.advance

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.KafkaUtils
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferFixed

import scala.collection.immutable

object anyReceiverTest {

  def main(args: Array[String]): Unit = {
    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "localhost:9092", //指定Kafka的集群地址
      "key.deserialize" -> classOf[StringDeserializer], //指定key的反序列化器
      "value.deserialize" -> classOf[StringDeserializer], //指定值的反序列化器
      "group.id" -> "", //consumer的分组id
      "auto.offset.reset" -> "latest", //从新定义消费者以后,不从头消费分区里的数据,只消费定义消费者以后的数据
      "enable.auto.commit" -> (false: java.lang.Boolean) //是否自动提交offsets,也就是更新kafka里的offset,表示已经被消费过了
    )

    //定义消费主题topic
    val topics = Array("topic1", "topic2")

    val sparkConf: SparkConf = new SparkConf().setMaster("local[3]").setAppName("KafkaDatasourceTest")
    val streamingContext: StreamingContext = new StreamingContext(sparkConf, Seconds(2))
    //禁用消费者预先缓存分区上的消息
    sparkConf.set("spark.streaming.kafka.consumer.cahce.enable", "false")

    //设置接受kafka数据的并行度
    val numReceiver = 5
    //利用KafkaUtils创建多个接收器
    val kafkaStreams: immutable.IndexedSeq[InputDStream[ConsumerRecord[String, String]]] = (1 to numReceiver).map(i => {
      /**
        * PreferConsistent:将分区分布在所有的Excutor上,也就是每Executor都会消费Kafka数据
        * PreferBrokers:只有当执行程序与Kafka代理程序位于相同的节点时,才可以使用。
        * Subscribe:传入具体的kafka参数
        */
      KafkaUtils.createDirectStream[String, String](
        streamingContext,
        PreferFixed(Map[TopicPartition, String]()),
        Subscribe[String, String](topics, kafkaParams))
    })

    · //合并多个接收器
    val stream: DStream[ConsumerRecord[String, String]] = streamingContext.union(kafkaStreams)
  }
}

合理设置接收器接受数据的块间隔

当接收器接受数据以后会合并成数据块然后再存储到Spark内存中,既然是将数据合并成数据块,那么多久合并一次呢,这里就引入了一个配置参数叫块间隔,由spark.streaming.blockInterval设置。每个批次的数据块数量决定了程序处理的任务数(批处理间隔时间/块间隔),我们都知道计算任务的并行度是根据Executor上的Core来决定的,如果任务数量小于Core数量,那么就需要缩短块间隔,提高任务的并行度。需要注意的是块间隔建议最小值为50ms,不建议低于这个值。

数据处理的并行度

如果在程序的任何阶段使用的任务数量不高,可能造成资源的不充分利用,我们可以设置spark.default.parallesim参数设置任务的并行度。

数据系列化

  • 数据序列化,通过Receiver接受到的数据以StorageLevel.MEMORY_AND_SER2方式进行持久化的,也就是说先对数据进行序列化,这样减少了GC的开销,然后缓存到内存中,当内存达到了最大缓存量,就把剩余的序列化数据溢写到磁盘中。这种序列化方式显然性能收到很大的影响,因为我们在处理缓存数据的时候需要对其进行反序列化。
  • 流式处理得到需要持久化的RDD,在流式计算中,产生的中间RDD会对其进行持久化,而与SparkCore中持久化RDD的级别StorageLeval.MEMORY_ONLY不同,流式计算持久化RDD的级别是StorageLeval.MEMORY.ONLY.SER,而是对RDD进行序列化以减少GC的开销。
    对于以上两种情况我们都可以使用Kryo序列化器来介绍CPU和内存的开销。使用Kryo序列化器详解请点击该链接Kryo序列化器配置

正确设置批处理时间间隔

SparkStreaming按时间间隔来进行批次的处理,如果想要保持系统的稳定性,那么处理数据的速度要跟的上接受数据的速度。换句话说就是数据的处理速度要小于数据接受的速度。
确定应用程序稳定的批处理的时间间隔应该是保守的,我们可以查看sparklog4j日志里的总延迟,如果延迟在允许范围内系统就是稳定的,如果延迟过高,那么就需要进行反复测试进行最优的时间间隔。

内存调优

  • 调优指南Spark应用程序调优
  • 中已经详细的介绍了Spark应用程序的调优。这里我们针对SparkStreaming的应用程序进行调优的介绍。
  • SparKStreaming应用程序消耗内存大小取决于数据的转换类型,例如窗口操作那么就需要根据窗口的小来确定内存的大小,如果用到的updateStateByKey函数,那么就就需要更大的内存。
  • 由于接受到的数据的持久化方式为StorageLevel.MEMORY_AND_DIS_SER2方式,也就是将数据序列化后,将数据存在内存中,内存放不下的数据就会溢写到磁盘上,当处理这批数据的时候就需要反序列化该数,这绝对影响性能,这样我们可以设置数据的持久化方式
  • DStream的持久化:默认持久化方式为先序列化在进行持久化,这样减少内存的开销和GC的开销。也可以使用对数据进行压缩,由spark.rdd.compress参数控制,默认是false
  • 清除旧数据SparkStreaming默认清除旧数据的时间是根据窗口长度决定的,清除旧数据的时间我们可以利用StreamingContext.remember()来实现
  • CMS垃圾回收器:强烈推荐使用mark-sweep并行垃圾回收器,用来保证GC的开销。虽然并行垃圾回收器会降低系统的吞吐量,但是它可以减少batch的处理时间。这个需要在Driver端和Executor端都要设置。在Driver端通过spark-submit中的--driver-java-options参数来设置,在Excutor端需要用spark.excutor.extralJavaOptions配置参数。
./bin/spark-submit --name "My app" --master local[4] --conf spark.eventLog.enabled=false
  --conf "spark.executor.extraJavaOptions=-XX:+PrintGCDetails -XX:+PrintGCTimeStamps" myApp.jar

具体的详细配置请参考官网Spark参数配置