文章目录
1. SparkStreaming 简介
SparkStreaming 是流式处理框架,是 Spark API 的扩展。支持可扩展、高吞吐量、容错的实时数据流处理。实时数据的来源可以是:Kafka、Flume、Twitter、ZeroMq 或者是 TCP sockets,并且可以使用高级功能的复杂算子来处理流数据。例如:map、reduce、join、window。最终,处理后的数据可以存放在文件系统、数据库等,方便实时展示。
2. SparkStreaming 与 Storm 的区别
- Storm 是纯实时的流式处理框架,SparkStreaming是准实时的处理框架 (微批处理)。因为微批处理,SparkStreaming 的吞吐量比 Storm 要高。
- Storm 的事物机制要比 SparkStreaming 的完善。
- Storm 支持动态资源调度。(Spark1.2 开始也支持)。
- SparkStreaming 擅长复杂的业务处理。Storm 不擅长复杂的业务处理。擅长简单的汇总行的计算。
3. SparkStreaming 初始
3.1 SparkStreaming 初始理解
注意:
-
receiver task 是 7*24 小时一直在执行,一直接受数据。将一段时间内接收来的数据保存到 batch 中。假设 batchInterval 为 5s,那么会将接收过来的数据每隔 5s 封装到一个 batch 中,batch 没有分布式计算特性,这一个 batch 的数据又被封装到一个 RDD 中,RDD 最终封装到一个 DStream 中。
例如:假设 batchInterval 为 5s,每隔 5s 通过 SparkStreaming 将得到一个 Dstream,然后计算这 5s 的数据。假设执行任务的时间是 3s,那么第 6~9 秒一边接收数据,一边在计算任务,9~10 秒只是在接收数据。然后再第 11s 的时候重复上面的操作。
-
如果 job 执行的时间大于 batchInterval 会有什么样的问题?
如果接收过来的数据设置的级别是仅内存,接收过来的数据会越积越多。最后可能会导致 OOM。如果设置 StorageLevel 包含 disk,则内存存放不下的数据会溢写到磁盘,这样会加大延迟。
3.2 SparkStreaming 代码
3.2.1 socket 生产数据
import org.apache.commons.lang3.RandomUtils;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class NcServer implements Runnable{
String[] words = {
"java","python","spark","flink","hive","hadoop","hbase"};
private Socket socket;
public NcServer(Socket socket) {
this.socket = socket;
}
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(8080);
ExecutorService es = Executors.newSingleThreadExecutor();
while (true){
Socket socket = serverSocket.accept();
System.out.println("接受请求");
es.execute(new NcServer(socket));
}
}
@Override
public void run() {
try {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
while (true) {
String word = words[RandomUtils.nextInt(0,words.length)];
System.out.println(word);
out.write(word+"\n");
out.flush();
TimeUnit.MILLISECONDS.sleep(1000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.2.2 SparkStreaming 代码注意事项
- receiver 模式下接收数据,local 的模拟线程必须大于等于 2,一个线程用来 receiver 接收数据。另一个线程用来执行 job。
Durations
时间设置就是我们能接收数据的延迟度。这个需要根据集群的资源情况以及任务的执行情况来调节。- 创建
StreamingContext
方式有两种:StreamingContext(conf,Durations.seconds(5))
和StreamingContext(sc,Durations.seconds(5))
。 - 所有的代码逻辑完成后要有一个 output operation 类算子。
StreamingContext.start()
后不能添加业务逻辑StreamingContext.stop()
无参的 stop 方法将SparkContext
一同关闭。stop(false)
,不会关闭SparkContext
。并且之后不能调用start()
。
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{
Durations, StreamingContext}
import org.apache.spark.{
SparkConf, SparkContext}
object WordCountFromSocket {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setAppName("WordCountOnLine").setMaster("local[2]")
// 通过 SparkConf 创建 StreamingContext 这种方式默认会创建SparkContext
val ssc = new StreamingContext(conf,Durations.seconds(5))
ssc.sparkContext.setLogLevel("ERROR")
// 通过 SparkContext 创建 StreamingContext
//val sc = new SparkContext(conf)
//val ssc = new StreamingContext(sc,Durations.seconds(5))
//从ssc中获取SparkContext()
// val context: SparkContext = ssc.sparkContext
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("127.0.0.1",8080)
val words: DStream[String] = lines.flatMap(line=>{
line.split(" ")})
val pairWords: DStream[(String, Int)] = words.map(word=>{
(word,1)})
val result: DStream[(String, Int)] = pairWords.reduceByKey((v1, v2)=>{
v1+v2})
// 输出
result.print()
// 启动
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}
4. SparkStreaming 算子操作
4.1 foreachRDD
output operation 算子,必须对抽取出来的 RDD 执行 action 类算子,代码才能执行。
result.foreachRDD(wordCountRDD=>{
println("******* 在 Driver 端执行 *******")
val sortRDD: RDD[(String, Int)] = wordCountRDD.sortByKey(false)
val result: RDD[(String, Int)] = sortRDD.filter(tp => {
println("******* Executor 端执行 *******")
true
})
result.foreach(println)
})
注意事项:
foreachRDD
中可以拿到DStream
中的 RDD,对 RDD 进行操作,但是一点要使用 RDD 的 action 算子触发执行,不然 DStream 的逻辑也不会执行。froeachRDD
算子内,除了拿到的 RDD 算子操作外,这段代码是在 Driver 端执行的,可以利用这点做到动态的改变广播变量。
4.2 transform
-
transformation 类算子
-
可以通过 transform 算子,对 DStream 做 RDD 的任意操作。
//广播变量 val filterWord = ListBuffer[String]("java","flink") val broadcastWord: Broadcast[ListBuffer[String]] = ssc.sparkContext.broadcast(filterWord) words.transform(rdd =>{ println("在 Driver 端执行") // 修改广播变量 val bword: ListBuffer[String] = broadcastWord.value bword += "hadoop" println(broadcastWord.value) val filterRdd = rdd.filter((word: String) => { val wordList = broadcastWord.value !wordList.contains(word) }) filterRdd }).map(word =>(word,1)).reduceByKey(_+_).print()
注意事项:
transform
算子可以拿到 DStream 中的 RDD,对 RDD 使用 RDD 的算子操作,但是最后要返回 RDD,返回的 RDD 又被封装到一个 DStream。transform
中拿到的 RDD 的算子外,代码是在 Driver 端执行的。可以做到动态的改变广播变量。
4.3 updateStateByKey
-
transformation 算子
-
updateStateByKey 作用:
- 为 SparkStreaming 中每个 key 维护一份 state 状态,state 类型可以是任意类型的,也可以是一个自定义的对象。更新函数也可以自定义。
- 通过更新函数对该 key 的状态不断更新。对于每个新的 batch 而言,SparkStreaming 会在使用
updateStateByKey
的时候为已经存在的 key 进行 state 的状态更新。
-
使用
updateStateByKey
要开启 checkpoint 机制和功能。 -
什么时候会把内存中的 state 写入到磁盘?
如果 batchInterval 设置的时间小于 10s,那么每 10s 写入磁盘一次。
如果 batchInterval 设置的时间大于 10s,那么就会按照 batchInterval 的时间间隔写入磁盘一次。这样做的目的是减少写入磁盘的次数。
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{
DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{
Durations, StreamingContext}
object UpdateStateByKey {
def main(args: Array[String]): Unit = {
val conf = new SparkConf();
conf.setAppName("UpdateStateByKey")
conf.setMaster("local[2]")
val ssc = new StreamingContext(conf,Durations.seconds(5))
// 设置日志级别
ssc.sparkContext.setLogLevel("ERROR")
val host = "127.0.0.1"
val port = 8080
val lines: ReceiverInputDStream[String] = ssc.socketTextStream(host,port)
val words = lines.map(word => (word,1))
val path = "/app/checkpoint/updatStateByKey";
ssc.sparkContext.setCheckpointDir(path)
/**
* currentValues:当前批次某个 key 对应所有的 value 组成的一个集合。
* preValue:以往批次当前 key 对应的总状态值。
*/
val result: DStream[(String, Int)] = words.updateStateByKey((currentValues: Seq[Int], preValue: Option[Int]) => {
var totalValue = preValue.getOrElse(0)
for (value <- currentValues) {
totalValue += value
}
Option(totalValue)
})
result.print()
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}
4.4 window
4.4.1 window 操作图解
假设每隔 5 秒 1 个batch,上图中窗口长度为 15s,窗口滑动间隔 10s。
窗口长度和滑动间隔必须是 batchInterval 的整数倍。如果不是整数倍会检测报错。
4.4.2 window 操作优化
checkpoint
保存上一次执行的结果。- 执行当前 window 操作的时候,把上一次的执行结果加上这次的结果,然后减去这次滑出去的结果。
object WindowWordCount {
def main(args:Array[String]) {
Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
val conf = new SparkConf().setAppName("WindowWordCount").setMaster("local[2]")
val sc = new SparkContext(conf)
//创建StreamingContext
val ssc = new StreamingContext(sc, Seconds(5))
//获取从Socket发送过来的数据
val hostname = "127.0.0.1"
val port = 8080
val lines = ssc.socketTextStream(hostname, port, StorageLevel.MEMORY_ONLY_SER)
val words = lines.flatMap(_.split(","))
// windows操作
words.map(x =>(x, 1)).reduceByKeyAndWindow((a:Int, b:Int) => (a+b),Seconds(5), Seconds(5)).print();
// 优化 window 操作
// 定义checkpoint目录
val path = "/app/checkpoint/window"
ssc.checkpoint(path)
words.map(w =>(w,1)).reduceByKeyAndWindow(
(v1:Int,v2:Int) => {
v1+v2},
(v1:Int,v2:Int) => {
v1-v2},
Seconds(15),
Seconds(5)
).print()
ssc.start()
ssc.awaitTermination()
}
}
5. Driver HA (Standalone 或 Mesos)
因为 SparkStreaming 是7*24 小时运行,Driver 是一个简单的进程,有可能会挂掉,所以实现 Driver 的 HA 就有必要。如果使用的 Client 模式无法实现 Driver HA。这里针对的是 cluster 模式。Yarn 平台 cluster 模式提交任务,AM (ApplicationMaster) 相当于 Driver,如果挂掉会自动启动 AM 。这里所说的 Driver HA 针对的是 Spark standalone 和 Mesos 资源调度的情况下。实现 Driver 的高可用有两个步骤:
-
第一 :提交任务的时候要加上
--supervise
,当 Driver 挂掉的时候回自动重启 Driver。 -
第二:使用
StreamingContext.getOrCreate(checkpointPath,creatingFunc)
。Driver中元数据包括:
- 创建应用程序的配置信息。
- DStream 的操作逻辑。
- job 中没有完成的批次数据,也就是 job 的执行进度。
package com.abcft.spark.streaming import org.apache.log4j.{ Level, Logger} import org.apache.spark.{ SparkConf, SparkContext} import org.apache.spark.storage.StorageLevel import org.apache.spark.streaming.{ Seconds, StreamingContext} /** * Driver HA : * 1.在提交application的时候 添加 --supervise 选项 如果Driver挂掉 会自动启动一个Driver * 2.代码层面恢复Driver(StreamingContext) * */ object WindowWordCountDriverHA { val checkpointPath = "/app/checkpoint/driverHA" def main(args:Array[String]) { /** * StreamingContext.getOrCreate(checkpointPath,createStreamingContext) * 这个方法首先会从 checkpointPath 目录中获取StreamingContext【 因为StreamingContext是序列化存储在Checkpoint目录中,恢复时会尝试反序列化这些objects。 * 如果用修改过的 class 可能会导致错误,此时需要更换 checkpoint 目录或者删除 checkpoint 目录中的数据,程序才能起来。】 * * 若能获取回来StreamingContext,就不会执行 createStreamingContext 这个方法创建,否则就会创建 * */ val ssc: StreamingContext = StreamingContext.getOrCreate(checkpointPath,createStreamingContext) ssc.start() ssc.awaitTermination() } def createStreamingContext():StreamingContext ={ Logger.getLogger("org.apache.spark").setLevel(Level.ERROR) Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF) val conf = new SparkConf().setAppName("WindowWordCount").setMaster("local[2]") val sc = new SparkContext(conf) //创建StreamingContext val ssc = new StreamingContext(sc, Seconds(5)) //义checkpoint目录 ssc.checkpoint(checkpointPath) //获取从Socket发送过来的数据 val hostname = "127.0.0.1" val port = 8080 val lines = ssc.socketTextStream(hostname, port, StorageLevel.MEMORY_ONLY_SER) val words = lines.flatMap(_.split(",")) words.map(w =>(w,1)).reduceByKeyAndWindow( (v1:Int,v2:Int) => { v1+v2}, (v1:Int,v2:Int) => { v1-v2}, Seconds(15), Seconds(5) ).print() ssc } }