概述
spark streaming是spark core API的扩展,它支持可伸缩、高吞吐量、容错的实时数据流处理。数据可以从多种来源(如Kafka、Flume、Kinesis或TCP套接字)中摄取,并且可以使用复杂的算法来处理,这些算法有高级函数表示,如map、reduce、join和window。最后,可以将处理过的数据推送到文件系统、数据库和活动仪表板。实际上,你可以在流上应用Spark的机器学习和图形处理算法。
在内部,它的工作方式如下。spark streaming接收实时输入数据流,并将数据分成几个批次,然后由spark引擎进行处理,生成最终的结果流。
Spark提供了一种称为discretized stream(DStream)的高级抽象,它表示连续的数据流。DStreams可以从Kafka、Flume和Kinesis等源的输入数据流中创建,也可以通过对其它DStreams应用高级操作来创建。在内部,DStream被表示为RDDs的序列。
Linking
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.4.0</version>
</dependency>
初始化 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))
还可以从现有的SparkContext对象创建StreamingContext对象。
import org.apache.spark.streaming._
val sc = ... // existing SparkContext
val ssc = new StreamingContext(sc, Seconds(1))
离散流(DStreams)
DStream是SparkStreaming提供的基本抽象。它表示连续数据流,或者表示从源接收的输入数据流,或者表示通过转换输入流生成的处理数据流。在内部,DStream由一系列连续的RDDs表示,这是Spark对不可变的分布式数据集的抽象。DStream中的每个RDD包含一定间隔的数据,如下图所示。
在DStream上应用的任何操作都会转换为对底层RDDs的操作。例如,在前面将行流转换为单词的示例中,对行DStream中的每个RDD应用平台地图操作,以生成单词DStream的RDD。如下图所示。
这些底层的RDD转换由spark引擎计算。DStream操作隐藏了大部分这些细节,并为开发人员提供了更高级别的API以方便使用。这些操作将在后面的部分中详细讨论。
输入数据流和接收器
spark提供了两类内置流源
基本数据源:StreamingContext API中的直接可用源。示例:文件系统和套接字连接。
高级数据源:Kafka,Flume,Kinesis等。可以通过额外的实用程序类获得。如链接部分中所讨论的,这需要针对额外的依赖项进行链接。
自定义数据源:你所要做的就是实现一个用户定义的receiver,它可以接收来自定义源的数据并将其推入Spark。
Receiver可靠性:Reliable Receiver 和 Unreliable Receiver
DStreams转换操作
Transformation | Meaning |
---|---|
map(func) | 通过函数传递源DStream的每个元素,返回一个新的DStream |
flatMap(func) | 类似于map,但是每个输入项都可以映射到0或多个输出项 |
filter(func) | 仅通过选择func返回true的源DStream的记录来返回新的DStream |
repartition(numPartitions) | 通过创建更多或更少的分区来更改此数据流中的并行度级别。 |
union(otherStream) | 返回一个新的DStream,其中包含源DStream和其他DStream中元素的合并。 |
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,其中每个键的值使用给定的约简函数进行聚合。注意:默认情况下,这将使用SPark的默认并行任务数(本地模式为2,而在集群模式下,该数量由config属性spark.default.parallelism确定)来进行分组。您可以传递一个可选的numTask参数来设置不同数量的任务。 |
join(otherStream, [numTasks]) | 当在(k,v)和(k,w)对的两个数据流上调用时,返回(k,(v,w))对的新的DStream,每对具有用于每个键的所有对元素。 |
cogroup(otherStream, [numTasks]) | 当调用(K,V)和(K,W)对的DStream时,返回一个新的DStream(K,Seq[V],Seq[W])元组。 |
transform(func) | 通过将RDD到-RDD函数应用到源DStream的每个RDD中,返回一个新的DStream。这可以用于在DStream上执行任意的RDD操作。 |
updateStateByKey(func) | 返回一个新的“状态”DStream,其中每个键的状态通过在键的前一个状态上应用给定的函数和键的新值来更新。这可用于维护每个键的任意状态数据。 |
Window操作
Spark Streaming还提供窗口计算,允许您在滑动数据窗口上应用转换。下图说明了这个滑动窗口。
如图所示,每当窗口在源DStream上滑动时,属于该窗口的源RDD被组合并被操作以生成窗口DStream的RDDs。在这种特殊情况下,操作将应用于最后3个时间单位的数据,并由2个时间单元滑动。这表明任何窗口操作都需要指定两个参数。
- 窗口长度-窗口的持续时间(图中为3)。
- 滑动间隔-执行窗口操作的间隔(图中为2)。
这两个参数必须是源DStream的批处理间隔的倍数(图中为1)。
让我们用一个例子来说明窗口操作。比方说,您希望通过每10秒在最后30秒的数据中生成字数来扩展前面的示例。要做到这一点,我们必须在最后30秒的数据中对DStream(Word,1)对应用还原ByKey操作。这是使用ReduceByKeyAndWindow操作完成的。
// Reduce last 30 seconds of data, every 10 seconds
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
一些常见的窗口操作如下。所有这些操作都采用上述两个参数-窗口长度和幻灯片Interval。
Transformation | Meaning |
---|---|
window(windowLength, slideInterval) | 返回一个新的DStream,它是基于源DStream的加窗批计算的。 |
countByWindow(windowLength, slideInterval) | 返回流中元素的滑动窗口计数。 |
reduceByWindow(func, windowLength, slideInterval) | 返回一个新的单元素流,它是通过使用func将流中的元素聚合在滑动间隔上创建的。函数应该是相联的和可交换的,这样才能正确地并行计算。 |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | 当调用(K,V)对的DStream时,返回一个新的(K,V)对的DStream,其中每个键的值使用给定的减函数在滑动窗口中的批上进行聚合。注意:默认情况下,这将使用Spark的默认并行任务数(本地模式为2,而在集群模式下,该数量由config属性spark.default.parallelism确定)来进行分组。您可以传递一个可选的numTask参数来设置不同数量的任务。 |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | 一个更有效的版本,上面的ReduceByKeyAndWindow(),其中每个窗口的减少值是使用前一个窗口的约简值递增计算的。这是通过减少进入滑动窗口的新数据和“逆还原”离开窗口的旧数据来完成的。一个例子是窗口滑动时键的“加”和“减”计数。然而,它只适用于“可逆约简函数”,即具有对应的“逆约”函数(作为参数inFunc)的约简函数。与ReduceByKeyAndWindow一样,可以通过一个可选参数来配置减缩任务的数量。注意,必须启用检查点才能使用此操作。 |
countByValueAndWindow(windowLength, slideInterval, [numTasks]) | 当在(k,v)对的数据流上调用时,返回(k,long)对的新DStream,其中每个键的值是其在滑动窗口内的频率。类似于“减少关键点”和“窗口”,通过可选参数可配置减少任务的数量。 |
DStreams输出操作
Output Operation | Meaning |
---|---|
print() | 在运行流应用程序的驱动程序节点上打印数据流中每一批数据的前十个元素。这对于开发和调试是有用的。在PythonAPI中称为PPRINT()。 |
saveAsTextFiles(prefix, [suffix]) | 将此DStream的内容保存为文本文件。每个批处理间隔处的文件名都是基于前缀和后缀:“前缀-时间_IN_MS[.后缀]”生成的。 |
saveAsObjectFiles(prefix, [suffix]) | 将此DStream的内容保存为序列化Java对象的SequenceFiles。每个批处理间隔处的文件名都是基于前缀和后缀:“前缀-时间_IN_MS[.后缀]”生成的。这在PythonAPI中是不可用的。 |
saveAsHadoopFiles(prefix, [suffix]) | 将这个DStream的内容保存为Hadoop文件。每个批处理间隔处的文件名都是基于前缀和后缀:“前缀-时间_IN_MS[.后缀]”生成的。这在PythonAPI中是不可用的。 |
foreachRDD(func) | 将函数func应用于从流生成的每个RDD的最通用的输出操作符。此函数应将每个RDD中的数据推送到外部系统,例如将RDD保存到文件,或通过网络将其写入数据库。请注意,函数func是在运行流应用程序的驱动进程中执行的,其中通常包含RDD操作,这将强制计算流RDD。 |
foreachRDD设计模式
foreachRDD是一个强大的原语,允许将数据发送到外部系统。然而,理解如何正确有效地使用这个原语是很重要的。要避免的一些常见错误如下。
将数据写入外部系统通常需要创建连接对象(例如,到远程服务器的TCP连接),并使用它将数据发送到远程系统。为此,开发人员可能会无意中尝试在spark驱动程序中创建一个连接对象,然后尝试在火花工作人员中使用它来保存RDD中的记录。例如(在Scala中)。
dstream.foreachRDD { rdd =>
val connection = createNewConnection() // executed at the driver
rdd.foreach { record =>
connection.send(record) // executed at the worker
}
}
这是不正确的,因为这要求将连接对象序列化并从驱动程序发送到工作人员。这样的连接对象很少可以在机器之间传输。此错误可能表现为序列化错误(连接对象不可序列化)、初始化错误(需要在工作人员处初始化连接对象)等等。正确的解决方案是在员工处创建连接对象。然而,这可能导致另一个常见的错误-为每个记录创建一个新的连接。例如:
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
通常,创建连接对象需要时间和资源开销。因此,为每个记录创建和销毁一个连接对象可能会产生不必要的高开销,并会显著降低系统的总体吞吐量。更好的解决方案是使用rdd.foreachPartition-创建一个连接对象并使用该连接发送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
}
}
注意,池中的连接应该根据需要延迟创建,如果不使用一段时间,则超时。这实现了向外部系统最有效地发送数据。
DataFrame和SQL操作
你可以轻松地对流数据使用DataFrames和SQL操作。您必须使用StreamingContext正在使用的SparkContext创建SparkSession。此外,必须这样做,以便能够在驱动程序故障时重新启动。这是通过创建一个延迟实例化的SparkSession实例来完成的。下面的示例显示了这一点。它修改前面的单词计数示例,以使用DataFrames和SQL生成单词计数。每个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()
}
Checkpointing
使用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()
Accumulators, Broadcast Variables, and Checkpoints
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[(String, Int)], 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
})