Spark 2.1.0 -- Spark Streaming Programming Guide




概述
spark streaming 是核心spark api的扩展,提供可伸缩、高吞吐和容错的流处理接口,用来处理实时在线流数据。流数据的输入源可以是kafka, flume , kinesis 或tcp sockets,流数据处理可以用map ,reduce , join 和window表达的复杂算法。甚至,可以对流数据使用spark 机器学习或图处理算法。最终处理结果可以直接写到文件系统、数据库或在线画板。


在spark streaming 的内部处理如下:spark streaming 接收实时输入数据流,将数据切分成批数据,spark 处理引擎按批次处理流数据,同样按批次输出处理结果。




spark streaming提供数据流的高阶抽象,称为离散流(discretized stream) 或DStream 。DStreaam可以从Kafka,flume,kinesis中接收输入数据,也可以从其它DStream中生成。本质来看,DStream是一种RDD序列。

本指南从使用DStream开始编写spark streaming程序 ,读者可以使用scala , java , python语言来编写程序,本指南会覆盖这三种语言的代码版本,每段代码有这三个语言的tab标签,可以点取相应语言查看对应版本代码。



注意:有部分api暂不支持python, 或在python使用的API与其它语言完全不同,在查阅指南时,请查看"python API"高亮的提示。

1 快速上手示例
在详细讲解spark streaming编程之前,我们先来看一个简单的spark streaming程序例子。假设我们想计算从服务器接收tcp socket中单词的数量。如下:

首先,我们引入spark streaming 的类,及StreamingContext 的隐式方法到开发环境中,如DStream 。  StreamingContext 是所有streamng功能的主入口,我们创建本地StreamingContext对像 ,它使用两个计算线程,每1秒采集一次批次数据 。

import   org.apache.spark._
import   org.apache.spark.streaming._
import   org.apache.spark.streaming.StreamingContext._   // not necessary since Spark 1.3

// Create a local StreamingContext with two working thread and batch interval of 1 second.
// The master requires 2 cores to prevent from a starvation scenario.

val  conf  =   new   SparkConf (). setMaster ( "local[2]" ). setAppName ( "NetworkWordCount" )
val  ssc  =   new   StreamingContext ( conf ,   Seconds ( 1 ))

通过这个上下文创建DStream的对象表示从TCP接收的流数据,指定接收消息的主机名和端口。

// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

这个lines 表示这个流数据,它的每条记录是一行文本,接着,我们要把这行文本按空格拆分成多个单词。
 

// Split each line into words
val words = lines.flatMap(_.split(" "))


flatMap 将一行文本表示的DStream,转化为每个单词表示的DStream的集合。
在上面的例子中,每行文本转化为单词的集合,记为words 。 接下来,需要计算一下单位的总数
  

import   org.apache.spark.streaming.StreamingContext._
// not necessary since Spark 1.3 // Count each word in each batch
val  pairs  =  words . map ( word  =>   ( word ,   1 ))
val  wordCounts  =  pairs . reduceByKey ( _   +   _ )

// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts . print ()

进一步,words 的DStream 一一映射为 (word ,1 ) 对表示的DStream ,接下来可以将这个新的DStream 聚合出每个单词出现的频次。最终,wordCounts,print() 打印出每秒出现的单词频次 。


ssc.start()             // Start the computation
ssc.awaitTermination()  // Wait for the computation to terminate



完整的代码示例见 spark streaming example : (https://github.com/apache/spark/blob/v2.1.0/examples/src/main/scala/org/apache/spark/examples/streaming/NetworkWordCount.scala)



如果已经下载并编译spark的代码,可以按照以下步骤运行示例代码。首先需要linux Netcat 命令创建一个数据源服务


$ nc -lk 9999

在另一个终端上,可以运行这个示例代码


$ ./bin/run-example streaming.NetworkWordCount localhost 9999



这样在Netcat终端上输入的句子,都会计算并输出每句的单词频次,形如:

# TERMINAL 1:# Running Netcat

$ nc -lk 9999

hello world



...
 
# TERMINAL 2: RUNNING NetworkWordCount

$ ./bin/run-example streaming.NetworkWordCount localhost 9999
...
-------------------------------------------
Time: 1357008430000 ms
-------------------------------------------
(hello,1)(world,1)
...


二 基于概念

接下来深入讲解spark streaming 前,先了解一下概念

2.1 Linking

例似spark , spark streaming 的编译依赖与maven 仓库,编写自己的spark streaming 代码后,需要添加依赖到sbt 中


libraryDependencies += "org.apache.spark" % "spark-streaming_2.11" % "2.1.0"


spark streaming core 中不包含从kafka , flume , kinesis 中接收数据,需要引入相应的spark-streaming-zyx_2.11 的依赖包。如下:

Source Artifact
Kafka spark-streaming-kafka-0-8_2.11
Flume spark-streaming-flume_2.11
Kinesis spark-streaming-kinesis-asl_2.11 [Amazon Software License]

为了了解更多最新的流数据支持,详见 maven 仓库: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.spark%22%20AND%20v%3A%222.1.0%22



2.2 初始化StreamingContext

开始编写spark streaming 程序时,都需要从StreamingContext对象入手。
首先创建一个SparkConf 的对应,然后创建一个StreamingContext的对象。

import   org.apache.spark._
import   org.apache.spark.streaming._

val  conf  =   new   SparkConf (). setAppName ( appName ). setMaster ( master )
val  ssc  =   new   StreamingContext ( conf ,   Seconds ( 1 ))

上例中appName 是集群UI中用于区别程序的名称。master 是spark/mesos/yarn 等的URL,或者单机模式用local[*] 来表示 。 实际集群模式,我们并不会硬编码master到代码中,而是在用spark-submit 启动程序时通过参数传入。 但是做本地测试 和 单元 测试,最好传入"local[*]"来运行spark streaming . 获取到sparkstreaming 对象后,仍然可以使用ssc.sparkContext来获取 SparkContext .

每批次数据的间隔设置有讲究,除了要考虑时延的要求外,还需要综合考虑集群整体资源情况,详见performance tuning 章节 : http://spark.apache.org/docs/latest/streaming-programming-guide.html#setting-the-right-batch-interval

import org.apache.spark.streaming._

val  sc  =   ...                 // existing SparkContext
val  ssc  =   new   StreamingContext ( sc ,   Seconds ( 1 ))

可以用已存在的SparkContext 来创建StreamingContext.
之后需要做以下几件事:
1 通过输入DStream 定义输入源
2 通过对DStream 进行变换和输出操作定义流计算
3 启动接收和处理流数据,streamingContext.start()
4 程序会持续运行直到手动停止或出错,streamingContext.awaitTermination()
5 也可以通过streamingContext.stop()来手动停止

需要牢记以下几点:

1 StreamingContext启动后,程序只会有唯一一个context,不能创建和添加新的context
2 StreamingContext停止后,无法重启此context
3 每个JVM只能启动且仅有一个StreamingContext
4 stop() 即可以停止StreamingContext,也会停止SparkContext。 为了只停止StreamingContext , 将stop() 的参数stopSparkContext 设置为false
5 SparkContext可以重复利用创建多次StreamingContext , 不过每次需要将先前创建的StreamingContext停止后,才可以创建成功新的StreamingContext 。


2.3 离散流(DStreams)

DStream 是Spark Streaming 的基本的数据抽象,描述连续的数据流,即可以是从源接收到的数据流,也可以是对源数据变换之后的数据流。内部来看,DStream 实际是连续地RDD 序列,同时有其它特征,如不变地常量和分布式数据集。DStream 的每个RDD 前事有固定的时间采集间隔,见下图:



既然DStream 底层是RDD的序列,实际所有对DStream的变换最终都会作用于RDD上. 例如,上图中对lines 转化为words 为例, 实际是对lines 这个RDD 作用了flatMap 操作,最终得到 words RDD 。




进一步,对 RDD 的变换最终是借助spark 引擎来处理。 DStream 的方法向上隐藏了这些具体的实现,只向开发人员暴露 抽象地API, 后续章节会详细讨论。


2.4  input Dstream 和DStream接收器

input dstream 即从数据源接收到的DStream 数据 ,在上面的例子中,lines 是从netcat 服务端接收到的 input  DStream  。 每一个input DStream (除了file stream,后续讨论)都会绑定一个接收器的对象, 这个对象负责接收源数据后,将数据暂时存储在内存中,以备后续计算处理。


spark streaming 提供两类内建的流数据源
1 basic sources: 基本数据源, StreamingContext API 支持的数据源,如 文件系统 或socket 连接。
2 advanced source: 扩展数据源, 通过扩展实现类支持的数据源,如kafka , flume , kinesis 等,这些数据源就要求引入依赖的jar 包。


下面我们会分别详细讲解这两种数据源 。

注意,你也可以在程序中同时接收多种数据源,这样就需要在程序中创建多种input stream 。 对应地,需要创建多种接收器负责接收多种流数据 。 但需要注意, spark worker/executor 可以长期稳定运行,因些每个spark streaming 程序 会长期占用分配的CPU core  , 为了保证程序可以长期运行,需要保证spark streaming 需要的CPU core 资源 ,这些CPU资源 一部分用于接收流数据,另一部分用于计算流数据 。


需要注意:
1 当在本地运行spark streaming 程序时,需要注意不要把master 设置"local" 或 "local[1]"。 因为这样spark 程序只会用到本地一个线程,一旦程序接收input DStream 涉及到接收器时,单个线程会用于接收器,这样就没有线程来计算接收到的数据 。 因此,本地运行spark时需要使用 "local[n]", 同时要保证n 大于程序 中用到接收器的数量。
2 同样地,以spark 集群模式运行时,需要保证集群使用的CPU core 资源要多于接收器的数量, 否则,集群也会出现无资源处理接收到的数据 。

2.4.1  基本数据源

前述的例子中,我们使用scc.socketTextStream(...) 来接收TCP socket 传送的数据,除此而外,StreamingContext API 还提供了方法用文件来创建数据源。

1>  文件数据流 : 从文件系统中读取文件作为数据源, 也支持HDFS API(如HDFS , S3 , NFS等)。 DStream 创建如:


  streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)

spark streaming 监控文件系统上dataDirector , 只要发现有新文件时,会处理这个文件(如果嵌套的目录有新文件时,暂不支持)  , 注意:

1  新产生的文件需要与之前的文件保持相同的格式
2  文件必须是从外部目录移动到 dataDirectory, 或者是在dataDirectory 对文件进行改名
3  文件移动到dataDirectory, 文件的大小不能再发生变化 , 所以如果是持续追加的方式写入文件, 新追加的数据不会被读取。


对于简单的文本文件,可以使用 StreamingContext.textFileStream(dataDirectory)  来读取。 并且这种file stream 并不需要接收器, 因此不需要为接收器分配 core  。

python API  : fileStream is not available in the Python API, only    textFileStream is    available.



2>  基于定制接收器的流数据 : DStream 可以从定制的接收器中创建流数据 , 见: http://spark.apache.org/docs/latest/streaming-custom-receivers.html



3>   RDD 队列作流数据 :  为了测试spark streaming程序 ,可以创建常量RDD队列,StreamingContext.queueStream(queueOfRDDs)  。 RDD队列中每一个元素作为一批次流数据 ,测试时和通常的流程序一样。

更多详尽的Stream 的内容,请参数相关StreamingContext 的创建函数 : http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.streaming.StreamingContext


2.4.2 扩展数据源

 python API  : As of Spark 2.1.0, out of these sources, Kafka, Kinesis and Flume are available in the Python API.


这种数据源依赖于spark 以外的包支持,有些包的依赖关系比较复杂(如kafka 和flume) 。 因此, 为了防止引入依赖包的版本冲突等问题, 这类数据源创建 DStream 的功能,已移交给相应的包库中阐述,此处不详述。

注意到,  由于spark shell 中不支持这类扩展数据源,因此,无法直接通过 spark-shell 来测试程序。 但可以通过 maven 仓库引入相关的依赖包到spark classpath 中,这样可以在spark-shell 中测试。

常用扩展数据源
1>  kafka : spark streaming 2.1.0 中集成了版本高于0.8.2.1的kafka 数据源 ,详见: http://spark.apache.org/docs/latest/streaming-kafka-integration.html
2>  flume : spark streaming 2.1.0 中集成了版本高于 1.6.0的flume 数据源, 详见: http://spark.apache.org/docs/latest/streaming-flume-integration.html
3>  kinesis  : spark streaming 2.1.0 中集成了高于 1.2.1 的kinesis 客户端库, 详见: http://spark.apache.org/docs/latest/streaming-kinesis-integration.html




2.4.3 自定义数据源

Python API : This is not yet supported in Python.


可以通过自定义的数据源来创建input DStream。 需要实现一个自定义的接收器, 从自定义数据源接收数据,并将数据推给spark  , 详见: http://spark.apache.org/docs/latest/streaming-custom-receivers.html

2.4.3.1 接收器可靠性

有两种方法保证接收器中数据的可靠性。 数据源(如kafka , flume) 对传送的数据有一个确认机制。如接收数据源数据的系统,可以确认数据的正确性, 同时也可以确认不存在数据传输的失败。 这样有两类接收器:

1 可靠的接收器 : 可靠的接收器在正确接收数据,并在spark中备份存储后 ,会向可靠数据源(需要确认消息)发送确认消息,以保证数据明确到达。

2 非可靠的接收器 : 非可靠的接收器不会向数据源发送确认消息。适用于消息源不支持确认消息的场景,或者消息源支持确认消息,但不想或无需发送复杂的确认消息。

编写可靠接收器可参考 : http://spark.apache.org/docs/latest/streaming-custom-receivers.html




2.5 DStream 上的变换

前面说过,DStream 底层实际上是RDD,因此, 允许我们对DStream 进行变换。DStream 支持以下


Transformation Meaning
map(func) Return a new DStream by passing each element of the source DStream through a function func.
flatMap(func) Similar to map, but each input item can be mapped to 0 or more output items.
filter(func) Return a new DStream by selecting only the records of the source DStream on which func returns true.
repartition(numPartitions) Changes the level of parallelism in this DStream by creating more or fewer partitions.
union(otherStream) Return a new DStream that contains the union of the elements in the source DStream and otherDStream.
count() Return a new DStream of single-element RDDs by counting the number of elements in each RDD of the source DStream.
reduce(func) Return a new DStream of single-element RDDs by aggregating the elements in each RDD of the source DStream using a function func (which takes two arguments and returns one). The function should be associative and commutative so that it can be computed in parallel.
countByValue() When called on a DStream of elements of type K, return a new DStream of (K, Long) pairs where the value of each key is its frequency in each RDD of the source DStream.
reduceByKey(func, [numTasks]) When called on a DStream of (K, V) pairs, return a new DStream of (K, V) pairs where the values for each key are aggregated using the given reduce function. Note: By default, this uses Spark's default number of parallel tasks (2 for local mode, and in cluster mode the number is determined by the config property spark.default.parallelism) to do the grouping. You can pass an optional numTasks argument to set a different number of tasks.
join(otherStream, [numTasks]) When called on two DStreams of (K, V) and (K, W) pairs, return a new DStream of (K, (V, W)) pairs with all pairs of elements for each key.
cogroup(otherStream, [numTasks]) When called on a DStream of (K, V) and (K, W) pairs, return a new DStream of (K, Seq[V], Seq[W]) tuples.
transform(func) Return a new DStream by applying a RDD-to-RDD function to every RDD of the source DStream. This can be used to do arbitrary RDD operations on the DStream.
updateStateByKey(func) Return a new "state" DStream where the state for each key is updated by applying the given function on the previous state of the key and the new values for the key. This can be used to maintain arbitrary state data for each key.



2.5.1 UpdateStateByKey 操作 

UpdateStateByKey 操作允许持续更新RDD的状态,需要以下两步: 
1  定义状态 : 这个状态可以是任意数据类型
2 定义状态的更新操作: 定义更新状态的操作,即可以将之前的状态值和新的数据流发生关系

每批次数据到达 后, spark 会将状态更新操作(updateStateByKey )应用于所有已存在的keys , 不管这个批次是否有Key 的数据 。 如果更新函数返回 None , 会乎略这个key的状态。

用以下的例子展示 。 假设我们想持续保存数据源中单词的总数。 此处, 这个计算count的可以定义为状态值, 定义更新状态的函数为: 

def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
    val newCount = ...  // add the new values with the previous running count to get the new count
    Some ( newCount )
}


下面将状态更新操作应用于 DStream的每个单词上(即,pairs  DStream 形如 (word , 1) )

val runningCounts = pairs.updateStateByKey[Int](updateFunction _)


更新状态函数(updateStateByKey  )会对每个单词作key 的DStream 进行计算, 本次的状态值newValues 是一个(word , 1 ) 的序列, runningCount 是之前汇总的结果。
 
注意: 使用UpdateStateByKey 需要给DStream 配置checkpoint 目录, 详见 checkpointing 章节 。

2.5.2 变换操作

变换操作允许任意 RDD-to-RDD的函数作用于DStream 。 这样即使DStream API上没有罗列出操作,仍然可以使用RDD的操作应用于DStream 。 例如, 将数据源每批次数据与另一个数据集联合在
一起的操作,DStream API中不支持,尽管如此, 仍然可以使用transform 操作来实现 。 例如,在实现对数据实时清洗时,可以基于输入数据流与预定义的垃圾信息关联来过滤数据 。
 
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information

val  cleanedDStream  =  wordCounts . transform  {
rdd  =>
  rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
  ...
}

注意到, 前述函数会计算每批次数据,  这样可以实现随时间变化的操作,如随批次数据更新的操作, 如 , RDD 操作  , 分区数, 广播变量等。


2.5.3 窗口函数

spark streaming  同时支持窗口计算,这样可以将变换作用于移动的窗口数据。 下图展示移动的窗口: 



如上图, 每个窗口的数据源在每批次DStream间移动,落在这个窗口中的RDD 组合在一起生成窗口DStream 。  上例中, 窗口涵盖最新的3 个批次数据 ,这样每次移动两个批次的位移,
这样窗口操作会有两个参数 
 1> 窗口宽度 : 窗口的跨度 , 上图中是3 
 2> 移动间隔: 窗口操作执行的周期 ,上图中是2 

以上两个参数必须是DStream 源批次的倍数。 上例是1 

下例展示窗口操作的例子。 假设每10秒一批次数据 , 我们只累加最近30秒流数据的单词总数。这样我们需要使用 reduceByKey 作用于最新30秒DStream 的pairs (word, 1) 上 。 
可以使用reduceByKeyAndWindow  来实现 


// Reduce last 30 seconds of data, every 10 seconds
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (+ b), Seconds(30), Seconds(10))

所有窗口操作都需要两个参数 : 窗口跨度和 移动间隔。
常用窗口操作见下:  


Transformation Meaning
window(windowLengthslideInterval) Return a new DStream which is computed based on windowed batches of the source DStream.
countByWindow(windowLengthslideInterval) Return a sliding window count of elements in the stream.
reduceByWindow(funcwindowLengthslideInterval) Return a new single-element stream, created by aggregating elements in the stream over a sliding interval using func. The function should be associative and commutative so that it can be computed correctly in parallel.
reduceByKeyAndWindow(funcwindowLengthslideInterval, [numTasks]) When called on a DStream of (K, V) pairs, returns a new DStream of (K, V) pairs where the values for each key are aggregated using the given reduce function func over batches in a sliding window. Note: By default, this uses Spark's default number of parallel tasks (2 for local mode, and in cluster mode the number is determined by the config property spark.default.parallelism) to do the grouping. You can pass an optional numTasks argument to set a different number of tasks.
reduceByKeyAndWindow(funcinvFuncwindowLengthslideInterval, [numTasks])

A more efficient version of the above reduceByKeyAndWindow() where the reduce value of each window is calculated incrementally using the reduce values of the previous window. This is done by reducing the new data that enters the sliding window, and “inverse reducing” the old data that leaves the window. An example would be that of “adding” and “subtracting” counts of keys as the window slides. However, it is applicable only to “invertible reduce functions”, that is, those reduce functions which have a corresponding “inverse reduce” function (taken as parameter invFunc). Like in reduceByKeyAndWindow, the number of reduce tasks is configurable through an optional argument. Note that checkpointing must be enabled for using this operation.

countByValueAndWindow(windowLength,slideInterval, [numTasks]) When called on a DStream of (K, V) pairs, returns a new DStream of (K, Long) pairs where the value of each key is its frequency within a sliding window. Like in reduceByKeyAndWindow, the number of reduce tasks is configurable through an optional argument.
 


2.5.4 关联操作

最终, 需要说明spark streaming 中支持的关联操作。

2.5.4.1 流与流数据之间joi (stream-stream 关联)

流数据可以很容易与其它流数据关联

val stream1: DStream[StringString] = ...
val stream2: DStream[StringString] = ...
val joinedStream = stream1.join(stream2)

在每批次数据时, RDD stream1 会和 RDD stream2 关联在一起。 当然也可以使用leftOuterJoin ,  rightOuterJoin , fullOuterJoin 等。进一步,  也可以和其它窗口流数据进行关联。

val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)

2.5.4.2 流与数据集关联

在前述文中已提过,使用DStream.transform 操作可以实现数据流与数据集关联,此处提及另一个窗口流与数据集关联。

val dataset: RDD[StringString] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform {
rdd =>
     rdd.join(dataset)
}

事实上, 可以动态调整需要关联的数据集。 此处, transform 会作用于每批次数据流, 并且与此时dataset 所引用的对象进行关联操作。

2.6 DStream 上的输出操作

输出操作允许DStream 数据推送到其它外部系统,如数据库或文件系统 。 输出操作允许变换后的数据被外围系统落地,这个操作会触发DStream 变换的执行(类似于RDD的action)。
当前, 只定义了以下输出

Output Operation Meaning
print() Prints the first ten elements of every batch of data in a DStream on the driver node running the streaming application. This is useful for development and debugging. 
Python API This is called pprint() in the Python API.
saveAsTextFiles(prefix, [suffix]) Save this DStream's contents as text files. The file name at each batch interval is generated based on prefix and suffix"prefix-TIME_IN_MS[.suffix]".
saveAsObjectFiles(prefix, [suffix]) Save this DStream's contents as SequenceFiles of serialized Java objects. The file name at each batch interval is generated based on prefix and suffix"prefix-TIME_IN_MS[.suffix]"
Python API This is not available in the Python API.
saveAsHadoopFiles(prefix, [suffix]) Save this DStream's contents as Hadoop files. The file name at each batch interval is generated based on prefix and suffix"prefix-TIME_IN_MS[.suffix]"
Python API This is not available in the Python API.
foreachRDD(func) The most generic output operator that applies a function, func, to each RDD generated from the stream. This function should push the data in each RDD to an external system, such as saving the RDD to files, or writing it over the network to a database. Note that the function func is executed in the driver process running the streaming application, and will usually have RDD actions in it that will force the computation of the streaming RDDs.
 

2.6.1 使用foreachRDD的模式

dstream.foreachRDD 经常用于推送到外围系统,因此,有必要介绍一下如何正确和高效地使用它, 以下错误要避免 

经常将数据写到外围系统 ,需要创建一个连接对像(例TCP 连接到远端服务器) ,并使用此对象次数据发送出去。 出于这种应用,开发人员经常会动不动就在spark driver 侧创建连接对象,
并在worker 上使用它来保存RDD数据 。 例如,

dstream.foreachRDD { rdd =>
  val connection = createNewConnection()  // executed at the driver
  rdd.foreach { record =>
    connection.send(record) // executed at the worker
  }
}

以上使用不正确的地方是: 这个连接对象会被序列化后,从driver 发送给worker 使用。 但这种连接对象很少需要在主机间传送。这种使用将会引发错误(连接对象未序列化), 
初始化错误(连接对象需要在worker 上初始化)等。 正确的使用应该是在worker 上创建这个连接对象。
但这样又会造成另一个错误  -- 每个记录需要创建一次新的连接对象。 例如

dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }
}

这样,创建连接对象需要时间和资源消耗。 因此,  对处理每条记录而创建和销毁连接对象会引入不必要的系统开销,同时大大降低整个系统的吞吐。 一个好的解决方案是: 
使用rdd.foreachPartition  ,这样可以对RDD的每个分区创建一个连接对象,而不是每条记录 。 

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }
}

这样相当于处理多条记录时才创建和销毁一次连接对象。 

最终, 可以优化为在多个RDD 或流批次间 重复利用连接对象。  可以维护一个静态的连接对象池, 这个连接池可以被RDD的多个批次重复使用,将数据推送到外围系统 。
因此, 可以减少资源和时间开销

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    // ConnectionPool is a static, lazily initialized pool of connections
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  // return to the pool for future reuse
  }
}

注意到,这个连接池应该按需创建,并且在一段时间不使用后销毁。这样才能保证高效推送数据到外围系统 。 

其它点: 
1 DStream 的输出操作是lazy地, 就像RDD 在执行action 时 lazy 一样。 特别 , DStream 输出操作内嵌套的RDD action 操作强制要求接收到的数据都被处理。因此,应用中
没有输出操作,或者像 dstream.foreachRDD 这类输出操作内没有嵌套RDD action 操作, 此时DStream  不会执行任何操作。这样系统只是丢弃接收的数据,不做其它动作。

2 默认, 输出操作按应用定义的顺序 ,每次执行一次。




2.7 DataFrame 和 SQL 操作

在spark streaming 程序中可以使用dataframe 和 sql 。 需要先从StreamingContext 中取到SparkContext ,然后再创建一个SparkSession . 
因为SparkSession 是单例且lazy 地 ,这样即使drvice 失败了,仍然可以重启 SparkSession  来继续程序。
下例仍然从SparkStreaming中接收数据 ,使用SQL 和Dataframe 来计算单词总数 。这样每个RDD转化为dataframe 。 
注册为临时表并执行 SQL  。


/** DataFrame operations inside your streaming program */

val words: DStream[String] = ...

words.foreachRDD { rdd =>

  // Get the singleton instance of SparkSession
  val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
  import spark.implicits._

  // Convert RDD[String] to DataFrame
  val wordsDataFrame = rdd.toDF("word")

  // Create a temporary view
  wordsDataFrame.createOrReplaceTempView("words")

  // Do word count on DataFrame using SQL and print it
  val wordCountsDataFrame = 
    spark.sql("select word, count(*) as total from words group by word")
  wordCountsDataFrame.show()
}



也可以对另一个线程中采集的Streaming data 上运行SQL , 只需要把另一个线程中采集到的数据注册为一个临时表。这样可以实现异步
执行StreamingContext  。  这里需要注意的地方,需要在StreamingContext 中设置(持续记住) 最大数据量执行所需要的时间。
否则,StreamingContext  并不知道异步SQL需要执行多长时间,那么很有可能在执行SQL结束前就把流数据清空。例如,
假设执行一批次最长需要5 分钟, 需要调用 streamingContext.remember(Minutes(5)) 。



2.8 MLlib 操作

也可以对streaming data 使用MLlib中提供的机器学习。 首先, 算法库中提供流式机器学习支持(如流式线性回归,流式K均值等),
,这些算法可以同时学习流数据,并将模型应用于流数据。除此而外,对于大型机器学习算法,建议使用离线学习和训练,然后将学习好的
模型应用于流式数据。 详见:  http://spark.apache.org/docs/latest/ml-guide.html

2.9 缓存 和持久

与RDD相同,DStream 允许开发使用将流数据持久化在内存。调用persist() API 可以将DStream 每个RDD自动持久化到内存。 
这样当流数据需要反复计算时,这样可以明显节省计算时间。 对于窗口计算(如reduceByWindow/ reduceByKeyAndWindow)
及状态更新操作(如 updateStateByKey),这个API 默认持久化到内存。 因此, 窗口DStream  会自动保存在内存中,不需要开发人员手动调用。


对于从网络中接收Input stream (如 kafka , flume , socket 等)  , 默认持久化级别是 将流数据至少复制到两个节点,这样可以避免故障时数据丢失。

注意DStream 默认持久化级别是将数据序列化后存储在内存,这点不像RDD 。 这点会在性能 优化章节提及 。 其它存储级别可以参见spark programming guide 。


2.10 checkpoing

流式程序需要持续运行24 * 7 ,因此,不能让应用程序来解决快速故障恢复问题。因此, Spark Streaming  需要定期checkpoint
关键信息到容错存储上, 这样一旦故障发生时可以快速恢复。 有两类数据 checkpoint  方式:

1> 元数据checkpoint   。 将流计算中元数据存储在容灾的存储上,如HDFS。  这样一旦流应用的driver 节点出现故障后,可以快速恢复
driver 节点(后续章节详细讨论)。 元数据包含: 
    i>> 配置  :   创建流应用所需要的配置文件
    ii>> DStream 操作 : 流计算中涉及的DStream 操作
    iii>> 未完成批次 : 已压入执行队列的批次任务,并且尚未完成。

2> 数据checkpoint  。将计算生成的RDD保存在容灾存储。 这种场景适用于横跨多个批次数据,并且需要前一批次状态信息的变换。
在这类变换中,新生成的RDD是依赖于前述RDD数据 ,这样就会导致每次计算时,需要依赖的批次数据链持续增长。为了避免在故障恢复时无止尽地回溯,
需要将这种有状态依赖的变换周期性保存到容灾存储上,以减少之间的依赖。

简言之,元数据checkpoint主要用于driver 的快速故障恢复,数据的checkpoint 主要用于有状态的变换出错快速恢复。

2.10.1 何时开启checkpoint 

当应用程序有以下需求时,需要开启checkpoint 

  i>> 有状态的变换  -  如果程序中使用updateStateByKey 或 reduceByKeyAndWindow  ,那么程序中需要周期将RDD数据checkpoint 到容灾存储
  ii>> 快速恢复driver 正在运行的程序   -   元数据 checkpoint 用于快速恢复进度信息 

注意,不涉及前述流状态的变换,可以不使用checkpoint  。driver 故障恢复只部分适用于 checkpoint (接收到但未处理的数据会丢弃)。一般运行spark streaming
的应用大部分可以接受。未来会支持hadoop之外的容灾存储。

2.10.2 如何配置checkpoint

开启checkpoing 只需要配置一个容灾的文件系统,将信息周期存储起来,可以调用API StreamingContext.checkpoint(checkpointDirectory) 。 这样就可以使用
前述有状态变换了。额外 , 为了保证driver 能从故障中恢复, 你需要重写流应用来实现以下行为: 
1> 当应用第一次启动,需要创建一个新的StreamingContext , 创建流数据并启动程序 
2> 当应用重启后,需要用checkpoint目录数据重新创建一个StreamingContext 

以上行为可以简单使用一个 StreamingContext.getOrCreate  ,见下: 

// Function to create and setup a new StreamingContext
def  functionToCreateContext () :   StreamingContext   =   {
  val ssc = new StreamingContext(...)   // new context
  val lines = ssc.socketTextStream(...) // create DStreams
  ...
  ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
  ssc
}

// Get StreamingContext from checkpoint data or create a new one
val  context  =   StreamingContext . getOrCreate ( checkpointDirectory ,  functionToCreateContext  _ )

// Do additional setup on context that needs to be done, // irrespective of whether it is being started or restarted
context .   ...

// Start the context
context . start ()
context . awaitTermination ()

如果checkpointDirectory 目录存在,可以使用checkpoint数据重新创建context 。 如果checkpointDirectory 目录不存在(如第一次运行), 
functionToCreateContext 会重新创建一个新的context 并创建DStream 流。 见scala 例子 :  https://github.com/apache/spark/tree/master/examples/src/main/scala/org/apache/spark/examples/streaming/RecoverableNetworkWordCount.scala


除了使用getOrCreate 方法外, 还需要保证driver 进程会在发生故障时自动重启。这个需要在应用程序架构来保证,详见  部署: http://spark.apache.org/docs/latest/streaming-programming-guide.html#deploying-applications 章节

注意,checkpoint RDD需要引入容灾存储的开销, 这样会导致处理checkpoint RDD数据的时长变长。因此,checkpoint的时间间隔需要仔细考虑。如果
设置过小,如1 秒, 频繁checkpoint 数据会降低计算的吞吐,相反,如果checkpoint 间隔太长,又会导致lineage 和任务数增长,也存在不利的影响。
当因为有状态变换而使用RDD checkpoint 时,默认的时间间隔控制在批次间隔的倍数,并且至少要大于10秒,这个值可以使用dstream.checkpoint(chekpointInterval)来设置。
一般, checkpoint的间隔在5~ 10秒 间变动都是合适的。


2.11 累加器, 广播变量和checkpoint 

累加器和 广播变量 不能通过checkpoint 的方式恢复。如果同时使用,需要创建lazy  单例的累加器和广播变量,以便在driver 故障恢复后可以重新实例化。见下例 : 

object WordBlacklist {

  @volatile private var instance: Broadcast[Seq[String]] = null

  def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          val wordBlacklist = Seq("a", "b", "c")
          instance = sc.broadcast(wordBlacklist)
        }
      }
    }
    instance
  }}

object DroppedWordsCounter {

  @volatile private var instance: LongAccumulator = null

  def getInstance(sc: SparkContext): LongAccumulator = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = sc.longAccumulator("WordsInBlacklistCounter")
        }
      }
    }
    instance
  }}

wordCounts.foreachRDD { (rdd: RDD[(StringInt)], time: Time) =>
  // Get or register the blacklist Broadcast
  val blacklist = WordBlacklist.getInstance(rdd.sparkContext)
  // Get or register the droppedWordsCounter Accumulator
  val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
  // Use blacklist to drop words and use droppedWordsCounter to count them
  val counts = rdd.filter { case (word, count) =>
    if (blacklist.value.contains(word)) {
      droppedWordsCounter.add(count)
      false
    } else {
      true
    }
  }.collect().mkString("[", ", ", "]")
  val  output  =   "Counts at time "   +  time  +   " "   +  counts
})

2.11 部署应用
这章节讨论如何部署spark streaming 应用程序 

2.11.1 需求
为了运行spark streaming 应用,需要整理以下信息: 

1> 集群管理  -  spark 应用程序的基本需求,详见 部署 指南:  http://spark.apache.org/docs/latest/cluster-overview.html

2> 打包应用程序 jar  -  需要把streaming 应用打包成Jar 包,如果使用spark-submit 启动程序 ,应用不需要spark 和spark streaming 的jar 包。
但是如果使用额外的 数据源 (如kafka , flume ) , 需要把数据源组件所依赖的所有jar 包都部署到环境上。例如, 应用使用KafkaUtils ,就需要
包含spark-streaming-kafka-0-8_2.11 和 所有间接依赖的jar 。 

3> executor 配置充足的内存 - 因为接收到的数据存储在内存,因此executor 需要配置走够的内存保存接收的数据 。注意, 如果窗口跨度10分钟,
executor 内存需要保存最近10分钟的数据 。 所以,应用使用的内存需要考虑数据的操作。

4> 配置checkpoint - 如果应用需要checkpoint , 需要在hadoop api 兼容的容灾文件系统(如HDFS, S3等)上配置checkpoint目录,并且应用开发中要

5> 配置自动重启driver  -  为了保障driver 故障自动重启, 流应用程序的架构需要监控driver 进程,在故障时适时重启。不同的集群管理可以通过不同的工具实现。
i>> spark standalone - spark 应用程序的driver 可以分配到spark standalone 集群内,即 , 应用程序driver 在 worker 节点运行。 进一步,
spark standalone 集群的管理节点可以监控driver , 一旦driver 非正常退出 ,或运行driver 的节点失败, 管理节点会重启应用的driver  。 见:
spark standalone 指南中 集群模式和管理模式 :  http://spark.apache.org/docs/latest/spark-standalone.html

ii>> YARN  - yarn 同样支持自动重启应用。 详见 YARN 文档

iii>> Mesos - Marathon 框架用于实现在mesos上自动重启。

6> 配置WAL(write ahead logs)  -  spark 1.2 之后, 我们引入WAL 来实现容灾存储。如果 开启此选项, 所有接收器收到的数据,优先LOG之前(WAL)
写到checkpoint目录。 这样可以避免driver 恢复期间丢失数据 , 如此保证 0 数据丢失(详细见 : http://spark.apache.org/docs/latest/streaming-programming-guide.html#fault-tolerance-semantics) 。 可以配置spark.streaming.receiver.writeAheadLog.enable为 true (其它配置项见: http://spark.apache.org/docs/latest/configuration.html#spark-streaming)
尽管如此,此功能会牺牲部分接收器的吞吐量。为了提升整体的吞吐,需要使用多个接收器并行工作(见: http://spark.apache.org/docs/latest/streaming-programming-guide.html#level-of-parallelism-in-data-receiving) 。 此外,当开启WAL后,就不需要将spark 接收到的数据复制到其它节点, 因为WAL实际在某种程度上也是复制数据。因此,
需要将存储级别调整为 StorageLevel.MEMORY_AND_DISK_SER . 如果使用S3 来存储WAL, 记得开启spark.streaming.deiver.writeAheadLog.closeFileAfterWrite 
和spark.streaming.receiver.writeAheadLog.closeFileAfterWrite  , 详见:  http://spark.apache.org/docs/latest/configuration.html#spark-streaming

7> 设置最大接收速率 - 如果集群资源不能满足流式应用程序处理数据的速率需要,那只能通过限定接收器的最大速率 records/sec 来平衡。详见:
http://spark.apache.org/docs/latest/configuration.html#spark-streaming,接收器速率参数 : spark.streaming.reeceiver.maxRate 和
kafka 的限速率参数 spark.streaming.kafka.maxRatePerPartition 。 spark 1.5 中引入backpressure 特性,spark streaming
会自动计算速率并动态调整,这样就无需手工设置限定速率。 可能通过设置spark.streaming.backpressure.enable 为true 开启此选项。

2.11.1 升级应用代码
如果spark streaming 应用需要升级代码,以下是两个建议: 
1> 升级后的spark streaming 应用程序可以原有应用程序并行运行。等到新程序验证无误并准备取代老程序时,此时可以把老程序停掉。
这样只需要把流数据源分别引到两个目的即可。
2> 老应用程序要gracefully 停止(详见streamingContext.stop(...)  for gracefully shutdown 参数 ), 这样可以接收到的数据完全处理完。
这样新升级的应用程序可以启动,同时新程序会接上老程序需要处理的数据继续工作。需要注意,此处使用的数据源支持buffer (如kafka , flume) ,
这样在老程序停止,新程序还没有启动间隙,数据会在buffer缓存下来。使用新程序从上一次checkpoing点启动 会报错,因为checkpoing点的数据包含
序列化的scala对象,新程序反序列化这些对象会报错。  这种情况,新应用程序可以使用新checkpoint 目录,或者在启动前删除之前checkpoing目录数据。 



2.12 监控应用程序 

除了spark 监控能力外, 有一些其它方法可以监控spark streaming 。当使用 StreamingContext 时, 在spark web UI 上可以显示streaming TAB 页,
此页可以查看当前正在接收器及相关统计信息(诸如接收器状态,接收到的记录数,接收到错误记录数等), 完成批次(每批次处理时间,队列延时等).
这些信息可以更好的掌握流应用程序的进度。

WEB UI中以下两个指标额外重要: 
1) 处理时间  -  每批次数据处理时间
2) 等待时间  -  每批次在队列中等待运行的时间

如果批次的处理时间高于批次的时间间隔,和/或 排队等待时间持续增加, 那么说明系统没法及时处理当前的批次数据,渐渐批次数据会越堆越多,在这种情况,

 Spark Streaming  应用程序执行的进度也可以通过  StreamingListener 接口来监控, 这个接口可以获取到接收器的状态和处理时间。 注意到这个暂时
处理于开发阶段的API , 日后此接口的功能会进一步扩展。


3 性能优化

优化集群上spark streaming 应用程序需要花费很多心思。 这章节给出部分可以优化spark streaming 应用的参数 。总观你的程序,程序员需要
首先考虑两件事; 
1> 合理利用集群资源 ,以保证每批次数据可以及时处理
2> 设置合理的批数据大小,这样才能保证接收器处理数据,与接收新数据基本平衡。

3.1 降低批次数据处理时间
减少spark 处理每批次数据的时间,有多种优化的方法, 详见优化指南:  http://spark.apache.org/docs/latest/tuning.html
本章节只列出重要的内容

3.1.1 数据接收的并行级别
从网络中接收数据 (如kafka , flume , socket等)需要反序列化成原始数据,然后存储在spark 内存中处理。如果接收数据成为瓶颈,
就需要考虑提升接收并行度。
注意,每个DStream 源创建一个接收器(运行在一台worker 主机), 这个接收器只接收单一流数据。为了同时接收多个数据流,需要创建多个DStreams
数据源,并且配置每个接收器接收数据流不同的分区。例如, 单个kafka input dstream 接收两个topic 的数据, 可以拆分为两个kafka input dstream, 
每个kafka input dstream 只接收一个topic 。 这样虽然需要创建两个接收器,但却保证数据可以同时并行接收, 即提升了网络 的吞吐。 可以把多个input 
dstream 联合在一起创建成一个dstream , 这样可以让变换同时操作多个input dstream 。 见下: 

val numStreams = 5
val kafkaStreams = (1 to numStreams).map {
=>
KafkaUtils.createStream(...)
}
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()

另一个优化的参数是接收器的block 间隔(block interval ), 即 spark.streaming.blockInterval 见:  http://spark.apache.org/docs/latest/configuration.html#spark-streaming
大多数接收器,会把接收到的数据一起组成一个块数据,然后再存储在spark 内存中。每批次数据的块数,决定了处理接收数据的变换会起多少个task 。
每个接收器每批次生成的任务数, 近似于 batch interval  /  block interval 。  例如, block interval 是200 ms ,  2 秒会创建10个task 。 
如果上面这个任务数太小(如相对于单台主机的核心数), 这样主机的CPU没有充分利用。为了提高每批次的任务数,需要缩短block interval . 
尽管这样,建议最小block interval 大约在50ms ,这个值低于50 会导致任务频繁起动。 

另一种提高接收数据流并发的方法,就是显示重新对输入数据流进行分区 inputStream.repartition 分区数。 这样会对接收到的数据在计算前,重新在多台主机间分布。

3.1.2 数据处理的并行级别

如果每个stage 所运行的并行task数过低,会导致集群资源利用率不高。例如, 在使用reduceByKey 和 reduceByKeyAndWindow 时,每个stage
并行task数由 spark.default.parallelism 配置项控制 ,见:   http://spark.apache.org/docs/latest/configuration.html#spark-properties
这时通过PairDStreamFunctions 来设置并行级别,或者重新设置spark.default.parallelism 的默认值,可以修改这个并行task 数。


3.1.3 数据序列化

数据序列化的性能损耗可以优化序列化格式来减少,在流数据中,经常使用以下两种序列化: 
1> input data(输入数据) : 默认,接收器收到输入数据后,在executor 内存中存储的级别是 StorageLevel.MEMORY_AND_DISK_SER_2 。 
这样,数据会序列化成字节流来降低 GC 消耗, 同时备份到另一台主机降低executor 失败对任务的影响。同时, 数据优先保存在内存中,只有当
内存不够用的情况,才会将数据保存到本地磁盘。这样序列化过程中性能有损耗  -- 接收器需要先反序列化接收到的数据,然后再将数据序列化为
spark 支持的格式。

2> 持久化流操作生成的RDD : streaming 计算生成的RDD持久会在内存。 例如,窗口操作的数据由于要多次计算,会把数据持续化在内存,
但却不像spark core 中默认的 StorageLevel.MEMORY_ONLY, 实际默认存储级别是StorageLevel.MEMORY_ONLY_SER ,这样序列化后的数据存储在
内存中,可以减少GC损耗 。
 
在上面两种情况,使用kryo 序列化可以减少CPU和内存的消耗。参见spark  优化指南: http://spark.apache.org/docs/latest/tuning.html#data-serialization
对于kryo , 需要考虑注册自定义类,禁止跟踪 对象的引用(object reference tracking ) , 见 kryo 相关的配置项

在特定的例子, 如当流计算操作的数据量并不大时, 使用上述两种序列化是方便的,这样可以降低反序列化时GC消耗。例如, 
每批次间隔时间较长(a few seconds), 且不使用窗口操作, 可以显示设置存储级别来禁止持久化数据时序列化过程。
由于序列化时会带来CPU的性能损耗, 减少GC的消耗可以提供性能 。


3.1.4 任务频繁启动消耗

如果每秒启动的任务次数太多(如每秒50个以上), 频繁将任务发送给worker 节点执行的损耗就变得显著,应用程序很难达到秒级延迟。
通过调整以下项可以降低这种损耗 

1> 执行模式(execution mode) :  让spark 运行在standalone 或mesos 的粗放化资源管理的模式,会比mesos精细化资源管理的模式更
节省任务启动时间,大约每批次的节省时间相差几百毫秒 。 。详见:  http://spark.apache.org/docs/latest/running-on-mesos.html


3.2 恰当的批次间隔
为了使spark streaming 应用运行稳定, 系统需要保证处理数据和接收数据同步。换句话说,处理数据的时间需要能赶上甚至超过源头生成数据
的时间。在streaming web UI 界面上可以持续监控数据处理时间,需要保证这个时间小于批数据采集间隔。

 当集群资源一定的条件下,流计算批次的间隔完全由数据生成的速率决定。 例如, 以前面wordcountnetwork 为例, 在特定的数据生成速率下,
系统可以保证按每2秒报告一次结果,而不可能是500毫秒。因此,在生产环境需要设置合适的批次间隔,才能保证跟上数据生成的速率 。

在设置批次时间间隔时,首先配置一个相对保守的时间间隔(如5~10秒),然后逐渐将间隔缩短。为了验证系统能否跟上数据生成的进度,需要
检查一下每批次数据的处理时延(可以查看spark driver 的"Total delay", 或者StreamingListener 接口 )。如果系统的时延持续与批次数据生成
的时间相当,系统可以稳定运行。否则, 如果批次数据计算的延时持续增长,这说明系统没法及时计算生成的数据,这样系统会很不稳定。一旦
有一组保守的配置,可以慢慢提升数据生成的速率 和/或 降低每批次数据量。 注意,数据生成的速率加快后,每批次计算的时延会暂时变长,因此,
需要运行一段时间等计算时延稳定后,观察新的配置组合是否能使系统稳定。


3.2.1 内存优化
内存优化,spark 应用的GC优化详见: http://spark.apache.org/docs/latest/tuning.html#memory-tuning 。
建议详细阅读一下上文中内容,会对优化spark  streaming 应用程序有很大的帮助。

spark streaming 应用使用的集群内存,很大因素由数据使用的变换所决定。 例如,如果想使用window 操作计算最后10分钟数据,
这样,要求集群有足够的内存保存近10分钟的数据。 或者如果对大量的keys使用updateStateByKey操作, 这样内存使用就会很高。
相反,如果只想作一些简单的过滤-落地操作,实际使用的内存就很低。

 
通常,接收器收到的数据存储级别 StorageLevel.MEMORY_AND_DISK_SER_2 , 这样内存放不下时,会将数据写到磁盘。这样会
降低streaming程序的性能 ,因此最好保证集群的内存足够运行程序 。建议先用小量数据计算得到内存使用,以此评估大数据量时
内存真实使用情况。

内存优化的另一方面是GC , 对于流计算的应用程序,要求计算的时延很低,因此JVM GC 引起长时间程序暂停是不希望看到地。 

以下有几个参数可以优化内存的使用,减少GC的消耗。
1> DStream 持久化级别 : 在前述数据序列化章节: http://spark.apache.org/docs/latest/streaming-programming-guide.html#data-serialization
 , 输入数据和RDD默认要序列化为字节,相比于反序列化数据再持久,这样可以降低内存使用和GC 消耗,开启kryo序列化可以进一步降低序列后大小
和内存占用。如果想进一步降低内存占用,需要使用压缩(见spark 配置  spark.rdd.compress),但会消耗CPU的时间。

2>  清除老数据 : 默认, DStream 变换生成的RDD和所有输入数据是自动清除地。 spark 根据变换的时效,spark streaming
决定哪些数据需要清除。 例如, 假设使用窗口操作操作最近10分钟, spark streaming 会保存最后10分钟的数据, 并将之前的数据
清除。 如果想保存更长的时间,需要设置 streamingContext.remember 。

3> CMS  GC  : 强烈推荐使用并发 标记-清除  地GC, 这样可以保证GC 相关的应用停止时间相对变短。 尽管并发GC 可以降低系统
处理的吞吐, 却可以保证系统处理批数据的连续。 需要确保 CMS  GC 在 driver (使用 --driver-java-options 在spark-submit) 和
executor (使用 spark.executor.etraJavaOptions  配置)

4> 其它提示 :  为了进一步降低GC消耗, 需要做以下尝试
     使用 OFF_HEAP 存储级别来持久化RDD , 详见 :  http://spark.apache.org/docs/latest/programming-guide.html#rdd-persistence
     尽量使用更多的executor ,同时保证executor 使用小HEAP size , 这样会降低JVM heap 的GC压力。

3.2.2 重要点: 
1> 每个DStream 关联一个接收器。为了在多个接收器上保持读取数据的并发,例如,需要创建多个DStream 。 每个接收器
在一个executor上运行, 占用主机一个CPU核心,需要保证占用一个CPU核心外, 主机仍然有其它核心来处理数据, 例如,
spark.cores.max  需要考虑接收器占用的CPU核心。 接收器在executor 间 round robin 方式。
2> 从流数据源接收到的数据 , 接收器会为他们创建一个数据块,每隔 blockInterval 毫秒后,接收器会生成新的数据块。
 executor 在每一个批次时间间隔,共要创建N 个数据块,其中   N = batchInterval / blockInterval 。 这些数据块被当前executor
的BlockManager 分发给其它 executor 的BlockManager 。 至此, driver 上运行的网络输入跟踪器 知晓 需要进一步处理的块地址。
3> 在批次时间间隔,driver  会用executor 上的数据块创建RDD。 或者说,这些数据块实际就是RDD的分区。 每个分区对应一个task 。 
如果blockInterval == batchInterval , 那么RDD只有一个分区,而且很有可能task 会本地执行。 
4> executors(其中一个executor 负责接收块,另一个负责块复制 ) 上执行块的map task , 可以保证executor 上的块与block interval 无法。
除非 使用非本地调度。 blockInterval 时间越长,意味着数据块更大。同时增加spark.locality.wait 以提高本地节点处理块的机率。
这两个参数之间需要平衡,以保证大的数据块可以在本地执行。
5> 除了依赖batchInterval 和blockInterval ,  也可以重新分配 输入数据分区 inputDStream.repartition(n)。 此接口会重要随机合并
RDD 中数据, 创建有n个分区的RDD。 为了提升并发度,即使合并需要消耗时间,仍然建议使用repartition 。 driver 的jobsheduler
调度 job , 每个job 会处理 RDD数据。 任意时间点上,只有一个job是活跃地,因此, 如果当前有一个job 在执行时,其它job会排队。
6> 如果有两个DStream, 对应会有两种RDD格式,会生成两个job 并且顺序执行。为了避免顺序执行,需要将两个DStream 联合在一起。
这样可以保证单个 unionRDD 可以满足两种格式的RDD, 同时只生成一个job ,且不影响RDD的分区。
7> 如果批数据处理的时间比batchInterval 还要长,显示接收器的内存会最终填满,接收器会抛异常而终止(很可能是BlockNotFoundException)
. 当前还没有办法可以暂停接收器。 使用sparkConf 配置,spark.streaming.receiver.maxRate ,可以限制接收器的速率。

4 故障恢复
在本章节,我们会讨论spark streaming 应用程序的故障恢复相关内容。 

4.1 背景
 为了理解spark streaming 的故障恢复的语义,需要先了解spark RDD 故障恢复的语义。
1> RDD 是不可变常量, 确定可以反复计算,分布式的数据集。每一个RDD是由一组操作序列生成的,因此,RDD数据的恢复也依赖于
容灾输入数据和这个操作 序列。
2> 如果某个RDD的分区数据由于worker节点失败而丢失, 这个分区数据可以对容灾的输入数据,作用于操作的序列来恢复。
3>  假设所有RDD的变换是确定的, RDD变换得到的结果始终如一,不受spark 集群失败的影响。

spark 支持的容灾文件有HDFS和 S3 . 因此, 由容灾数据所生成的RDD数据 ,一样也具体容灾特性。然而 ,这并不是spark streaming
通用的应用场景, 因为spark streaming 的数据是通过网络采集而来。为了让spark streaming 生成的RDD也具有容灾特性,通常会让
接收到的数据复制到集群worker 节点的多个executor上(默认系数是2)。 这样在发生故障时,就有两种数据需要恢复: 
1 接收并复制的数据 - 单个worker 节点失败不会影响数据,因此在另一个worker 节点有一个备份
2 接收到的数据,准备复制 -因为数据还没有来得及复制,因些只能从数据源做数据恢复

进一步,也有两种故障类型需要格外 注意
1 worker 节点失败 - 任意一个运行executor的worker 节点都有可能失败, 这样节点上的所有内存中数据会丢失。如果接收器恰好
在失败的节点上运行, 那么buffer中的数据会丢失。
2 driver 节点失败 - 如果运行spark streaming 应用的driver 节点失败,很显然 SparkContext会丢失, 这样所有executor内存中数据
会丢失。

有了以上知识, 就可以更好的理解 spark streaming 的故障恢复语义。 


4.2 定义
流系统的关键指标是: 系统处理一条记录需要多少时间 。 在基本场景下: 系统提供了三种消息传送保障机制:
1> 最多一次 : 每条记录至多处理一次
2> 至少一次 : 每条记录至少处理一次。 本类型虽然不会丢数据,但是会导致数据重复处理
3> 恰好一次 : 每条记录恰好处理一次 - 无数据丢失及数据重复处理, 这是三者中最强的保障机制


4.3 基础语义

 在任意一个流处理的平台, 处理数据需要三步走: 
1> 接收数据 : 从接收器或其它途径接收数据
2> 变换数据 : 对接收到的数据使用DStream或RDD变换
3> 外推数据 : 最终结果外推给其它系统,如文件系统,数据库或数据盘等

如果流应用程序想获得点到点的恰好只处理一次的保障,这样每一步都需要提供恰好只处理一次的保障。也就是说,每条记录恰好接收
一次,恰好变换一次,恰好外推一次。下面再仔细了解一下spark streaming 的语义
1> 接收数据 : 不同的数据源提供不同的保障机制,在下一章节会详细说明
2> 变换数据 : 所有接收到的数据恰好被处理一次, 归功于RDD提供的保障。 尽管有失败,只要接收到的数据可读,还是可以把最终
结果恢复
3> 外推数据 :  外推操作默认使用至少一次语义,因为它依赖于输入操作的类型(是否支持幂等)和下游系统的语义(是否支持事务)。
但开发人员可以自己实现事务机制来保障恰好一次语义。 本章节后续会详细讲解。

4.4 接收数据语义
不同的数据源提供不同的保障机制,从至少一次到恰好一次,可参考 其它内容

4.4.1 文件数据源
如果输入文件是存储在容灾文件系统,如HDFS, spark streaming 可以从失败中恢复,并还原所有结果。这样可以支持恰好一次
保障机制,意味着即使系统失败,也能保证所有数据恰好只处理一次,不重不漏。

4.4.2 基于接收器的源
基于接收器的输入源,容灾语义依赖于失败的场景 和接收器类型。在前述讨论的,有两类接收器: 

1> 可靠接收器 - 可以保证接收到的数据都被复制成功,此时接收器通知可靠数据源发送下一批数据。如果接收器失败,数据源不会
收到通知消息。 因此, 如果接收器重启了,数据源需要重新发送数据,这样才能保证无数据丢失。

2> 不可靠接收器 - 接收器不会向数据源发送确认消息,因此,接收器可能会由于worker 或driver 的失败导致丢数据。


使用不同的接收器,可以获得不同的语义。 可靠接收器: 即使worker 节点失败了,仍然不会丢数据 ;不可靠接收器 : 接收到数据如果
没有复制完成,此时worker 失败会导致数据丢失。如果driver节点失败,除了以上的数据会丢失,在内存中接收到的数据和复制完成的
数据也会丢失。这会景程有状态的变换。

为了避免接收到的历史数据丢失,spark 1.2引入WAL(write ahead logs ), WAL会将接收到的数据保存到容灾存储。当开启WAL和
可靠接收器, 数据不会丢失。这样来看,可以提供恰好只一次的保障机制。

下表总结了故障的语义。


Deployment Scenario Worker Failure Driver Failure
Spark 1.1 or earlier, OR
Spark 1.2 or later without write ahead logs
Buffered data lost with unreliable receivers
Zero data loss with reliable receivers
At-least once semantics
Buffered data lost with unreliable receivers
Past data lost with all receivers
Undefined semantics
Spark 1.2 or later with write ahead logs Zero data loss with reliable receivers
At-least once semantics
Zero data loss with reliable receivers and files
At-least once semantics
   
4.4.3 kafka direct API 
spark 1.3 中引入了新的kafka direct API , 新API可以保证所有从kafka 传输的数据,在spark streaming 中支持恰好只一次。除了
这个,如果实现了恰好只一次输出操作, 开发人员可以获得点到点地恰好只一次的保障机制,即从输入数据源,变换到外推全程恰好只
一次。 详见kafka 集成指南:  http://spark.apache.org/docs/latest/streaming-kafka-integration.html 。

4.5 输出操作语义
输出操作(类似foreachRDD) 支持至少一次语义,也就是,变换后的数据会写到外部系统 ,假设出现worker 失败的情况,允许
落地至少一次。可以简单地将输出数据写到saveAsXXXFiles文件系统里(因为文件会自动覆盖)。
有两种方式可以实现恰好只一次的语义。

1> 幂等更新 :  幂等更新是指不管输出成功几次,最终都可以得到同一个结果。例如, saveAsXXXFiles 总是生成同一个文件。

2> 事务更新 : 更新操作支持事务,这样可以实现恰好只有一次语义。其中一种实现方式是: 
     使用批次时间(foreachRDD中支持)和RDD的分区索引来创建标识, 这个标识唯一确定流应用中的一个blob 
     使用这个标识更新外部系统的blob事务(即,自动实现恰好只一次)。 即, 如果这个标识的数据还没有提交,
     应用程序会自动提交标识以及分区数据,否则, 如果已经提交,乎略更新。


dstream.foreachRDD { (rdd, time) =>
  rdd.foreachPartition { partitionIterator =>
    val partitionId = TaskContext.get.partitionId()
    val uniqueId = generateUniqueId(time.milliseconds, partitionId)
    // use this uniqueId to transactionally commit the data in partitionIterator
  }
}


5 where to go from here (后续内容,有兴趣请自己查阅原文)























































































 


































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值