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
在内部,其按如下方式运行。Spark Streaming接收到实时数据流同时将其划分为分批,这些数据的分批将会被Spark的引擎所处理从而生成同样按批次形式的最终流。
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都包含来自一定间隔的数据,如下图:
在DStream上使用的任何操作都会转换为针对底层RDD的操作。例如:之前那个将行的流转变为词流的例子中,flatMap操作应用于行DStream的每个RDD上 从而产生words DStream的RDD。如下图:
这些底层的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同样也提供基于时间窗口的计算,也就是说,你可以对某一个滑动时间窗内的数据施加特定转换算子。如下图所示:
红色的矩形就是一个窗口:一段时间内的数据流,这里面每一个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))
以下列出了常用的窗口算子。所有这些算子都有前面提到的那两个参数:窗口长度 和 滑动距离。
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。