Spark Streaming 是个批处理的流式(实时)计算框架。其基本原理是把输入数据以某一时间间隔批量的处理,当批处理间隔缩短到秒级时,便可以用于处理实时数据流。
支持从多种数据源获取数据,包括Kafk、Flume、Twitter、ZeroMQ、Kinesis以及TCP sockets,从数据源获取数据之后,可以使用诸如map、reduce、join等高级函数进行复杂算法的处理。最后还可以将处理结果存储到文件系统,数据库等。
Spark Streaming处理的数据流图:
以上的连续4个图,分别对应以下4个段落的描述:
目前而言SparkStreaming 主要支持以下三种业务场景
object NetworkWordCount {
def main(args: Array[String]) {
val sparkConf = new SparkConf()
val ssc = new StreamingContext(sparkConf, Seconds(1))
val lines = ssc.socketTextStream(hostname, port)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
1. 通过创建输入DStreams来定义输入源。
2. 通过将转换和输出操作应用于DStream来定义流式计算。
3. 开始接收数据并使用它进行处理streamingContext.start()
。
4. 等待处理停止(手动或由于任何错误)使用streamingContext.awaitTermination()
。
5. 可以手动停止处理streamingContext.stop()
。
1. 一旦上下文开始,就不能设置或添加新的流计算。
2. 一旦上下文停止,它将无法重新启动。
3. 只有一个StreamingContext可以在JVM中同时处于活动状态。
Spark Streaming提供了两类输入源。
· 基本来源:StreamingContextAPI中直接提供的资源。示例:文件系统,套接字连接。
1.文件系统:streamingContext.fileStream(hdfsDataDirectory)
SparkStreaming将监听目录dataDirectory
并处理在该目录中创建的任何文件(不支持嵌套目录中写入的文件)
文件必须具有相同的数据格式。
必须dataDirectory通过将数据原子移动或重命名为数据目录来创建文件。
移动后,文件不能更改。因为,如果文件被不断附加,则不会读取新的数据。
Sparkstreaming
监听对应主机-端口,处理发送到该端口的数据。
· 高级来源:Kafka,Flume等资源可以通过额外的实用类来获得。
实际应用场景中,Kafak使用较多,主要介绍Kafka的使用:
KafkaUtils.createStream(ssc, zkQuorum, groupId, topicsMap)
ssc:streamingContext
zkQuorum:kafka元数据在zookeeper中的存储地址(示例:node1:2181/kafka)
groupId:spark streaming接受kafka数据使用的用户组id,可通过该参数控制每次接受kafka数据的索引位置,spark streaming每次启动都会从该groupId上次接收到的数据位置开始接收。
topicsMap:Map[String, Int]类型对象,key对应接收的数据 topic名称,value为线程数量。sparkstreaming接收kafka数据的启动的线程数量,即并发量
如果要在流式应用程序中并行接收多个数据流,则可以创建多个输入DStream
操作 | 含义 |
map(func) | 通过传递源DStream的每个元素通过函数func返回一个新的DStream |
flatMap(func) | 与map类似,但每个输入项可以映射到0个或更多的输出项。 |
filter(func) | 通过仅选择func返回true 的源DStream的记录来返回新的DStream |
repartition(numPartitions) | 通过创建更多或更少的分区来更改此DStream中的并行级别。 |
union(otherStream) | 返回一个新的DStream,它包含源DStream和otherDStream中元素的并集。 |
count() | 通过计算源DStream的每个RDD中的元素数量来返回单元素RDD的新DStream |
reduce(func) | 通过使用函数func(它需要两个参数并返回一个),通过聚合源DStream的每个RDD中的元素来返回单元素RDD的新DStream 。该函数应该是关联的,以便可以并行计算。 |
countByValue() | 当调用类型为K的元素的DStream时,返回一个新的DStream(K,Long)对,其中每个键的值是源DStream的每个RDD中的频率。 |
reduceByKey(func,[numTasks]) | 当(K,V)对的DStream被调用时,返回(K,V)对的新DStream,其中使用给定的reduce函数聚合每个键的值。注意:默认情况下,使用Spark的默认并行任务数(2为本地模式,群集模式中的数字由config属性决定spark.default.parallelism )进行分组。您可以传递可选numTasks 参数来设置不同数量的任务。 |
join(otherStream,[numTasks]) | 当(K,V)和(K,W)对的两个DStream被调用时,返回一个新的(K,(V,W))对的DStream与每个键的所有元素对。 |
cogroup(otherStream,[numTasks]) | 当调用(K,V)和(K,W)对的DStream时,返回一个新的DStream(K,Seq [V],Seq [W])元组。 |
transform(func) | 通过对源DStream的每个RDD应用RDD到RDD函数来返回一个新的DStream。这可以用于对DStream进行任意RDD操作。 |
updateStateByKey(func) | 返回一个新的“状态”DStream,其中通过对键的先前状态应用给定的功能和键的新值来更新每个键的状态。这可以用于维护每个密钥的任意状态数据。 |
Transform操作:
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...)
val cleanedDStream = wordCounts.transform(rdd => {
rdd.join(spamInfoRDD).filter(...) ...
})
UpdateStateByKey 操作:
val lines = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap).map(_._2)
//产生我们需要的pair rdd
val linerdd = lines.map{row =>{
···
(key, amt)
}}
val addFunc = (currValues: Seq[Int], preValueState: Option[Int]) =>{
//通过spark内部的reducebykey按key规约,然后这里传入某key当前批次的seq,再计算key的总和
val currentCount = currValues.sum
//已经累加的值
val previousCount = preValueState.getOrElse(0)
//返回累加后的结果,是一个Option[Int]类型
Some(currentCount + previousCount)
}
linerdd.updateStateByKey[Int](addFunc _).print()
Windows操作
下图说明了这个窗口。
如图:
1. 红色的矩形就是一个窗口,窗口hold的是一段时间内的数据流。
2.这里面每一个time都是时间单元,在官方的例子中,每隔window size是3 time unit, 而且每隔2个单位时间,窗口会slide一次。
所以基于窗口的操作,需要指定2个参数:
· window length - The duration of the window (3 inthe figure)
· slide interval - The interval at which the window-basedoperation is performed (2 in the figure).
举个例子吧:
还是以wordcount举例,每隔10秒,统计一下过去30秒过来的数据。
val windowedWordCounts = pairs.reduceByKeyAndWindow(_ + _, Seconds(30), Seconds(10))
这里的paris就是一个DStream,每条数据类似(word,1)
一些常见的窗口操作如下。所有这些操作都采用上述两个参数 - windowLength和slideInterval。
操作 |
含义 |
window(windowLength,slideInterval) |
返回基于源DStream的窗口批次计算的新DStream。 |
countByWindow(windowLength,slideInterval) |
返回流中元素的滑动窗口数。 |
reduceByWindow(func,windowLength,slideInterval) |
返回一个新的单元素流,通过使用func在滑动间隔中通过在流中聚合元素创建。 |
reduceByKeyAndWindow(func,windowLength,slideInterval,[ numTasks ]) |
当对(K,V)对的DStream进行调用时,返回(K,V)对的新DStream,其中每个键的值 在滑动窗口中使用给定的减少函数func进行聚合。 |
countByValueAndWindow(windowLength, slideInterval,[numTasks ]) |
当调用(K,V)对的DStream时,返回(K,Long)对的新DStream,其中每个键的值是其滑动窗口内的频率。 |
输出操作允许将DStream的数据推送到外部系统,如数据库或文件系统。由于输出操作实际上允许外部系统使用变换后的数据,所以它们触发所有DStream变换的实际执行(类似于RDD的动作)。目前,定义了以下输出操作:
操作 |
含义 |
print() |
在运行流应用程序的驱动程序节点上的DStream中打印每批数据的前十个元素。 |
saveAsTextFiles(prefix,[ suffix ]) |
将此DStream的内容另存为文本文件。基于产生在每批间隔的文件名的前缀和后缀:“前缀TIME_IN_MS [.suffix]”。 |
saveAsObjectFiles(prefix,[ suffix ]) |
将DStream的内容保存为 |
saveAsHadoopFiles(prefix,[ suffix]) |
将此DStream的内容另存为Hadoop文件。基于产生在每批间隔的文件名的前缀和后缀:“前缀TIME_IN_MS [.suffix]”。 |
foreachRDD(func) |
对从流中生成的每个RDD 应用函数func的最通用的输出运算符。此功能应将每个RDD中的数据推送到外部系统,例如将RDD保存到文件,或将其通过网络写入数据库。请注意,函数func在运行流应用程序的驱动程序进程中执行,通常会在其中具有RDD动作,从而强制流式传输RDD的计算。 |
注意:
可以轻松地在流数据上使用DataFrames和SQL操作。您必须使用StreamingContext正在使用的SparkContext创建一个SparkSession。此外,必须这样做,以便可以在驱动程序故障时重新启动。
这在下面的示例中显示。将每个RDD转换为DataFrame,注册为临时表,然后使用SQL进行查询。
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()
}
可以通过训练出个模型,然后将模型作为广播变量,在DStream操作中使用该模型预测相关数据。
与RDD类似,DStreams还允许开发人员将流的数据保留在内存中。也就是说,使用persist()
DStream上的方法将自动将该DStream的每个RDD保留在内存中。如果DStream中的数据将被多次计算(例如,相同数据上的多个操作),这是非常有用的。对于基于窗口的操作,像reduceByWindow
和reduceByKeyAndWindow
和基于状态的操作一样updateStateByKey
,这是隐含的。因此,基于窗口的操作生成的DStreams将自动保留在内存中,无需开发人员的调用persist()
。
流式应用程序必须全天候运行,因此必须能够适应与应用程序逻辑无关的故障(例如,系统故障,JVM崩溃等)。为了可以这样做,Spark Streaming需要检查足够的信息到容错存储系统,以便可以从故障中恢复。
启用 checkpoint,需要设置一个支持容错的、可靠的文件系统(如 HDFS、s3 等)目录来保存 checkpoint 数据。通过调用 streamingContext.checkpoint(checkpointDirectory) 来完成。另外,如果你想让你的application能从 driver 失败中恢复,你的application 要满足:
· 若 application为首次重启,将创建一个新的 StreamContext 实例
· 如果 application是从失败中重启,将会从 checkpoint 目录导入 checkpoint 数据来重新创建 StreamingContext 实例
通过 StreamingContext.getOrCreate
可以达到目的:
def functionToCreateContext(): StreamingContext = {
val ssc = new StreamingContext(...) // new context
val lines = ssc.socketTextStream(...) // create DStreams
...
ssc.checkpoint(checkpointDirectory) // set checkpoint directory
ssc
}
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
```
context.start()
context.awaitTermination()
与spark提交方式一样
spark-submit
--class
:您的应用程序的入口点(例如org.apache.spark.examples.SparkPi
)
--master
:集群的主URL(例如spark://10.2.9.114:7077;提交到yarn集群写:yarn
)
--deploy-mode
:是否将驱动程序部署在工作节点(cluster
)或本地作为外部客户端(client
)(默认值:client
)†
--conf
:任意Spark配置属性,key = value格式。对于包含空格的值,用引号括起“key = value”(如图所示)。
application-jar
:包含应用程序和所有依赖关系的捆绑jar的路径。该URL必须在集群内全局可见,例如所有节点上存在的hdfs://
路径或file://
路径。
application-arguments
:参数传递给主类的main方法,如果有的话。
提交到Yarn集群的特殊参数:
--executor-memory 每个executor内存大小
--num-executors executor数量
--executor-cores executor cpu数量
1. 通过有效利用集群资源减少每批数据的处理时间。
2. 设置正确的批量大小,使得批量的数据可以像接收到的那样快速处理(即数据处理与数据摄取保持一致)。
val numStreams = 5
val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) }
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()
可以通过调优序列化格式来减少数据串行化的开销
例如HashMap
和LinkedList
等一些结构占用空间较大,可考虑优化使用对象类型。
使用广播功能可以大大减少群集上启动作业的成本。
task大都集中在特定的少数executor上执行,并行度不够。
原因:
这些点为receiver所在节点。Receiver会将接收到的数据的第一个副本放在本地,另外的副本随机分布在其他节点。党我们只设置一个副本时(e.g. MEMORY_ONLY_SER),数据会全部集中在receiver所在的几个节点,task也会被优先分发到这些点上的executor中执行。
原因:Spark Streaming存在执行一定时间后失败的问题
解决办法:定时重启Spark Streaming任务