swift x输入流_大数据框架Spark的流处理SparkStreaming详细总结

《大数据和人工智能交流》头条号向广大初学者新增C 、Java 、Python 、Scala、javascript 等目前流行的计算机、大数据编程语言,希望大家以后关注本头条号更多的内容。

1、流处理介绍

流数据是一组顺序、大量、快速、连续到达的数据序列,一般情况下,数据流可被视为一个随时间延续而无限增长的动态数据集合,应用于网络监控、传感器网络、航空航天、气象测控和金融服务等领域。

流处理是一种大数据处理技术,用于处理连续数据流,并能在收到数据短时间内快速检测出异常条件,检测时间从几毫秒到几分钟不等。例如,通过流处理查询来自温度传感器的数据流,您可以在温度达到一定的阈值的时候收到报警。流处理还有许多其他叫法:实时分析、流分析、复杂事件处理、实时流分析和事件处理。尽管某些术语历史上存在差异,但现在工具(框架)已经在流处理术语下趋于一致。

根据数据处理的时效性,大数据处理系统可分为批式(batch)大数据和流式(streaming)大数据两类。其中,批式大数据又被称为历史大数据,流式大数据又被称为实时大数据。

大数据技术就是处理海量数据并获取其中的价值,但这些价值并非完全一样。一些数据在发生后不久更有价值并随着时间推移其价值迅速下降。流处理支持这样的场景,提供更快的有价值信息,通常在从触发器开始的几毫秒到几秒内。

2、为什么使用流处理

(1)有些数据天然地作为无止尽事件流出现。如果进行批处理,需要先存储起来,在某个时间点停止收集来处理这些数据,然后您需要执行下一个批处理并考虑跨多个批次进行聚合。相比之下,流式处理能自然优雅地处理无止尽数据流,您可以检测模式、检查结果、多级别聚焦观察、还可以轻松地同时观察来自多个数据流。

(2)流处理天然地适合时间序列数据和随时间变化的模式检测。例如,如果您试图检测无止尽流中Web会话的长度则很难用批处理来检测,因为某些会话将被分割到两个不同的批处理中。流处理可以很容易地处理这种问题。你退一步想想,最连续的数据序列就是时间序列数据。举个例子,几乎所有的物联网数据都是时间序列数据,因此使用合理的编程模型是非常有意义的。

(3)批处理需要准备数据并尝试一次性处理这些数据,而流处理则在数据到来时处理它们,并随着时间推移连续地进行处理。所以流处理可以比批处理少用很多硬件,而且流处理还可以通过甩负载实现近似查询处理,所以流处理天然地适合那些近似结果就能满足需求的场景。

(4)有时候因数据太大导致无法存储,流处理能让你处理大数据并只保留有用的数据。

(5)有很多有用的流数据(例如客户交易、活动、网站访问),并且流数据随着物联网(IoT)各种传感器的广泛应用会更快地增长。

3、常用的流处理框架简介

(1)Apache Storm

Apache Storm是一个分布式实时大数据处理系统。Storm设计用于在容错和水平可扩展方法中处理大量数据。它是一个流数据框架,具有最高的摄取率。虽然Storm是无状态的,它通过Apache ZooKeeper管理分布式环境和集群状态。它很简单,您可以并行地对实时数据执行各种操作。Apache Storm 易于设置和操作,并且它保证每个消息将通过拓扑至少处理一次。

(2)Spark Streaming

Spark Streaming 是Spark核心API的一个扩展,可以实现高吞吐量的、具备容错机制的实时流数据的处理。支持从多种数据源获取数据,包括Kafk、Flume、Twitter、ZeroMQ、Kinesis 以及TCP sockets,从数据源获取数据之后,可以使用诸如map、reduce、join和window等高级函数进行复杂算法的处理。最后还可以将处理结果存储到文件系统,数据库和现场仪表盘。在"One Stack rule them all"的基础上,还可以使用Spark的其他子框架,如机器学习、图计算等,对流数据进行处理。

(3)Apache Flink

Apache Flink是由Apache软件基金会开发的开源流处理框架,其核心是用Java和Scala编写的分布式流数据流引擎。Flink以数据并行和流水线方式执行任意流数据程序,Flink的流水线运行时系统可以执行批处理和流处理程序。此外,Flink的运行时本身也支持迭代算法的执行。

4、Spark Streaming

(1)什么是Spark Streaming

1baefdc3ea1e6b88323ad6a254bb30af.png

在内部,其按如下方式运行。Spark Streaming接收到实时数据流同时将其划分为分批,这些数据的分批将会被Spark的引擎所处理从而生成同样按批次形式的最终流。

4b464599e304f42642fc9ab31ec7a7a5.png

Spark Streaming提供了被称为离散化流或者DStream的高层抽象,这个高层抽象用于表示数据的连续流。

创建DStream的两种方式:由Kafka,Flume取得的数据作为输入数据流;在其他DStream进行的高层操作。

在内部,DStream被表达为RDDs的一个序列。Spark Streaming,其实就是一种Spark提供的,对于大数据,进行实时计算的一种框架。它的底层,其实,也是基于我们之前讲解的Spark Core的。基本的计算模型,还是基于内存的大数据实时计算模型。而且,它的底层的核心组件还是我们在Spark Core中经常用到的RDD。针对实时计算的特点,在RDD之上,进行了一层封装,叫做DStream。其底层还是基于RDD的。所以,RDD是整个Spark技术生态中的核心。

   Spark streaming支持的数据输入源很多,如:Kafka、Flume、Twitter、ZeroMQ 和简单的 TCP 套接字等等。数据输入后可以用spark的高度抽象语:map、reduce、join、window 等进行运算。而结果也能保存在很多地方。如HDFS, 数据库等。另外,spark streaming也能和MLlib(机器学习)以及 Graphx 完美融合。

(2)为什么要用Spark Streaming

   Hadoop 的 MapReduce 及 Spark SQL 等只能进行离线计算,无法满足实时性要求较高的业务 需求,例如实时推荐、实时网站性能分析等,流式计算可以解决这些问题。目前有三种比较 常用的流式计算框架,它们分别是 Storm,Spark Streaming 和 fink。

(3)对比Spark Streaming与Storm的应用场景:

对于Storm来说,纯实时数据流处理,不能忍受1秒以上延迟的场景下使用,比如实时金融系统,要求纯实时进行金融交易和分析;对于实时计算的功能中,要求可靠的事务机制和可靠性机制,即数据的处理完全精准,一条也不能多,一条也不能少,也可以考虑使用Storm;如果还需要针对高峰低峰时间段,动态调整实时计算程序的并行度,以最大限度利用集群资源,也可以考虑用Storm;如果一个大数据应用系统,它就是纯粹的实时计算,不需要在中间执行SQL交互式查询、复杂的transformation算子等,那么用Storm是比较好的选择

对于Spark Streaming来说,1、不要求纯实时,不要求强大可靠的事务机制,不要求动态调整并行度,那么可以考虑使用Spark Streaming;如果一个项目除了实时计算之外,还包括了离线批处理、交互式查询等业务功能,而且实时计算中,可能还会牵扯到高延迟批处理、交互式查询等功能,那么就应该首选Spark生态,用Spark Core开发离线批处理,用Spark SQL开发交互式查询,用Spark Streaming开发实时计算,三者可以无缝整合,给系统提供非常高的可扩展性。

下面举个大名鼎鼎的WordCount的例子,让大家感受下SparkStreaming程序结构:

object StreamingWordCount {

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

val sc = new StreamingContext("local", "WordCount", Seconds(2) )

val lines = sc.socketTextStream("192.168.10.100", 8888)

//每一行数据分割按空格成单词

val words = lines.flatMap(_.split(" "))

// 在本批次内计单词的数目

val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)

// 打印每个RDD中的前10个元素到控制台

wordCounts.print()

sc.start()

sc.awaitTermination()

}

}

5、SparkStreaming中的对象

(1)StreamingContext对象

StreamingContext是所有流功能的主要入口点。要初始化Spark Streaming程序,必须创建一个StreamingContex,现定一个StreamingContex对象:

val conf = new SparkConf().setMaster("local[2]").setAppName("NetWorkWordCount")

val ssc = new Sttreaming Context(conf, Seconds(1))

appName参数是应用程序在集群UI上显示的名称。 master是Spark,Mesos或YARN群集URL,或者是在本地模式下运行的特殊"local [*]"字符串。

实际上,在群集上运行时,您不希望在程序中对master进行硬编码,而是使用spark-submit启动应用程序并在那里接收它。但是,对于本地测试和单元测试,您可以传递"local [*]"以在进程中运行Spark Streaming(检测本地系统中的核心数)。 请注意,这会在内部创建一个SparkContext,可以作为ssc.sparkContext访问。

定义StreamingContext上下文对象后,您必须执行以下操作:

【1】通过创建DStream来定义输入源。

【2】通过对DStream使用转换和输出操作来定义流计算。

【3】 使用streamingContext.start()来接收并处理数据

【4】使用streamingContext.awaitTermination()等待处理的停止(手动或者因为任何出错).

【5】处理进程可以使用streamingContext.stop()来手动停止。

注意事项:

【1】一旦streamingContext启动,就不能再对其计算逻辑进行添加或修改。

【2】一旦streamingContext被stop掉,就不能restart。

【3】单个JVM虚机同一时间只能包含一个active的StreamingContext。

【4】StreamingContext.stop() 也会把关联的SparkContext对象stop掉,如果不想把SparkContext对象也stop掉,可以将StreamingContext.stop的可选参数 stopSparkContext 设为false。

【5】一个SparkContext对象可以和多个StreamingContext对象关联,只要先对前一个StreamingContext.stop(sparkContext=false),然后再创建新的StreamingContext对象即可。

(2)Dstreams对象

离散数据流或者DStream是SS提供的基本抽象。其表现数据的连续流,这个输入数据流可以来自于源,也可以来自于转换输入流产生的已处理数据流。内部而言,一个DStream以一系列连续的RDDs所展现,这些RDD是Spark对于不变的,分布式数据集的抽象。一个DStream中的每个RDD都包含来自一定间隔的数据,如下图:

35dd6360db9defd86515a69df549f943.png

在DStream上使用的任何操作都会转换为针对底层RDD的操作。例如:之前那个将行的流转变为词流的例子中,flatMap操作应用于行DStream的每个RDD上 从而产生words DStream的RDD。如下图:

887223f78eb945de34c4891029e305db.png

这些底层的RDD转换是通过Spark引擎计算的。DStream操作隐藏了大多数细节,同时为了方便为开发者提供了一个高层的API。

6、输入DStream和接收器

输入DStream代表从某种流式数据源流入的数据流。在之前的例子里,lines 对象就是输入DStream,它代表从netcat server收到的数据流。每个输入DStream(除文件数据流外)都和一个接收器(Receiver)相关联,而接收器则是专门从数据源拉取数据到内存中的对象。

Spark Streaming主要提供两种内建的流式数据源:

(1) 基础数据源(Basic sources): 在StreamingContext API 中可直接使用的源,如:文件系统,套接字连接或者Akka actor。

(2)高级数据源(Advanced sources): 需要依赖额外工具类的源,如:Kafka、Flume、Kinesis、Twitter等数据源。

如果本地运行Spark Streaming应用,记得不能将master设为"local" 或 "local[1]"。这两个值都只会在本地启动一个线程。而如果此时你使用一个包含接收器(如:套接字、Kafka、Flume等)的输入DStream,那么这一个线程只能用于运行这个接收器,而处理数据的逻辑就没有线程来执行了。因此,本地运行时,一定要将master设为"local[n]",其中 n > 接收器的个数(有关master的详情请参考Spark Properties)。

将Spark Streaming应用置于集群中运行时,同样,分配给该应用的CPU core数必须大于接收器的总数。否则,该应用就只会接收数据,而不会处理数据。

7、DStream支持的转换算子

和RDD类似,DStream也支持从输入DStream经过各种转换算子映射成新的DStream。DStream支持很多RDD上常见的transformation算子,一些常用的见下表:

updateStateByKey 算子支持维护一个任意的状态。要实现这一点,只需要两步:

【1】定义状态 – 状态数据可以是任意类型。

【2】定义状态更新函数 – 定义好一个函数,其输入为数据流之前的状态和新的数据流数据,且可其更新步骤1中定义的输入数据流的状态。

在每一个批次数据到达后,Spark都会调用状态更新函数,来更新所有已有key(不管key是否存在于本批次中)的状态。如果状态更新函数返回None,则对应的键值对会被删除。

例如: 统计数据流中每个单词的出现次数。这里将各个单词的出现次数这个整型数定义为状态。我们接下来定义状态更新函数如下:

def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {

val newCount = ... // 将新的计数值和之前的状态值相加,得到新的计数值

Some(newCount)

}

该状态更新函数可以作用于一个包括(word, 1) 键值对的DStream上。

8、基于窗口(window)的算子

Streaming同样也提供基于时间窗口的计算,也就是说,你可以对某一个滑动时间窗内的数据施加特定转换算子。如下图所示:

4fe57c7e27a017165253e7abc2cc7850.png

红色的矩形就是一个窗口:一段时间内的数据流,这里面每一个time都是时间单元。

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

(1)窗口大小:是一段时间内数据流

(2)滑动间隔

Spark Streaming有特定的窗口操作,窗口操作涉及两个参数:一个是滑动窗口的宽度(Window Duration);另一个是窗口滑动的频率(Slide Duration),这两个参数必须是batch size的倍数。例如以过去5秒钟为一个输入窗口,每1秒统计一下WordCount,那么我们会将过去5秒钟的每一秒钟的WordCount都进行统计,然后进行叠加,得出这个窗口中的单词统计。

val wordCounts = words.map(x => (x, 1)).reduceByKeyAndWindow(_ + _, Seconds(5s),seconds(1))

但上面这种方式还不够高效。如果我们以增量的方式来计算就更加高效,例如,计算t+4秒这个时刻过去5秒窗口的WordCount,那么我们可以将t+3时刻过去5秒的统计量加上[t+3,t+4]的统计量,在减去[t-2,t-1]的统计量(如图5所示),这种方法可以复用中间三秒的统计量,提高统计的效率。

val wordCounts = words.map(x => (x, 1)).reduceByKeyAndWindow(_ + _, _ - _, Seconds(5s),seconds(1))

c31940f17b4564bab3b0fa2115ba9600.png

以下列出了常用的窗口算子。所有这些算子都有前面提到的那两个参数:窗口长度 和 滑动距离。

9、DStream输出算子

输出算子可以将DStream的数据推送到外部系统,如:数据库或者文件系统。因为输出算子会将最终完成转换的数据输出到外部系统,因此只有输出算子调用时,才会真正触发DStream 转换算子的真正执行,这一点类似于RDD 的action算子。目前所支持的输出算子如下表:

10、SparkStreaming的案例

示例-1:使用SparkStreaming处理HDFS上的文件

object HDFSWordCount{

val sparkConf = new SparkConf().setAppName("HDFSWordCount").setMaster("local[2]")

//创建StreamingContext

val ssc = new StreamingContext(sparkConf , Seconds(2))

//创建FileInputDStream去读文件系统上的数据

val lines = ssc.textFileStream("hdfs://192.168.10.100:9000/ss/data/input")

val words = lines.flatMap(_.split(" "))

val wordCount = words.map( x =>(x,1)).reduceByKey(_+_)

wordCount.print()

ssc.start()

ssc.awaitTermination()

}

测试的时候,将文件上传hdfs://192.168.10.100:9000/ss/data/input,文件名不能重复。

示例-2:对日志文件进行单词计数

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Seconds,StreamingContext}

import org.apache.spark.streaming.dstream.DStream.toPairDStreamFunctions

object Spark_Stream001 {

def main(args:Array[String]){

//1、建立配置对象

val conf = new SparkConf().setAppName("StreamingD01").setMaster("local")

//2、建立StreamingContext对象,第一个参数日志对象,

//第二个参数时间片:每隔20秒统计数据信息

val sc = new StreamingContext(conf,Seconds(20))

//3、定时监控HDFS的mylog下的日志信息

val dstream = sc.textFileStream("hdfs://706960b475f1:9000/mylog")

//4、对日志文件进行单词计数

dstream.flatMap(_.split("")).map((_,1)).reduceByKey(_ + _).print(10)

//5、开启监控

sc.start

sc.awaitTermination

sc.stop(true)

}

}

示例-3:滑动窗口的例子

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Seconds,StreamingContext}

import org.apache.spark.streaming.dstream.DStream.toPairDStreamFunctions

object Spark_Stream002 {

def main(args:Array[String]){

//1、建立配置对象

val conf = new SparkConf().setAppName("StreamingD01").setMaster("local")

//2、建立StreamingContext对象,第一个参数日志对象,

//第二个参数时间片:每隔20秒统计数据信息

val sc = new StreamingContext(conf,Seconds(5))

//3、定时监控HDFS的mylog下的日志信息

val dstream = sc.textFileStream("hdfs://46f59470c8f2:9000/mylog")

//4、对日志文件进行单词切分

val words=dstream.flatMap(_.split("")).map((_,1))

//SparkStreaming开窗函数reduceByKeyAndWindow,实现单词计数

//方法中需要三个参数

//reduceFunc:第一个就是一个函数

//windowDuration:第二个表示窗口长度

//slideDuration :第三个表示滑动窗口的时间间隔,也就意味着每隔多久计算一次

val result= words.reduceByKeyAndWindow((x:Int,y:Int)=>x+y,Seconds(10),Seconds(5));

result.print()

//5、开启监控

sc.start

sc.awaitTermination

sc.stop(true)

}

}

示例-4:SparkStream整合Flume

其中Flume的配置文件flume-pull-streaming.conf内容如下:

a1.sources =r1

a1.sinks =k1

a1.channels =c1

a1.sources.r1.type =spooldir

a1.sources.r1.spoolDir=/root/flume-pull/

a1.sinks.k1.type = org.apache.spark.streaming.flume.sink.SparkSink

a1.sinks.k1.hostname =46f59470c8f2

a1.sinks.k1.port = 44445

a1.channels.c1.type = memory

a1.sources.r1.channels =c1

a1.sinks.k1.channel =c1

处理的日志log001内容如下:

Hello you

Hello me

源代码为:

import org.apache.spark.SparkConf

import org.apache.spark.streaming.{Seconds,StreamingContext}

import org.apache.spark.streaming.flume.{FlumeUtils,SparkFlumeEvent}

object Spark_Stream003 {

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

if (args.length != 2) {

System.err.println("Usage: FlumePushWordCountTest ")

System.exit(1)

}

val Array(hostname, port) = args

val sparkConf = new SparkConf().setMaster("local[2]")

.setAppName("FlumePushWordCountTest")

val ssc = new StreamingContext(sparkConf, Seconds(5))

ssc.sparkContext.setLogLevel("ERROR")

val flumeStream =FlumeUtils.createPollingStream(ssc, hostname, port.toInt)

val lines=flumeStream.map(x => new String(x.event.getBody.array()).trim)

.flatMap(_.split(""))

.map((_, 1))

.reduceByKey(_ + _)

.print()

ssc.start()

ssc.awaitTermination()

}

}

示例-5:SparkStreaming整合kafka

package cn.rgsoft

import org.apache.spark.SparkConf

import org.apache.spark.streaming.kafka010.KafkaUtils

import org.apache.spark.streaming.Seconds

import org.apache.spark.streaming.StreamingContext

import org.apache.kafka.common.serialization.StringDeserializer

import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent

import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe

object Spark_Stream004 {

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

val conf = new SparkConf()

.setMaster("local")

.setAppName("kafka_streaming")

val ssc = new StreamingContext(conf, Seconds.apply(5))

val kafkaParams = Map[String, Object](

"bootstrap.servers" -> "46f59470c8f2:9092",// kafka 集群

"key.deserializer" -> classOf[StringDeserializer],

"value.deserializer" -> classOf[StringDeserializer],

"group.id" -> "dsffaa",

// 每次都是从头开始消费(from-beginning),可配置其他消费方式

"auto.offset.reset" -> "earliest",

"enable.auto.commit" -> (false: java.lang.Boolean) )

val topics = Array("kafka_streaming") //主题,可配置多个

val stream = KafkaUtils.createDirectStream[String, String]( ssc,

PreferConsistent,

Subscribe[String, String](topics, kafkaParams) )

val rd2=stream.map(e=>(e.value())) //e.value() 是kafka消息内容,e.key为空值

rd2.print()

ssc.start()

ssc.awaitTermination()

}

}

11、SparkStreaming性能调优

(1)数据接收并行度调优:创建更多的输入DStream和Receiver

通过网络接收数据时,比如Kafka,Flume,会将数据反序列化,并存储在Spark的内存中。如果数据接收成为系统的瓶颈,可以考虑并行化数据接收。每个输入DStream都会在某个Worker的Executor上启动一个Receiver,该Receiver接收一个数据流。因此可以通过创建多个输入DStream,并配置它们接收数据源不同的分区数据,达到接收多个数据流的效果。

比如,一个接收两个Kafka Topic的输入DStream,可以拆分成两个输入DStream,每个分别接收一个topic的数据。这样就会创建两个Receiver,从而并行地接收数据,提高吞吐量。多个DStream可以使用union算子进行合并,从而形成一个DStream。后续的算子操作只需要针对合并之后的DSream即可。

代码示例:

int numStreams = 5;

List> kafkaStreams = new ArrayList>(numStreams);

for (int i = 0; i < numStreams; i++) {

kafkaStreams.add(KafkaUtils.createStream(...));

}

JavaPairDStream unifiedDStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));

unifiedDStream.print();

(2)数据接收并行度调优:调节block interval

数据接收并行度调优,除了创建更多输入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。

比如,batch interval为1s,block interval为100ms,则会创建10个task。如果每个batch的task数量太少,即低于每台机器的CPU Core,说明batch的task数量偏少,导致所有的CPU资源没有被完全利用起来。此时应该为batch增加block的数量,需要减小block interval。

但是,需要注意的是,推荐的block interval的最小值为50ms,如果低于这个值,那么大量的task的启动时间可能会变成性能的一个开销。

(3)数据接收并行度调优——输入流数据重分区

使用inputStream.repartition(),将接收到的batch,分不到指定数量的机器上,然后进行后续操作。

(4)任务启动调度

如果每秒钟启动的task过多,比如每秒启动50个,100个,那么发送这些task去Worker节点上的Executor的性能开销将会大大增加,可以使用下述操作减少这方面的性能开销:

Task序列化:使用Kryo序列化机制来序列化task,减小task的大小,从而减少发送这些task到各个Worker节点上的Executor的时间

执行模式:在Standalone模式下运行Spark,可以达到更少的task启动时间

(5)数据处理并行度调优

如果在计算的任何stage中使用的并行task的数量没有足够多,那么集群资源是无法被充分利用的。

举例来说,对于分布式的reduce操作,比如reduceByKey和reduceByKeyAndWindow,默认的并行task的数量是由spark.default.parallelism参数决定的。也可以在reduceByKey等操作中,传入第二个参数,手动指定该操作的并行度,也可以调节全局的spark.default.parallelism参数。

(6)数据序列化调优

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

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

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

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

在一些特殊的场景中,比如需要为流式应用保持的数据总量并不是很多,也许可以将数据以非序列化的方式进行持久化,从而减少序列化和反序列化的CPOU开销,而且又不会有太昂贵的GC开销。举例来说,如果设置的batch interval,并且没有使用window操作,那么可以通过显式地设置持久化级别,来禁止持久化对数据进行序列化。这样就可以减少用于序列化和反序列化的CPU性能开销,并且不用承担太多的GC开销。

(7)batch interval调优

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

给予流式计算的本质,在固定集群资源条件下,应用能保持的数据接收速率,batch interval的设置会有巨大的影响。例如,在WordCount例子中,对于一个特定的数据接收速率,应用业务可以保证每2秒打印一次单词计数,而不是每500ms。因此batch interval需要设置,让预期的数据接收速率可以在生产环境中保持住。

为应用计算合适的batch大小,比较好的方法是先设置一个很保守的batch interval,比如5s~10s,以很慢的数据接收速率进行测试。要检查应用是否跟得上这个数据速率,可以检查每个batch的处理时间的延迟,如果处理时间与batch interval基本吻合,那么应用就是稳定的。否则,如果batch调度的延迟持续增长,那么久意味着应用无法跟得上这个速率,就是不稳定的。此时可以提升数据处理的速度,或者增加batch interval,以保证应用的稳定。

注意:由于临时性的数据增长导致的暂时的延迟增长是合理的,只要延迟情况可以在短时间内回复即可。

(8)内存调优——内存资源

Spark Streaming应用需要的集群内存资源,是由使用的transformation操作类型决定的。举例来说,如果想要使用一个窗口长度为10分钟的window操作,那么集群就必须有足够的内存来保存10分钟内的数据。如果想要使用uodateStateByKey来维护许多key的state,那么内存资源就必须足够大。反过来说,如果想要做一个简单的map-filter-store操作,那么需要使用的内存就很少。

通常来说,通过Receiver接收到的数据,会使用StorageLevel.MEMPRY_AND_DISK_SER_2持久化级别来进行存储,因此无法保存在内存中的数据就会溢写到磁盘上。而溢写到磁盘上,会降低应用的性能。因此,通常的建议是为应用提供它需要的足够的内存资源。

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

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

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

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

68c252051aca78351bf28deb0bf95a15.png

尊敬的读者:如果您感觉好麻烦关注、收藏、点赞,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值