Spark |05 SparkStreaming
从数据处理的方式角度:
流式(Streaming)数据处理;
批量(batch)数据处理;
从数据处理延迟的长短:
实时数据处理: 毫秒级别;(流式处理 != 实时数据处理)
离线数据处理: 小时 or 天级别
1. Spark Streaming
Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。
Spark Streaming准实时(秒,分钟),微批次(时间)的数据处理框架
和Spark基于RDD的概念很相似,Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫作DStream(不连续的流)。DStream 是随时间推移而收到的数据的序列。在
内部,每个时间区间收到的数据都作为 RDD存在,而DStream是由这些RDD所组成的序列(因此得名“离散化”)。所以 简单来将,DStream 就是对 RDD 在实时数据处理场景的一种封装。
离线数据:不可改变数据;实时数据:改变对数据; 流式处理; 批量处理
批量(微批次,不是流式处理)
什么是DStream
DSream 代表了一系列连续的RDD,DStream中每个RDD包含特定时间间隔的数据;离散流, 一个或多个RDD
2. 架构
特点
易用、 容错、 易整合到spark体系
整体架构
SparkStreaming架构
背压机制
Spark 1.5 以前版本,用户如果要限制 Receiver 的数据接收速率,可以通过设置静态配制参 数“spark.streaming.receiver.maxRate”的值来实现,此举虽然可以通过限制接收速率,来
适配当前的处理能力,防止内存溢出,但也会引入其它问题(如果接收数据太快,消费太难,就会有积压;如果接收的很慢,消费的很快,就会资源浪费)。
比如:producer 数据生产高于 maxRate,当 前集群处理能力也高于 maxRate,这就会造成资源利用率下降等问题。 为了更好的协调数据接收速率与资源处理能力,1.5 版本开始
Spark Streaming 可以动态控制 数据接收速率来适配集群数据处理能力。背压机制(即 Spark Streaming Backpressure):根据 JobScheduler 反馈作业的执行信息来动态调整
Receiver 数据接收率。 通过属性“spark.streaming.backpressure.enabled”来控制是否启用 backpressure 机制,默认值 false,即不启用。
WordCount
导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.0.0</version>
</dependency>
需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数
StreamingContext中有这个构造方法: def this(conf: SparkConf, batchDuration: Duration)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
//第二个参数表示批量处理的周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//逻辑处理
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop101", 9999)
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordToOne: DStream[(String, Int)] = words.map((_, 1))
val wordToCount: DStream[(String, Int)] = wordToOne.reduceByKey(_+_)
wordToCount.print()
//由于SparkStreaming采集器是长期执行的任务,所以不能直接关闭 ssc.stop()
//如果main方法执行完毕, 应用程序也会自动结束,所以不能让main执行完毕
//1. 启动采集器
ssc.start()
//2. 等待采集器的关闭
ssc.awaitTermination()
}
[kris@hadoop101 ~]$ nc -lk 9999
Hello world
Hello
Hello java
Hello spark
如果程序运行时,log日志太多,可以将spark conf目录下的log4j文件放中resources里边,日志级别改成ERROR
DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据
3. DStream 创建
1 RDD 队列
测试过程中,可以通过使用 ssc.queueStream(queueOfRDDs)来创建 DStream,每一个推送到 这个队列中的 RDD,都会作为一个 DStream 处理。
需求:循环创建几个 RDD,将 RDD 放入队列。通过 SparkStream 创建 Dstream,计算 WordCoun
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def main(args: Array[String]): Unit = { val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming") //第二个参数表示批量处理的周期 val ssc: StreamingContext = new StreamingContext(conf, Seconds(3)) val rddQueue: mutable.Queue[RDD[Int]] = new mutable.Queue[RDD[Int]]() val inputStream: InputDStream[Int] = ssc.queueStream(rddQueue, oneAtATime = false) val mappedStream: DStream[(Int, Int)] = inputStream.map((_, 1)) val reduceStream: DStream[(Int, Int)] = mappedStream.reduceByKey(_+_) reduceStream.print() //1. 启动采集器 ssc.start() for (i <- 1 to 5) { rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10) Thread.sleep(2000) } //2. 等待采集器的关闭 ssc.awaitTermination() }
2. 文件数据源
文件数据流:能够读取所有HDFS API兼容的文件系统文件,通过fileStream方法进行读取,Spark Streaming 将会监控 dataDirectory 目录并不断处理移动进来的文件,记住目前不支持嵌套目录。
streamingContext.textFileStream(dataDirectory), 其他与上边代码相同;
注意事项:
1)文件需要有相同的数据格式;
2)文件进入 dataDirectory的方式需要通过移动或者重命名来实现;
3)一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据;
3. 自定义数据源
需要继承Receiver,并实现onStart、onStop方法来自定义数据源采集。 自定义数据源,实现监控某个端口号,获取该端口号内容。
自定义数据采集器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def main(args: Array[String]): Unit = { val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming") //第二个参数表示批量处理的周期 val ssc: StreamingContext = new StreamingContext(conf, Seconds(3)) val messageDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver()) messageDS.print() //1. 启动采集器 ssc.start() //2. 等待采集器的关闭 ssc.awaitTermination() } /** * 自定义数据采集器 * 1. 继承Receiver, 定义泛型, 传递参数 * 2. 重写方法 */ class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){ private var flg = true override def onStart(): Unit = { new Thread(new Runnable { override def run(): Unit = { while (flg) { val message = "采集的数据为: " + new Random().nextInt(10).toString store(message) Thread.sleep(500) } } }).start() } override def onStop(): Unit = { flg = false } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 自定义数据采集器 class CustomerReceive(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY){ //有一个构造方法 var socket: Socket = null //读数据并将数据发送给Spark def receive(): Unit = { //创建一个Socket val socket = new Socket(host, port) //字节流 ---->字符流 val inputStream: InputStream = socket.getInputStream //字节流 //字符流 val bufferedReader: BufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8")) var line: String = null while ((line = bufferedReader.readLine()) != null){ if (!"--END--".equals(line)){ store(line) //存储到这里边 }else{ return } } } //启动采集器 //最初启动的时候,调用该方法,作用为:读数据并将数据发送给Spark override def onStart(): Unit = { new Thread(new Runnable{ override def run(): Unit = { receive() } }).start() } //关闭采集器 override def onStop(): Unit = { if (socket != null){ socket.close() socket = null } } } //测试: object FileStream { def main(args: Array[String]): Unit = { // 创建流式处理环境对象 // 创建对象时,需要传递采集数据的周期(时间) val conf: SparkConf = new SparkConf().setAppName("Streaming").setMaster("local[*]") val streamContext: StreamingContext = new StreamingContext(conf, Seconds(5)) // 从端口号获取数据 val socketDStream: ReceiverInputDStream[String] = streamContext.receiverStream(new CustomerReceive("hadoop101", 9999)) // 一行一行的数据 line ==> word val wordDStream: DStream[String] = socketDStream.flatMap(_.split(" ")) // word ==> (word, 1) val wordToCountDStream: DStream[(String, Int)] = wordDStream.map((_, 1)) // reduceByKey val wordToSumDStream: DStream[(String, Int)] = wordToCountDStream.reduceByKey(_ + _) //打印数据 wordToSumDStream.print() // TODO 启动采集器 streamContext.start() // TODO Driver不能停止,等待采集器的结束 // wait, sleep streamContext.awaitTermination() } }
4. Kafka数据源(重点)
版本选型
ReceiverAPI:需要一个专门的 Executor 去接收数据,然后发送给其他的 Executor 做计算。存在的问题,接收数据的 Executor 和计算的 Executor 速度会有所不同,特别在接收数据
的 Executor 速度大于计算的 Executor 速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用。
DirectAPI:是由计算的 Executor 来主动消费 Kafka 的数据(Executro节点又消费又计算数据),速度由自身控制。
Kafka 0-8 Receiver 模式、Kafka 0-8 Direct 模式(当前版本不适用)
Kafka 0-10 Direct 模式
1)需求:通过 SparkStreaming 从 Kafka 读取数据,并将读取过来的数据做简单计算,最终打印到控制台。
2)导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.1</version>
</dependency>
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
//第二个参数表示批量处理的周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//定义kafka参数
val kafkaPara: Map[String, Object] = Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop101:9092,hadoop102:9092,hadoop103:9092",
ConsumerConfig.GROUP_ID_CONFIG -> "kris", //消费者组
"key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
)
val kafkaDataDS: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream( //泛型是kafka传来的数据K V的类型
ssc,
LocationStrategies.PreferConsistent, //位置的策略: 采集和计算的节点如何匹配, 由框架来匹配
ConsumerStrategies.Subscribe[String, String](Set("test"), kafkaPara) //消费者策略, 订阅的Kafka主题
)
kafkaDataDS.map(_.value()).print()
//1. 启动采集器
ssc.start()
//2. 等待采集器的关闭
ssc.awaitTermination()
}
KafkaUtils 对象可以在 StreamingContext 和 JavaStreamingContext 中以你的 Kafka 消息创建出 DStream。由于 KafkaUtils 可以订阅多个主题,因此它创建出的 DStream 由成对的主题和消息组成。要创建出一个流数据,需要使用 StreamingContext 实例、一个由逗号隔开的 ZooKeeper 主机列表字符串、消费者组的名字(唯一名字),以及一个从主题到针对这个主题的接收器线程数的映射表来调用 createStream() 方法。
//监听kafka消息 object KafkaStreaming { def main(args: Array[String]): Unit = { // 创建配置对象 val sparkConf = new SparkConf().setAppName("KafkaStreaming").setMaster("local[*]") // 创建流式处理环境对象 // 创建对象时,需要传递采集数据的周期(时间) val socket: StreamingContext = new StreamingContext(sparkConf, Seconds(5)) // 一个类如果创建SparkContext,那么这个类我们称之为Driver类 // 从Kafka集群中获取数据 //定义kafka参数 val kafkaParams = Map[String, String]( "group.id" -> "kris", "zookeeper.connect" -> "hadoop101:2181", ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG ->"org.apache.kafka.common.serialization.StringDeserializer",//StringDeserializer的全类名,StringDeserializer implements Deserializer<String> ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer" ) //别导错包流,是kafka.clients.consumer里对 //定义topic参数 val topicMap = Map("thrid" -> 3) val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream[String, String, StringDecoder, StringDecoder]( socket, kafkaParams, topicMap, StorageLevel.MEMORY_ONLY) //StorageLevel别导错包流 val wordToCountDStream = kafkaDStream.map { case (k, v) => {(v, 1)} } val wordToSumDStream: DStream[(String, Int)] = wordToCountDStream.reduceByKey(_ + _) //打印数据 wordToSumDStream.print() //启动采集器 socket.start() //Driver不能停,等待采集器对结束 socket.awaitTermination() } }
启动kafka并中控制台启动一个生产者
[kris@hadoop101 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop101:9092 --topic thrid
打印:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
------------------------------------------- Time: 1555065970000 ms ------------------------------------------- (Hello world,1) ------------------------------------------- Time: 1555065975000 ms ------------------------------------------- (Hello,1) ------------------------------------------- Time: 1555065980000 ms ------------------------------------------- (Hello,1) (java,1) ------------------------------------------- Time: 1555065985000 ms ------------------------------------------- (spark,1) -------------------------------------------
4. DStream转换
DStream上的原语与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。
有状态转化操作
UpdateStateByKey
UpdateStateByKey原语用于记录历史记录,有时,我们需要在 DStream 中跨批次维护状态(例如流计算中累加wordcount)。针对这种情况,updateStateByKey() 为我们提供了对一个状态变量的访问,用于键值对形式的 DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件 更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。
updateStateByKey() 的结果会是一个新的 DStream,其内部的 RDD 序列是由每个时间区间对应的(键,状态)对组成的。
updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,你需要做下面两步:
1. 定义状态,状态可以是一个任意的数据类型。
2. 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。(key只要相同它的状态就会更新)
key单词相同了就会形成一个数量对集合,Seq[Int]就是那个数量(比如Hello, 1; Hello, 1;Seq即1 1 1);Option只有两个值(some有值和none没值),为了解决空指针出现对类,不用判断当前对象是否为空,可直接使用option
更新状态:多条数据之间是否有关系, 有状态 无状态
周期间采集数据是无状态的,但实时数据需要是有状态的,与checkPoint做聚合-->就有状态了
把数据保持中CheckPoint,buffer临时对缓冲
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
//第二个参数表示批量处理的周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//ssc.sparkContext.setCheckpointDir("cp")
ssc.checkpoint("cp")
//无状态数据操作, 只对当前的采集周期内的数据进行处理
//在某些场合下,需要保留数据统计结果(状态),实现数据的汇总
//使用有状态操作时,需要设定检查点路径
val datas: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
val wordToOne: DStream[(String, Int)] = datas.map((_, 1))
//val wordToCount: DStream[(String, Int)] = wordToOne.reduceByKey(_+_) //无状态的
//updateStateByKey: 跟进key对数据的状态进行更新
//传递的参数中含有两个值
//第一个值表示相同的key的value
//第二个值表示缓存区相同key的value数据
val state: DStream[(String, Int)] = wordToOne.updateStateByKey(
(seq: Seq[Int], buff: Option[Int]) => { //seq序列: Seq[Int] 表相同key的value集合(当前周期中单词对数量的集合), buff: Option[Int]表相同key缓冲区的value, 所谓的checkPoint
val newCount = seq.sum + buff.getOrElse(0)
Option(newCount) //做完聚合操作再更新到缓冲区中, 它需要返回一个Option
}
)
state.print()
//1. 启动采集器
ssc.start()
//2. 等待采集器的关闭
ssc.awaitTermination()
}
打印:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
有状态转换操作 ------------------------------------------- Time: 1555070600000 ms ------------------------------------------- (Hello,1) (world,1) ------------------------------------------- Time: 1555070605000 ms ------------------------------------------- (Hello,2) (world,2) ------------------------------------------- Time: 1555070610000 ms ------------------------------------------- (Hello,3) (java,1) (world,2) ------------------------------------------- Time: 1555070615000 ms ------------------------------------------- (Hello,3) (java,1) (world,2)
Window Operations
Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前 Steaming 的允许状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。
- 窗口时长:计算内容的时间范围;
- 滑动步长:隔多久触发一次计算。
注意:这两者都必须为采集周期大小的整数倍。
窗口数据是指一段时间范围内的数据作为一个整体使用,随着时间的推移,窗口数据也会发生变化,这样的函数称为窗口函数,并且这个窗口可以发生变化,也称为滑动窗口;
关于 Window 的操作还有如下方法:
(1)window(windowLength, slideInterval): 基于对源 DStream 窗化的批次进行计算返回一个 新的 Dstream;
(2)countByWindow(windowLength, slideInterval): 返回一个滑动窗口计数流中的元素个数;
(3)reduceByWindow(func, windowLength, slideInterval): 通过使用自定义函数整合滑动区间 流元素来创建一个新的单元素流;
(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]): 当在一个(K,V) 对的 DStream 上调用此函数,会返回一个新(K,V)对的 DStream,此处通过对滑动窗口中批次数据使用 reduce 函
数来整合每个 key 的 value 值。
(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]): 这个函 数是上述函数的变化版本,每个窗口的reduce 值都是通过用前一个窗的reduce 值来递增计算。 通过 reduce 进
入到滑动窗口数据并”反向 reduce”离开窗口的旧数据来实现这个操作。一个例子是随着窗口滑动对 keys 的“加”“减”计数。通过前边介绍可以想到,这个函数只适用于”可逆的 reduce 函数”,也就是这些 reduce 函
数有相应的”反 reduce”函数(以参数 invFunc 形式 传入)。如前述函数,reduce 任务的数量通过可选参数来配置。
object DStreamWindow {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Stream").setMaster("local[*]")
val streamContext: StreamingContext = new StreamingContext(conf, Seconds(3)) //采集周期为3s
val socketDStream: ReceiverInputDStream[String] = streamContext.socketTextStream("hadoop101", 9999)
// 设定数据窗口:window
// 第一个参数表示窗口的大小(时间的范围,应该为采集周期的整数倍)比如采集周期为3s,滑动范围为6s,把两个采集周期作为一个整体。
// 第二个参数表示窗口的滑动的幅度(滑动的幅度即步长) 滑动步长 3s < 滑动窗口大小6s,会有重复数据。 所以滑动步长要 >= 滑动窗口的大小
val windowDStream: DStream[String] = socketDStream.window(Seconds(6), Seconds(3))
val wordDStream: DStream[String] = windowDStream.flatMap(_.split(" "))
val wordCountDStream: DStream[(String, Int)] = wordDStream.map((_, 1))
val wordSumDStream: DStream[(String, Int)] = wordCountDStream.reduceByKey(_+_)
wordSumDStream.print()
streamContext.start()
streamContext.awaitTermination()
}
}
new StreamingContext(conf, Seconds(3))
val wordToOne: DStream[(String, Int)] = lines.map((_, 1))
// 窗口的范围应该是采集周期的整数倍
// 窗口可以滑动的,但是默认情况下,一个采集周期进行滑动,下面的滑动窗口为6s,是采集周期的2倍作为一个整体;步长6s,就不会有重复数据了。
// 这样的话,可能会出现重复数据的计算,为了避免这种情况,可以改变滑动的滑动(步长)
val windowDS: DStream[(String, Int)] = wordToOne.window(Seconds(6), Seconds(6))
val wordToCount: DStream[(String, Int)] = windowDS.reduceByKey(_ + _)
reduceByKeyAndWindow(),每滑动一次就相当于把上次窗口的数据删除了,把下次窗口的数据增加了。 有增加、减少操作,避免了重复数据;
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
//第二个参数表示批量处理的周期
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
// reduceByKeyAndWindow : 当窗口范围比较大,但是滑动幅度比较小,那么可以采用增加数据和删除数据的方式
// 无需重复计算,提升性能。
val lines = ssc.socketTextStream("localhost", 9999)
val wordToOne: DStream[(String, Int)] = lines.map((_, 1))
val windowDS: DStream[(String, Int)] = wordToOne.reduceByKeyAndWindow(
(x: Int, y: Int) => (x + y), //增加的那个窗口的数据
(x: Int, y: Int) => (x - y), //减少滑过去的那个窗口的数据
Seconds(9), Seconds(3) //窗口大小9s,滑动3s,有6s都是重复的
)
windowDS.print()
//1. 启动采集器
ssc.start()
//2. 等待采集器的关闭
ssc.awaitTermination()
无状态操作
Transform
Transform原语允许DStream上执行任意的RDD-to-RDD函数。即使这些函数并没有在DStream的API中暴露出来,通过该函数可以方便的扩展Spark API。该函数每一批次调度一次。其实也就是对DStream中的
RDD应用转换。
Transform和map的区别:
// TODO XXXXXX (Drvier) * 1,这里可写Driver代码但只执行一遍; wordSumDStream.map{ case(word, sum) => { // TODO YYYYYY (Executor) * N ,这里执行的是Executor代码可执行N遍 (word, 1) } } // transform可以将DStream包装好的RDD抽取出来进行转换操作 // transform可以在每一个采集周期对rdd进行操作 // TODO AAAAAA (Driver) * 1 wordSumDStream.transform{ rdd => { // TODO BBBBBBB (Driver) * N rdd.map{ case (word, sum) => { // TODO CCCCCC (Executor) * N (word, 1) } } } }
join
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
val ssc = new StreamingContext(sparkConf, Seconds(5))
val data9999 = ssc.socketTextStream("hadoop101", 9999)
val data8888 = ssc.socketTextStream("hadoop101", 8888)
val map9999: DStream[(String, Int)] = data9999.map((_,9))
val map8888: DStream[(String, Int)] = data8888.map((_,8))
// 所谓的DStream的Join操作,其实就是两个RDD的join
val joinDS: DStream[(String, (Int, Int))] = map9999.join(map8888)
joinDS.print()
ssc.start()
ssc.awaitTermination()
DStream输出
输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。与RDD中的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么
这些DStream就都不会被求值。如果StreamingContext中没有设定输出操作,整个context就都不会启动。
输出操作如下:
(1)print():在运行流程序的驱动结点上打印DStream中每一批次数据的最开始10个元素。这用于开发和调试。在Python API中,同样的操作叫print()。
(2)saveAsTextFiles(prefix, [suffix]):以text文件形式存储这个DStream的内容。每一批次的存储文件名基于参数中的prefix和suffix。”prefix-Time_IN_MS[.suffix]”.
(3)saveAsObjectFiles(prefix, [suffix]):以Java对象序列化的方式将Stream中的数据保存为 SequenceFiles . 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。
(4)saveAsHadoopFiles(prefix, [suffix]):将Stream中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。
Python API Python中目前不可用。
(5)foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者通过网络将
其写入数据库。注意:函数func在运行流应用的驱动中被执行,同时其中一般函数RDD操作从而强制其对于流RDD的运算。
通用的输出操作foreachRDD(),它用来对DStream中的RDD运行任意计算。与transform() 有些类似,都可以让我们访问任意RDD。在foreachRDD()中,可以重用我们在Spark中实现的所有行动操作。
比如,常见的用例之一是把数据写到诸如MySQL的外部数据库中。 注意:
- (1)连接不能写在driver层面(序列化);
- (2)如果写在foreach则每个RDD都创建,得不偿失;
- (3)增加foreachPartition,在分区创建。
5. 优雅关闭
流式任务需要 7*24 小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。 使用外部文件系统来控制内部程序关闭。
/*
线程的关闭:
val thread = new Thread()
thread.start()
thread.stop(); // 强制关闭
//跳出run方法线程就会自动停止, stop是强制关闭, 有可能导致数据中断
*/
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
val wordToOne: DStream[(String, Int)] = lines.map((_, 1))
wordToOne.print()
ssc.start()
//如果想要关闭采集器, 在启动之后应该是创建一个线程去关闭它, 而不是在当前主线程中去关闭
// 而且需要在第三方程序中增加关闭状态
new Thread(
new Runnable {
override def run(): Unit = {
//优雅地关闭, 第一个参数是关闭spark环境, 第二个参数即优雅地关闭(把当前数据处理完了再关)
// 优雅地关闭(通过第三方的处理机制来判断它的状态是否需要关闭)
// 计算节点不在接收新的数据,而是将现有的数据处理完毕,然后关闭
// Mysql : Table(stopSpark) => Row => data
// Redis : Data(K-V)
// ZK : /stopSpark
// HDFS : /stopSpark
/*while (true) {
if (true) {
// 获取SparkStreaming状态
val state: StreamingContextState = ssc.getState()
if (state == StreamingContextState.ACTIVE) {
ssc.stop(true, true)
}
}
Thread.sleep(5000)
}*/
Thread.sleep(5000)//测试,启动5s之后会判断它的状态然后关闭
val state: StreamingContextState = ssc.getState()
if ( state == StreamingContextState.ACTIVE ) {
ssc.stop(true, true)
}
System.exit(0)
}
}
).start()
ssc.awaitTermination()
优雅的恢复数据
val ssc: StreamingContext = StreamingContext.getActiveOrCreate("cp", () => {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
val wordToOne: DStream[(String, Int)] = lines.map((_, 1))
wordToOne.print()
ssc
})
ssc.checkpoint("cp")
ssc.start()
ssc.awaitTermination()