1. 介绍
Spark Streaming是Spark生态系统中一个重要的框架,建立在Spark Core之上,与Spark SQL、GraphX、MLib相并列。
Spark Streaming是Spark Core的扩展应用,具有可扩展性、高吞吐量、可容错性等特点。
可以监控来自Kafka、Flume、HDFS、Twitter、Socket套接字等数据,通过复杂算法及一系列的计算分析数据,且可将分析结果存入HDFS、数据库或前端页面。
2. 工作原理
Spark的核心是RDD(或DataFrame)、对于Spark Streaming来说,它的核心是DStream。DStream是一系列RDD的集合,DStream可以按照秒数将数据流进行批量划分。
首先从接收到流数据之后,将其划分为多个批次,然后提交给Spark集群进行计算,最后将结果批量输出到HDFS、数据库或前端页面等。
详细可以参考下图:
图1
图2
当启动Spark Streaming应用的时候,首先会在一个节点的Executor上启动一个Receiver接收者,然后当从数据源写入数据的时候会被Recevier接收,接收到数据之后Receiver会将数据Split成多个Block,然后被分到各个节点(Replicate Blocks容灾恢复),然后Receiver向Streaming Context进行块报告,说明数据在哪几个节点的Executor上,接着在一定间隔时间内StreamingContext会将数据处理为RDD,并交给SparkContext划分到各个节点进行并行计算。
StreamingContext中内部定义了SparkContext,可以通过StreamingContext.sparkContetxt进行访问。
3. Spark Streaming Demo
官方给出的例子,是从Socket源端收集数据运行wordcount的案例。具体代码如下:
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object QuickStart {
def main(args: Array[String]): Unit = {
// 创建StreamingContext,包含2个线程,且批处理间隔为1秒
val conf = new SparkConf().setMaster("local[2]").setAppName("NetWorkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
// 创建DStream,数据源为TCP源
val lines = ssc.socketTextStream("localhost", 9999)
// 将每行文本且分为单词
val words = lines.flatMap(_.split(" "))
// 计算单词个数
val pairs = words.map(word => (word, 1))
val wordCount = pairs.reduceByKey(_ + _)
wordCount.print()
// 执行计算
ssc.start()
ssc.awaitTermination()
}
}
从Spark Streaming初始化的源码中可看到,其初始化有两种方式:
(1) 通过SparkConf来创建:如上例所示
(2) 通过SparkContext创建,可在Spark-Shell命令行中运行
例:在Spark-Shell中运行Spark Streaming,监控HDFS中的某个文件夹的数据传入,并按指定时间间隔统计词频
a. 编写SparkStreamingDemo.scala,并放置在Spark的某驱动节点上。
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
val ssc = new StreamingContext(sc, Seconds(10))
// read data
val lines = ssc.textFileStream("/music_logs/tmp/albumRecSta")
// process
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
b. 运行Spark-shell,以本地模式运行:spark-shell --master local[2]
c. 在Spark-Shell中加载代码,执行如下命令:
:load /home/hadoop/test/SparkStreamingDemo.scala
d. 然后上传任意文件到给定的HDFS目录下,通过观察Spark-Shell中的执行结果,可以实时查看Spark Streaming的处理。
4. 流程
通过上述代码,可以看出Spark Streaming的编程步骤:
(1) 初始化StreamingContext
(2) 定义输入源
(3) 准备流计算指令
(4) 利用streamingContext.start()启动接口和处理数据
(5) 处理过程一直持续,知道streamingContext.stop()被调用。
注意:
a. 一旦一个context已经启动,不能有新的流算子建立或加入到上下文中;一旦一个context已经停止,就不能再重新启动
b. 在JVM中,同一时间只能有一个StreamingContext处于活跃状态
c. streamingContext.stop()执行后,其sparkContext对象也会关闭。当stop设置参数为false时,只会关闭streamingContext。
d. 一个SparkContext可以重复利用去创建多个StreamingContext对象,前提条件是前面的StreamingContext在后面的StreamingContext创建之前关闭,且不关闭SparkContext。
5. 模块
(1) DStream
Spark Streaming提供的基本抽象,表示一个连续的工作流。可来自数据源获取、或者输入流通过转换算子生成处理。DStream由一系列连续的RDD组成,每个RDD都包含确定时间间隔内的数据。
上述代码中, flatMap操作应用于lines这个DStream的每个RDD,生成words这个DStream的过程如下:
(2) 输入DStream和Receiver
每个输入流DStream和一个Receiver对象关联,Receiver从源中获取数据,并将数据存入内存中用于处理。
Spark Streaming包含两类数据源:
a. 基本源:可在StreamingContext的API中直接引入,例如:文件系统(textFileStream)、套接字链接(socketTextStream)、Akka的actor等
b. 高级源:包括Kafka、Flume、Kinesis、Twitter等,需要额外的类来使用。如spark-streaming-kafka_2.10、spark-streaming-flume_2.10、spark-streaming-kinesis-asl_2.10、spark-streaming-twitter_2.10、spark-streaming-zeromq_2.10、spark-streaming-mqtt_2.10等
流应用中科创建多个输入DStream来处理多个数据流。将创建多个Receiver同时接收多个数据流。但是Receiver作为长期运行的任务运行在Spark的woker或executor中。因此占用一个核,所以需要考虑为Spark Streaming应用程序分配足够的核(如果本地运行,则为线程)。
注意:
a. 如果分配给应用程序的核数少于或等于输入DStreams或Receivers的数量,系统只能够接收数据而不能处理他们
b. 运行在本地时,只有一个核运行任务。
1) 基本源
a. 文件流:从任何与HDFS API兼容的文件系统中读取数据,创建方式:smc.fileStream[keyClass, valueClass, imputFormatClass](dataDirectory)
Spark Streaming将监控dataDirectory目录,并处理目录下生成的文件(嵌套目录不支持)。要求目录下的文件必须具有相同的数据格式;所有文件必须在dataDirectory目录下创建,文件时自动移动和重命名到目录下;一旦移动,文件必须修改。若文件被持续追加数据,新的数据不会被读取。
文件流不需要运行一个receiver,所以不需要分配核。
b. 自定义actor流:调用smc.actorStream(actorProps, actorName)方法从Akka actors获取数据流
c. RDD队列作为数据流:可调用smc.queueStream(queueOfRDDs)方法基于RDD队列创建DStreams。
代码示例:
a. 自定义Receiver
import java.io.PrintWriter
import java.net.ServerSocket
import scala.io.Source
/**
* 创建外部socket端,数据流模式器
*/
object StreamingSimulation {
def index(n: Int) = scala.util.Random.nextInt(n)
def main(args: Array[String]): Unit = {
// 调用该模拟器需要三个参数,文件路径、端口号、时间间隔
if(3 != args.length){
System.err.println("Usage: <fileName> <port> <millisecond>")
System.exit(1)
}
// 获取指定文件总行数
val fileName = args(0)
val lines = Source.fromFile(fileName).getLines.toList
val fileRow = lines.size
// 指定监听某端口,但外部程序请求时建立连接
val listener = new ServerSocket(args(1).toInt)
while (true){
val socket = listener.accept()
new Thread(){
override def run(): Unit = {
println("Got client connected from: " + socket.getInetAddress)
val out = new PrintWriter(socket.getOutputStream, true)
while(true){
Thread.sleep(args(2).toLong)
// 当该端口接收请求时,随机获取某行数据发送给对方
val content = lines(index(fileRow))
println("-------------------------------------------")
println(s"Time: ${System.currentTimeMillis()}")
println("-------------------------------------------")
println(content)
out.write(content + "\n")
out.flush()
}
socket.close()
}
}
}
}
}
View Code
import java.io.{BufferedReader, InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.{Logging, SparkConf}
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 自定义Receiver
*/
object CustomReceiver {
def main(args: Array[String]): Unit = {
if(2 > args.length){
System.err.println("Usage: CustomReceiver <hostName> <port>")
System.exit(1)
}
// Create the context with a 1 second batch size
val conf = new SparkConf().setAppName("CustomReceiver").setMaster("local[4]")
val smc = new StreamingContext(conf, Seconds(10))
// Create an input stream with the custom receiver on target ip:port and count the
// words in input stream of \n delimited text (eg. generated by 'nc')
val lines = smc.receiverStream(new CustomReceiver(args(0), args(1).toInt))
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
smc.start()
smc.awaitTermination()
}
}
class CustomReceiver(host: String, port: Int)
extends Receiver[String](StorageLevel.MEMORY_AND_DISK_SER_2) with Logging {
override def onStart(): Unit = {
// Start the thread that receives data over a connection
new Thread("Socket Receiver"){
override def run(): Unit = {
}
}
}
override def onStop(): Unit = {
// There is nothing much to do as the thread calling receive()
// is designed to stop by itself isStopped() returns false
}
/** Create a socket connection and receive data until receiver is stopped */
private def receive() {
var socket: Socket = null
var userInput: String = null
try {
logInfo("Connecting to " + host + ":" + port)
socket = new Socket(host, port)
logInfo("Connected to " + host + ":" + port)
val reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
userInput = reader.readLine()
while(!isStopped && userInput != null) {
store(userInput)
userInput = reader.readLine()
}
reader.close()
socket.close()
logInfo("Stopped receiving")
restart("Trying to connect again")
} catch {
case e: java.net.ConnectException =>
restart("Error connecting to " + host + ":" + port, e)
case t: Throwable =>
restart("Error receiving data", t)
}
}
}
View Code
b. 文件流
import java.util.logging.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* HDFS文件流
*/
object FileStreaming {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.spark").setLevel(Level.INFO)
Logger.getLogger("org.eclipse.jetty.Server").setLevel(Level.OFF)
val conf = new SparkConf().setAppName("fileStreamData").setMaster("local[2]")
val sc =new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(2))
//fileStream 用法
//val lines = ssc.fileStream[LongWritable, Text, TextInputFormat]("hdfs:///examples/").map{ case (x, y) => (x.toString, y.toString) }
//lines.print()
val lines = ssc.textFileStream("/root/application/dataDir/")
val wordCount = lines.flatMap(_.split(" ")).map(x => (x,1)).reduceByKey(_+_)
wordCount.print()
ssc.start()
ssc.awaitTermination()
}
}
View Code
c. 网络数据源
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 网络数据源:rawSocketStream
* Receives text from multiple rawNetworkStreams and counts how many '\n' delimited
* lines have the word 'the' in them. This is useful for benchmarking purposes. This
* will only work with spark.streaming.util.RawTextSender running on all worker nodes
* and with Spark using Kryo serialization (set Java property "spark.serializer" to
* "org.apache.spark.serializer.KryoSerializer").
* Usage: RawNetworkGrep <numStreams> <host> <port> <batchMillis>
* <numStream> is the number rawNetworkStreams, which should be same as number
* of work nodes in the cluster
* <host> is "localhost".
* <port> is the port on which RawTextSender is running in the worker nodes.
* <batchMillise> is the Spark Streaming batch duration in milliseconds.
*/
object RawNetWorkGrep {
def main(args: Array[String]): Unit = {
if (args.length != 4) {
System.err.println("Usage: RawNetworkGrep <numStreams> <host> <port> <batchMillis>")
System.exit(1)
}
val sparkConf = new SparkConf().setAppName("RawNetworkGrep")
val ssc = new StreamingContext(sparkConf, Seconds(args(3).toLong))
val rawStreams = (1 to 100).map(_ =>
ssc.rawSocketStream[String](args(0), args(1).toInt, StorageLevel.MEMORY_ONLY_SER_2)).toArray
val union = ssc.union(rawStreams)
union.filter(_.contains("the")).count().foreachRDD(r => println("Grep count: " + r.collect().mkString))
ssc.start()
ssc.awaitTermination()
}
}
View Code
d. TCP协议数据源
/**
* TCP协议的数据源
*/
object TcpOnStreaming {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("TCPOnStreaming example").setMaster("local[4]")
val sc = new SparkContext(conf)
val ssc = new StreamingContext(sc,Seconds(2))
// set the checkpoint directory
ssc.checkpoint("/Res")
// get the socket streaming data
val socketStreaming = ssc.socketTextStream("master", 9999)
val data= socketStreaming.map(x => (x, 1))
data.print()
val socketData = ssc.socketStream[String]("master", 9999, myDeserialize, StorageLevel.MEMORY_AND_DISK_SER)
socketData.print()
ssc.start()
ssc.awaitTermination()
}
def myDeserialize(data: InputStream): Iterator[String] = {
data.read().toString.map(x => x.hashCode().toString).iterator
}
}
View Code
e. RDD队列
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable
/**
* RDD队列
*/
class QueueStream {
val sparkConf = new SparkConf().setAppName("QueueStream").setMaster("local[4]")
// Create the context
val ssc = new StreamingContext(sparkConf, Seconds(1))
// Create the queue through which RDDs can be pushed to a QueueInputDStream
val rddQueue = new mutable.Queue[RDD[Int]]()
// Create the QueueInputDStream and use it do some processing
val inputStream = ssc.queueStream(rddQueue)
val mappedStream = inputStream.map(x => (x % 10, 1))
val reducedStream = mappedStream.reduceByKey(_ + _)
reducedStream.print()
ssc.start()
// Create and push some RDDs into rddQueue
for (i <- 1 to 30) {
rddQueue.synchronized {
rddQueue += ssc.sparkContext.makeRDD(1 to 1000, 10)
}
Thread.sleep(1000)
}
ssc.stop()
}
View Code
2) 高级源
a. kafka:【可参考, 】
import kafka.serializer.StringDecoder
import org.apache.spark.streaming.dstream.{DStream, InputDStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.immutable
/**
* Kafka与Spark Streaming结合示例
*
* @author songwang4
*/
object KafkaStreaming {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("SparkStreamingKafka_Receiver")
.setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))
createDStream(sc, ssc)
createDirectDStream(sc, ssc)
// 开启计算
ssc.start()
ssc.awaitTermination()
}
/**
* 直接采用KafkaUtils.createStream创建
* @param sc
*/
def createDStream(sc: SparkContext, ssc: StreamingContext): Unit ={
//开启wal预写日志,保存数据源的可靠性
sc.getConf.set("spark.streaming.receiver.writeAheadLog.enable", "true")
// 设置checkpoint
ssc.checkpoint("./Kafka_Receiver")
// 定义ZK地址
val zkQuorum = "node-1:2181,node-2:2181,node-3:2181"
// 定义消费组
val groupId = "spark_receiver"
// 定义topic相关信息(这里的value并不是topic分区数,它表示的topic中每一个分区被N个线程消费)
val topics = Map("kafka_spark" -> 2)
// 对接Kafka(这个时候相当于同时开启3个receiver接受数据)
val receiverDsStream: immutable.IndexedSeq[ReceiverInputDStream[(String, String)]] = (1 to 3).map(x => {
val stream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)
stream
})
val unionDStream: DStream[(String, String)] = ssc.union(receiverDsStream)
// 获取topic中的数据
val result: DStream[(String, Int)] = unionDStream.map(_._2).flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _)
result.print()
}
/**
* 采用KafkaUtils.createStream创建
* @param sc
*/
def createDirectDStream(sc: SparkContext, ssc: StreamingContext): Unit ={
// 配置Kafka相关参数
val kafkaParams = Map("metadata.broker.list" -> "node-1:9092,node-2:9092,node-3:9092", "group.id"->"Kafka_Direct")
// 定义topic
val topics = Set("kafka_spark")
// 采用是kafka低级api偏移量不受zk管理
val dStream: InputDStream[(String, String)] =
KafkaUtils.createDirectStream[String, String, StringDecoder,StringDecoder](ssc, kafkaParams, topics)
val result: DStream[(String, Int)] = dStream.map(_._2).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
result.print()
}
}
View Code
b. flume:【可参考, 】
6. DStream转换
DStream与RDD类似,允许将输入的DStream进行修改转换,常用的算子包括map(func), flatMap(func), filter(func), repartition(numPartitions), union(otherStream), count(), reduce(func), countByValue(), reduceByKey(func, [numTasks]), join(otherStream, [numTasks]), cogroup(otherStream, [numTasks]) , transform(func), updateStateByKey(func)。
其中:
(1) cogroup
当应用于两个DStream,一个包含(K, V),一个包含(K, W),返回一个包含(K, Seq[V], Seq[W])的元组
(2) updateStateByKey
UpdateStateByKey在Spark Streaming中可以每一个key通过checkpoint维护一份state状态,通过更新函数对该key的状态不断更新;对每一个新批次的数据而言,Spark Streaming通过使用upadteStateByKey为已经存在的key进行state状态更新(对每个新出现的key,会同样执行state的更新函数操作);但如果通过更新函数对state更新返回为none的话,此时key对应的state状态将被删除。
UpdateStateByKey中的state可以是任意类型的数据结构。
如果要不断的更新每个key的state,就一定会涉及到状态的保存和容错,这个时候就需要开启checkpoint机制和功能,需要说明的是checkpoint的数据可以保存一些存储在文件系统上的内容
关于流式处理对历史状态进行保存和更新具有重大实用意义,例如进行广告(投放广告和运营广告效果评估的价值意义,热点随时追踪、热力图)
例:向保持一个文本数据流中每个单词的运行次数
(3) transform
transform允许在DStream运行任何RDD-To-RDD函数,主要用于DStream API中未提供的RDD操作。例如join数据流中每个批次好另一个数据集的功能,未在DStream中提供,可简单使用transform实现。
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* 基于transform实现黑名单过滤
*/
object StreamingTransformTest {
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setAppName("BlackListFilter").setMaster("local[*]"))
val ssc = new StreamingContext(sc, Seconds(5))
// 设置黑名单
val blackListRdd = ssc.sparkContext.parallelize(Array(("jack", true), ("ws", false)), 3)
// 使用socketStreaming监听端口
val socketStream = ssc.socketTextStream("127.0.0.1", 8080)
val userStream = socketStream.map(line => (line.split(" ")(1), line))
// 基于leftOuterJoin进行过滤
val validStream = userStream.transform(rdd => {
val jRdd = rdd.leftOuterJoin(blackListRdd)
jRdd.filter(_._2._2.getOrElse(false))
}).map(_._2._1)
validStream.print()
ssc.start()
ssc.awaitTermination()
}
}
View Code
7. Window操作
窗口操作允许在一个滑动窗口数据上应用transformation算子。滑动窗口即窗口在源DStream上滑动,合并和操作落入窗内的源RDDs,产生窗口化的DStream的RDDs。下图是在三个时间单元的数据上进行窗口操作,并且每两个时间单元滑动一次。
window操作参数包括:(1) 窗口长度,即窗口持续时间; (2) 滑动的时间间隔,即窗口执行的时间间隔,注意,两个参数必须是源DStream的批时间间隔的倍数。
例:热点搜索词滑动统计,每个10秒,统计最近60秒的搜索词的搜索频次,并打印出排名靠前的3个搜索词及出现次数【参考:】
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamingWindowTest {
val sc = new SparkContext(new SparkConf().setAppName("WindowHotWordS").setMaster("local[*]"))
val ssc = new StreamingContext(sc, Seconds(5))
val searchLogsDStream = ssc.socketTextStream("spark1", 9000)
val searchWordPairDStream = searchLogsDStream.map(f => (f.split(" ")(1), 1))
/**
* 第二个参数是窗口长度;第三个参数是滑动间隔,即每隔10秒,将最近60秒的数据,作为一个窗口,进行内部的RDD聚合,然后统一对
* 一个RDD进行后续计算,然后,等待我们的滑动间隔到了以后,10秒到了,会将之前60秒的RDD,因为一个batch间隔是5秒,
* 所以之前60秒,就有12个RDD,给聚合起来,然后统一执行reduceByKey操作
* 所以这里的reduceByKeyAndWindow,是针对每个窗口执行计算的,而不是针对 某个DStream中的RDD
* 每隔10秒钟,出来 之前60秒的收集到的单词的统计次数
*/
val searchWordCountsDStream = searchWordPairDStream.reduceByKeyAndWindow((a: Int,b: Int) => a + b, Seconds(60), Seconds(10))
val finalDStream = searchWordCountsDStream.transform(searchWordCountsRDD => {
val countSearchWordRDD = searchWordCountsRDD .map(f => (f._2, f._1))
val sortedCountSearchWordsRDD = countSearchWordRDD.sortByKey(false)
val sortedSearchWordCountsRDD = sortedCountSearchWordsRDD.map(tuple => (tuple._1, tuple._2))
val top3SearchWordCounts = sortedSearchWordCountsRDD.take(3)
for (tuple <- top3SearchWordCounts) {
println("result : " + tuple)
}
searchWordCountsRDD
})
finalDStream.print()
ssc.start()
ssc.awaitTermination()
}
View Code
常用的窗口操作如下:
a. window(windowLength, slideInterval):基于源Dstream产生的窗口化的批数据计算一个新的DStream
b. countByWindow(windowLength, sildeInterval):对每个滑动窗口的数据执行count操作
c. reduceByWindow(func, windowLength, sildeInterval):对每个滑动窗口的数据执行reduce操作
d. reduceByKeyAndWindow(func, windowLength, slideInterval, [num Tasks]): 对每个滑动窗口的数据执行reduceByKey操作
e. countByValueAndWindow(windowLength, slideInterval, [num Tasks]):应用到一个(k, v)对组成的DStream中,返回一个由(k, v)对组成的新的DStream,每个key的值都是他们在滑动窗口中出现的频率
8. DStreams的输出操作
DStream的输出操作有如下几种:
a . print(): DStream的每个批数据中打印前10条元素。在开发和测试中常用。
b. saveAsObjectFiles(prefix, [suffix]):保存DStream的内容为一个序列化的文件,每一个批间隔文件的文件名基于prefix和suffox生成(prefix-TIME_IN_MS[.suffix]).
c. saveAsTextFiles(prefix, [suffix]):保存DStream的内容为一个文本文件。
d. saveAsHadoopFiles(prefix, [suffix]): 保存DStream的内容为一个hadoop文件。
e. foreachRDD(func):在从流生成的每个RDD上应用函数func的最通用的输出操作。函数可以将每个RDD中的数据推送到外部系统,如写到文件或数据库中
foreachRDD中常见的一般错误
a. 写数据到外部系统需要创建一个连接对象,可能不经意间在Spark的驱动创建一个连接对象,但在Spark worker中尝试调用这个连接对象保存记录到RDD中。由于需要先序列化连接对象,然后将它从driver发送到worker中,这样的连接对象在机器间不能传送,可能发生序列化错误或初始化错误。
dstream.foreachRDD(rdd => {
// 将在驱动服务器上执行
val connection = createNewConnection()
rdd.foreach(record => {
// 在worker节点上执行
connection.send(record)
})
})
View Code
b.正确的做法是在worker中创建连接对象。 还有一种常见的错误,即为每一条记录创建一个连接对象。创建一个连接对象有资源和时间的开支,将明显减少系统的整体吞吐量。
dstream.foreachRDD(rdd =>{
rdd.foreach(record => {
val connection = createNewConnection()
connection.send(record)
connection.close()
})
})
View Code
c. 更好的办法是利用rdd.foreachPartition方法,为RDD的partition创建一个连接对象,对该连接对象发送partition的所有记录。
dstream.foreachRDD(rdd =>{
rdd.foreachPartition(partitionRecords => {
val connection = createNewConnection()
partitionRecords .foreach(record => connection.send(record))
connection.close()
})
})
View Code
d. 最后通过 在多个RDD或批数据间重用连接对象,可以做更进一步的优化。可以保存一个静态的连接对象池,重复使用池中的对象将多批次的RDD推送到外部系统。
dstream.foreachRDD(rdd => {
rdd.foreachPartition(partitionRecords => {
// ConnectionPool是静态懒加载的连接池
val connection = ConnectionPool.getConnection()
partitiionRecords.foreach(record => connection.send(record))
// 返回连接池
ConnectionPool.returnConnection(connection)
})
})
View Code
注意:如果应用程序没有任何输出或者,存在输出操作dstream.foreachRDD(),但是没有任何的RDD的action操作存在dstream.foreachRDD中,name系统仅仅会接受输入,然后丢弃,因为DStreams的输出操作是懒执行的方式。
9. 缓存或持久化
DStream允许开发者持久化流数据到内存中。在DStream上使用persist方法。对于reduceByWindow、reduceByKeyAndWindow、updateStateKey操作,持久化是默认的,不需要调用persist方法。
10. Checkpointing
Spark Streaming应用程序如果不手动停止,则将一直运行下去,在实际中应用程序一般是24小时*7天不间断运行的,因此Streaming必须对诸如系统错误,JVM出错等与程序逻辑无关的错误(failures)具体很强的弹性,具备一定的非应用程序出错的容错性。Spark Streaming的Checkpoint机制便是为此设计的,它将足够多的信息checkpoint到某些具备容错性的存储系统如hdfs上,以便出错时能够迅速恢复。
存在两种checkpoint:
1) Metadata checkpointing:保存流计算的定义信息到容错存储系统如HDFS中,用于恢复应用程序中运行worker节点的故障。元信息包括:
a. Configuration: 创建Spark Streaming应用程序的配置信息
b. DStream operations: 定义Streaming应用程序的操作集合
c. Incomplete batches: 操作存在队列中未完成的批
2) Data checkpointing: 保存生成的RDD到可靠的存储系统中。
在有状态转换中是必须的。在这样的转换中,生成的RDD依赖于之前的批的RDD。随着时间推移,依赖链的长度会持续增长,在恢复的过程中,为了避免无限增长,有状态的转换的中间RDD将会定时地存储到可靠存储系统中。
应用程序在两种状态下必须开启checkpoint。
a. 使用有状态的转换。如用updateStateByKey、reduceByKeyAndWindow,checkpoint目录必须提供用以定期checkpoint RDD.
b. 从运行应用程序的driver的故障中恢复过来。使用元数据checkpoint恢复处理信息
在存储系统中设置一个目录用于保存checkpoint信息,可以通过streammingContext.checkpoint(checkpointDirectory)方法,该方法用于有状态的转换。
此外,如果想从driver故障中恢复,可以按照如下方式:
a. 当应用程序第一次启动,新建一个StreamingContext,启动所有Stream,然后调用start方法
b. 当应用程序因故障重启后,它将会从checkpoint目录重新创建StreamingContext。
// 创建病启动一个新的streamingcontext
def functionCreateContext(): StreamingContext = {
val ssc = new StreamingContext(...)
val lines = ssc.socketStreaming(...)
...
ssc.checkpoint(checkpointDirectory) // 设置checkpoint目录
ssc
}
// 从checkpoint数据获取StreamingContext或创建一个新的
val context = StreamingContext.getOrCreate(checkpointDirectory, functionCreateContext _)
// Do additional setup on context that needs to be done,
// irrespective of whether it is being started or restarted
context, ...
context.start()
context.awaitTermination()
View Code
若checkpointDirectory存在,上下文将会利用checkpoint数据重新创建,如果不存在,将会调用fuctionCreateContext函数创建一个新的上下文,建立DStream。
RDD的checkpoint有存储成本,需要认真设置批处理的时间间隔,典型地,设置checkpoint的间隔是DStream的滑动间隔的5-10倍大小。
Ps:
为了更好的容错保证,spark 1.2后引入了新的特性-预写日志(write ahead log)。使用该特性,从receiver获取的所有数据将预写日志到checkpoint目录。可以防止driver故障丢失数据,从而保证零数据丢失。
该功能可以通过设置参数spark.streaming.receiver.writeAheadLogs.enable为true时开启。
11. 性能优化
(1) 减少批数据的执行时间
1)数据接收的并行水平
创建多个输入DStream并配置他们可以从源中接收不同分区的数据流,从而实现多数据流接收。多个DStream被合并生成单个DStream,运用在单个输入DStream的转换操作可以运用在合并的DStream中。
val numSteams = 5
val kafakStreams = (1 to numStreams).map(i => KafkaUtils.createStream(...))
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()
View Code
另一个需考虑的参数是receiver的阻塞时间,可由配置参数spark.streaming.blockInterval决定,默认值200毫秒
2) 数据处理的并行水平
默认的并发任务数通过配置属性spark.default.parallelism确定。
3) 数据序列化
(2) 任务启动开支
1) 任务序列化,运行kyro,序列化任何可以减小任务的大小,从而减少任务发送到slave的时间
2) 执行模式:在Standalone模式或粗粒度Mesos模式下运行Spark可比细粒度的Mesos模式下运行Spark获得更短的任务启动时间
(3) 设置正确的批容量
找出正确的批容量的好方法,用一个保守的批间隔时间(5-10秒)和低数据速率来测试应用程序。
(4) 内存调优
减少Spark streaming应用程序垃圾回收的相关暂停,以获得更稳定的批处理时间:
a. DStream的默认持久化级别是存储到内存中
b. 默认情况下,通过Spark的LUR内置策略,SPakr Streaming生成的持久化RDD会将从内存中清理掉。然而,可以设置配置选项spark.streaming.unpersist为true来更智能地去持久化(unpersist)RDD。
c. 使用并发的标记-清除垃圾回收可以进一步减少垃圾回收的暂停时间。