Spark Streaming接收数据并将其分隔成一批批的数据,然后被Spark engine处理形成一批批的结果。需指出,Spark Streaming可以被应用与机器学习和图计算。
Spark Streaming提供了一个高级抽象称为DStream,代表连续的数据流。DStream可从kafka、flume、kinesis等数据源创建,DStream内部是一个RDDs序列。
StreamingContext是所有流处理的入口,下例创建一个2线程的本地StreamingContext,1秒一批
import org.apache.spark._
import org.apache.spark.streaming._
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
// 创建一个输入DStream代表TCP数据源,监听localhost 9999端口
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
wordCounts.print()
// 开始运行,接收并处理数据
ssc.start()
// 阻塞至运算终止(手动停止或出现错误),也可使用streamingContext.stop()停止处理过程
ssc.awaitTermination()
StreamingContext 可以由一个已知的SparkContext 对象创建
val ssc = new StreamingContext(sc, Seconds(1)) //sc 已知的SparkContext 对象
一旦context 启动,将不能增加新的流运算
一旦context终止,将不能被重启
一个JVM只允许一个Streamingcontext存在
stop()除了停止StreamingContext同时也停止SparkContext,如果仅停止Streamingcontext,则调用stop(false)
一个SparkContext可被多个StreamingContext重用,只要在新的被创建之前停止旧的
DStream内部是一个连续的RDDs序列,就像下图:
任何在DStream上的操作都相应转换到下一级RDDs,比如:
这些转换操作在底层隐藏了众多细节,由Spark engine执行。
每一个输入DStream代表从数据源接收数据,都跟一个Receiver对象(接收数据在被处理之前,将数据存储在内存)相关联。Spark Streaming提供两种支持的数据源:
基础数据源:从StreamingContext API直接使用,比如文件系统,socket连接
高级数据源:比如kafka、flume、kinesis等,通过额外的工具包引入
在本地使用Spark Streaming时,不要用“local” 或 “local[1]” 设置master url,因为基于receiver的DStream除了用一个线程跑receiver以外,还需要另外的线程来处理数据。所以通常使用“local[n]”。提交到集群上,分配给Spark Streaming应用的核数要多于receiver,否则没有多余的核来处理数据。
文件流用于从外部文件创建输入Dtream,创建方式如StreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass],它读取任何支持HDFS API的文件系统(HDFS、S3、NFS等)文件。
// 针对其他支持HDFS API的文件系统文件
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)
// 针对文本文件,最简单的方式
streamingContext.textFileStream(dataDirectory)
文件必须格式相同
文件进入dataDirectory的能被处理的正确方式:移动或者重命名
一旦文件被处理完成,即便修改了,新数据也不会再读取
RDD流一般用于测试,使用streamingContext.queueStream(queueOfRDDs)来创建输入DStream。
val queueOfRDDs = new mutable.Queue[RDD[Int]]()
// 创建DStream
val inputDStream = ssc.queueStream(queueOfRDDs,oneAtATime = false)
首先需要实现一个自定义Receiver,通过实现Receiver接口并实现onStart()、onStop()方法
class CustomReceiver(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) with Logging {
def onStart() {
// 开启一个线程用于接收数据
new Thread("Socket Receiver") {
override def run() { receive() }
}.start()
}
def onStop() {
// 这里没什么要做的,因为receive()内while循环条件isStopped返回false时自行停止
}
// 创建一个socket连接接收数据,知道receiver停止
private def receive() {
var socket: Socket = null
var userInput: String = null
try {
socket = new Socket(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()
// 重启,尝试重连
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)
}
}
}
然后在Spark Streaming程序中使用自定义receiver
val customReceiverStream = ssc.receiverStream(new CustomReceiver(host, port))
val words = customReceiverStream.flatMap(_.split(" "))
...
下面常用转换功能跟RDD类似
转换 | 意义 |
---|---|
map(func) | 映射 |
flatMap(func) | 一个元素拆分成一个序列 |
filter(func) | 过滤 |
repartition(numPartitions) | 重分区 |
union(otherStream) | 两个DStream的并集 |
count() | 统计DStream中每个RDD的元素个数,并返回一个DStream |
reduce(func) | 规约DStream中每个RDD的值为一个单值 |
countByValue() | 对数据类型为K的DStream调用此方法,返回一个新(K, Long) 对的DStream,其中long值为每个key在RDD中出现的个数 |
reduceByKey(func, [numTasks]) | 按key规约 |
join(otherStream, [numTasks]) | 两个DStream做连接,返回一个 (K, (V, W)) 对的新DStream |
cogroup(otherStream, [numTasks]) | 两个DStream联合分组,返回一个 (K, Seq[V], Seq[W]) 对的新DStream |
updateStateByKey转换使我们可以用新数据连续的更新被保存的任意状态。通过下面两步来使用这个功能:
// 定义更新状态方法,参数values为当前批次单词频度,state为以往批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _)
val previousCount = state.getOrElse(0)
Some(currentCount + previousCount)
}
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
// 使用updateStateByKey来更新状态,统计从运行开始以来单词总的次数
val stateDstream = pairs.updateStateByKey[Int](updateFunc)
stateDstream.print()
Transform允许在DStream上执行任意的RDD-to-RDD函数。如果在DStream的API中没有相应的RDD操作,通过该函数扩展Spark RDD API。每一批次调度一次。
// 包含spam的RDD
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...)
val cleanedDStream = wordCounts.transform { rdd =>
// 将spam与数据Rdd做连接进行数据清晰
rdd.join(spamInfoRDD).filter(...)
...
}
Spark Streaming提供一种窗口计算,允许你再一个滑动的窗口内做跨越多个批次的转换操作。
每一次滑动窗口,落入窗口内的RDDs被合并、处理产生出一个新的RDDs。就像上例,每个窗口整合三个单位的数据,并且滑动两次。这说明窗口转换需要两个参数:
// 每10s规约一次最近30s内的数据
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
其他窗口转换:
窗口转换 | 意义 |
---|---|
window(windowLength, slideInterval) | 创建基于窗口批次的新DStream |
countByWindow(windowLength, slideInterval) | 窗口内元素的个数 |
reduceByWindow(func, windowLength, slideInterval) | 按func规约窗口内的值 |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | 针对键值对数据,按func规约窗口内的值 |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | 上一个的高效版本,利用上一个窗口的结果,从而达到增量计算的目的。就是多了一个反相计算的函数 |
countByValueAndWindow(windowLength, slideInterval, [numTasks]) | 返回窗口内不同类型值的个数 |
val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)
val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)
val dataset: RDD[String, String] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) }
DStream API
PairDStream API
在driver上打印DStream中每一批数据的最开始10个元素。
以文本文件形式存储DStream的数据。每一批的存储文件名基于参数中的prefix和suffix,如”prefix-Time_IN_MS[.suffix]”。
以Java对象序列化的方式将DStream中的数据保存为 SequenceFiles。每一批的存储文件名基于参数,比如为"prefix-TIME_IN_MS[.suffix]"。
保存为Hadoop files。每一批的文件名基于参数,比如为"prefix-TIME_IN_MS[.suffix]"。
将函数 func 用于DStream的每一个RDD。其中传入函数func应实现将每一个RDD中数据存储到外部系统,如存入文件或者写入数据库。
// 常见错误用法一
dstream.foreachRDD { rdd =>
// 在 driver上执行
val connection = createNewConnection()
rdd.foreach { record =>
// 在 worker上执行。这时会把connection对象序列化发送到work节点上,
// 但connection不能序列化,会报初始化错误或不能序列化
connection.send(record)
}
}
// 常见错误用法二
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
// 此时,在worker上创建连接。但每一条记录都创建一次连接,资源消耗大,不必要
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
// 不完美用法
dstream.foreachRDD { rdd =>
// 为每一个分区创建一个连接。一个连接处理一个分区的数据。
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
// 最佳实践
dstream.foreachRDD { rdd =>
// 为每一个分区创建一个连接。一个连接处理一个分区的数据。
rdd.foreachPartition { partitionOfRecords =>
// 将连接池化。ConnectionPool 是一个静态、懒加载的连接池
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
// 使用完连接以后退还给连接池,下次复用
ConnectionPool.returnConnection(connection)
}
}
// 优化以后的最佳实践
dstream.foreachRDD { rdd =>
// 为每一个分区创建一个连接。一个连接处理一个分区的数据。
rdd.foreachPartition { partitionOfRecords =>
// 将连接池化。ConnectionPool 是一个静态、懒加载的连接池
val connection = ConnectionPool.getConnection()
var recordList = partitionOfRecords.toList
// 批量发送
connection.sendBatch(recordList)
// 使用完连接以后退还给连接池,下次复用
ConnectionPool.returnConnection(connection)
}
}