<Zhuuu_ZZ>Spark Streaming

一 Spark Streaming概述

1、离线和实时概念

  • 数据处理的延迟
    • 离线计算
      • 就是在计算开始前已知所有输入数据,输入数据不会产生变化,一般计算量级较大,计算时间也较长。例如今天早上一点,把昨天累积的日志,计算出所需结果。最经典的就是Hadoop的MapReduce方式;
    • 实时计算
      • 输入数据是可以以序列化的方式一个个输入并进行处理的,也就是说在开始的时候并不需要知道所有的输入数据。与离线计算相比,运行时间短,计算量级相对较小。强调计算过程的时间要短,即所查当下给出结果。

2、批量和流式概念

  • 数据处理的方式
    • 批:处理离线数据,冷数据。单个处理数据量大,处理速度比流慢。
    • 流:在线,实时产生的数据。单次处理的数据量小,但处理速度更快。
  • 近年来,在Web应用、网络监控、传感监测等领域,兴起了一种新的数据密集型应用——流数据,即数据以大量、快速、时变的流形式持续到达。实例:PM2.5检测、电子商务网站用户点击流。
  • 流数据具有如下特征:
    • 数据快速持续到达,潜在大小也许是无穷无尽的;
    • 数据来源众多,格式复杂;
    • 数据量大,但是不十分关注存储,一旦经过处理,要么被丢弃,要么被归档存储;
    • 注重数据的整体价值,不过分关注个别数据;

3、Spark Streaming是什么

  • Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象算子如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。
    在这里插入图片描述

  • 在 Spark Streaming 中,处理数据的单位是一批而不是单条,而数据采集却是逐条进行的,因此 Spark Streaming 系统需要设置间隔使得数据汇总到一定的量后再一并操作,这个间隔就是批处理间隔。批处理间隔是Spark Streaming的核心概念和关键参数,它决定了Spark Streaming提交作业的频率和数据处理的延迟,同时也影响着数据处理的吞吐量和性能。

  • 和Spark基于RDD的概念很相似,Spark Streaming使用了一个高级抽象离散化流(discretized stream),叫作DStreams。DStreams是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为RDD存在,而DStreams是由这些RDD所组成的序列(因此得名“离散化”)。DStreams可以由来自数据源的输入数据流来创建, 也可以通过在其他的 DStreams上应用一些高阶操作来得到。

4、Spark Streaming特点

  • 易用

  • 容错

  • 易整合到Spark体系

  • 缺点

    • Spark Streaming是一种“微量批处理”架构, 和其他基于“一次处理一条记录”架构的系统相比, 它的延迟会相对高一些。

5、Spark Streaming架构

  • 整体架构图
    在这里插入图片描述

  • 架构图
    在这里插入图片描述

6、背压机制

  • 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,即不启用。

二 DStream入门

1、WordCount案例实操

  • 需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数
  • 添加依赖
<dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-core_2.11</artifactId>
      <version>2.4.5</version>
    </dependency>
    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming_2.11</artifactId>
      <version>2.4.5</version>
    </dependency>
  • 编写Scala代码
object Spark01_WordCount {
def main(args: Array[String]): Unit = {
	
	//创建配置文件对象 注意:Streaming程序不能设置为local[1],至少需要2个线程
    val conf: SparkConf = new SparkConf().setAppName("Spark01_W").setMaster("local[*]")
  
    //创建Spark Streaming上下文环境对象
    val ssc = new StreamingContext(conf,Seconds(3))
    
    //操作数据源-从端口中获取一行数据
    val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoopwei",9999)
   
    //对获取的一行数据进行扁平化操作
    val flatMapDS: DStream[String] = socketDS.flatMap(_.split(" "))
   
    //结构转换
    val mapDS: DStream[(String, Int)] = flatMapDS.map((_,1))
   
    //对数据进行聚合
    val reduceDS: DStream[(String, Int)] = mapDS.reduceByKey(_+_)
   
    //输出结果   注意:调用的是DS的print函数
    reduceDS.print()
    
    //启动采集器
    ssc.start()
    
    //默认情况下,上下文对象不能关闭
    //ssc.stop()
    
    //等待采集结束,终止上下文环境对象
    ssc.awaitTermination()
  }
}
  • 或者编写Java代码也可以完成本项功能
package com.spark.sparkapps.sparkstreaming;

import java.util.Arrays;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;

import scala.Tuple2;

public class WordCountOnLine {
    public static <U> void main(String[] args) {
        
        /**
         * 第一步:配置SparkConf,
         *     1. 因为 Spark Streaming 应用程序至少有一条线程用于不断的循环结束数据,并且至少有一条线程用于处理
         *              接收的数据(否则的话无线程用于处理数据,随着时间的推移,内存和磁盘都会不堪重负)
         *  2. 对于集群而已,每个 Executor 一般肯定不止一个线程,那对于处理 Spark Streaming应用程序而言,每个 Executor 一般分配多少Core
         *     比较合适?根据我们过去的经验,5个左右的 Core 是最佳的(一个段子分配为基数 Core 表现最佳,)
         */
//        SparkConf conf = new SparkConf().setMaster("local[2]").setAppName("sparkStreaming");
        SparkConf conf = new SparkConf().setMaster("spark://hadoopwei:7077").setAppName("sparkStreaming");
        
        /**
         * 第二步:创建 SparkStreamingContext,
         *     1.这个 SparkStreaming 应用程序所有功能的起始点和程序调度的核心
         *         SparkStreamingContext 的构建可以基于 SparkConf参数,也可基于持久化的 SaprkStreamingContext的内容来回复过来
         *         (典型的场景是 Driver 奔溃后重新启动,由于 Spark Streaming 具有连续 7*24 小时不间断运行的特征,所以需要在 Driver 重新启动后继续上一次的状态,
         *            此时的状态恢复需要基于曾经的 Checkpoint)
         *     2.在一个Spark Streaming 应用程序中可以创建若干个 SaprkStreamingContext对象,使用下一个 SaprkStreamingContext
         *       之前需要把前面正在运行的 SparkStreamingContext 对象关闭掉,由此,我们获得一个重大的启发 SparkStreaming框架也只是Spark Core上的一个应用程序而言
         *      只不过 Spark Streaming 框架要运行的话需要Spark工程师写业务逻辑处理代码;
         */
        JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(5));
        
        /**
         * 第三步: 创建 Spark Streaming 输入数据来源 input Stream
         *      1.数据输入来源可以基于 File,HDFS, Flume,Kafka,Socket等;
         *      2.在这里我们制定数据来源于网络 Socket端口,Spark Streaming链接上改端口并在运行的时候一直监听该端口的数据(当然该端口服务首先必须存在),并且在后续会根据业务需要不断的
         *        有数据产生(当然对于Spark Streaming 引用程序的运行而言,有无数据其处理流程都是一样的)
         *   3.如果经常在每隔 5 秒钟没有数据的话不断的启动空的 Job 其实是会造成调度资源的浪费,因为并没有数据需要发生计算;真实的企业级生产环境的代码在具体提交 Job 前会判断是否有数据,如果没有的话
         *   不再提交 Job;
         */
        
       JavaReceiverInputDStream<String> lines = jsc.socketTextStream("hadoopwei", 9999);
       
       /**
        * 第四步:接下来就是 对于 Rdd编程一样基于 DStream进行编程!!!原因是DStream是RDD产生的模板(或者说类), 在 Saprk Stream发生计算前,其实质是把每个 Batch的DStream的操作翻译
        *         成为 Rdd 的操作!!!
        */
       
       JavaDStream<String> flatMap = lines.flatMap(new FlatMapFunction<String, String>() {
        public Iterable<String> call(String line) throws Exception {
            String[] split = line.split(" ");
            return Arrays.asList(split);
        }
       });
       
       JavaPairDStream<String, Integer> mapToPair = flatMap.mapToPair(new PairFunction<String, String, Integer>() {
        public Tuple2<String, Integer> call(String word) throws Exception {
            return new Tuple2<String, Integer>(word, 1);
        }
       });
       
       JavaPairDStream<String, Integer> reduceByKey = mapToPair.reduceByKey(new Function2<Integer, Integer, Integer>() {
        public Integer call(Integer v1, Integer v2) throws Exception {
            return v1+v2;
        }
       });
       
       
       /**
        * 此处print并不会直接触发 job 的执行,因为现在的一切都是在 Spark Streaming 框架的控制之下的,对于 Spark Streaming 而言具体是否触发真正的 job 运行
        * 是基设置的  Duration 时间间隔触发
        * 一定要注意的是 Spark Streaming应用程序要想执行具体的Job,对DStream就必须有 output Stream操作
        * output Stream有很多类型的函数触发,类print,saveAsTextFile,saveAsHadoopFile等,最为重要的一个方法是 foreachRDD,因为Spark Streaming处理的结果一般都会放在 Redis,DB,
        * DashBoard等上面,foreachRDD主要就是用来完成这些功能的,而且可以随意的自定义具体数据到底放在那里
        */
       reduceByKey.print();
       
       /**
        * Spark Streaming 执行引擎也就是Driver开始运行,Driver启动的时候是位于一条新的线程中的,当然其内部有消息接收应用程序本身或者 Executor 中的消息;
        * 
        */
       jsc.start();
       jsc.awaitTermination();
       
    }
}
  • 启动程序并通过NetCat发送数据:
[root@hadoopwei ~]# nc -lk 9999
  • 注意:如果程序运行时,log日志太多,可以将spark conf目录下的log4j文件里面的日志级别改成ERROR。

2、WordCount解析

  • Discretized Stream是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark算子操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示,每个RDD含有一段时间间隔内的数据。
    在这里插入图片描述

  • 对数据的操作也是按照RDD为单位来进行的
    在这里插入图片描述

  • 对这些 RDD的转换是由Spark引擎来计算的, DStream的操作隐藏的大多数的细节, 然后给开发者提供了方便使用的高级 API。
    在这里插入图片描述

3、几点注意

  • Streaming程序不能设置为local[1],至少需要2个线程
  • 一旦StreamingContext已经启动, 则不能再添加新的 streaming computations
  • 一旦一个StreamingContext已经停止(StreamingContext.stop()), 他也不能再重启
  • 在一个 JVM 内, 同一时间只能启动一个StreamingContext
  • stop() 的方式停止StreamingContext, 也会把SparkContext停掉. 如果仅仅想停止StreamingContext, 则应该这样: stop(false)
  • 一个SparkContext可以重用去创建多个StreamingContext, 前提是以前的StreamingContext已经停掉,并且SparkContext没有被停掉。

三 DStream创建

1、RDD队列

(1)用法及说明

  • 测试过程中,可以通过使用 ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的 RDD,都会作为一个 DStream 处理。

(2)案例实操

  • 需求:循环创建几个 RDD,将 RDD 放入队列。通过 SparkStream 创建 Dstream,计算WordCount
  • 编写代码
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable

object RDDStream {
 def main(args: Array[String]) {
 
 //1.初始化 Spark 配置信息
 val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")

 //2.初始化 SparkStreamingContext
 val ssc = new StreamingContext(conf, Seconds(4))
 
 //3.创建 RDD 队列
 val rddQueue = new mutable.Queue[RDD[Int]]()
 
 //4.创建 QueueInputDStream
 val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)

 //5.处理队列中的 RDD 数据
 val mappedStream = inputStream.map((_,1))
 val reducedStream = mappedStream.reduceByKey(_ + _)

 //6.打印结果
 reducedStream.print()

 //7.启动任务
 ssc.start()

//8.循环创建并向 RDD 队列中放入 RDD
 for (i <- 1 to 5) {
 rddQueue += ssc.sparkContext.makeRDD(1 to 10, 10)
 Thread.sleep(2000)
 }
 ssc.awaitTermination()
  } 
 }
  • 结果展示
-------------------------------------------
Time: 1608560564000 ms
-------------------------------------------
(1,2)
(2,2)
(3,2)
(4,2)
(5,2)
(6,2)
(7,2)
(8,2)
(9,2)
(10,2)

-------------------------------------------
Time: 1608560568000 ms
-------------------------------------------
(1,2)
(2,2)
(3,2)
(4,2)
(5,2)
(6,2)
(7,2)
(8,2)
(9,2)
(10,2)

-------------------------------------------
Time: 1608560572000 ms
-------------------------------------------
(1,1)
(2,1)
(3,1)
(4,1)
(5,1)
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1608560576000 ms
-------------------------------------------

-------------------------------------------

2、自定义数据源

(1) 用法及说明

  • 需要继承 Receiver,并实现 onStart、onStop 方法来自定义数据源采集。

(2)案例实操

  • 需求:自定义数据源,实现监控某个端口号,获取该端口号内容。
  • 自定义数据源
class MyReceiver(host:String,port:Int) extends  Receiver[String](StorageLevel.MEMORY_ONLY){

  //最初启动的时候,调用该方法,作用为:读数据并将数据发送给 Spark
  override def onStart(): Unit = {
    new Thread(new Runnable {
      override def run(): Unit = {
        receive()
      }
    }).start()
  }

  //将socket放在外面以便onStop方法关闭
  var socket:java.net.Socket=null

  //读数据并将数据发送给 Spark
def receive(): Unit ={
  //创建一个 Socket
  socket= new java.net.Socket(host,port)
  //创建一个 BufferedReader 用于读取端口传来的数据
  val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
  //读取数据
  var line:String=null;
  //当 receiver 没有关闭并且输入数据不为空,则循环发送数据给 Spark
  while ((line=reader.readLine())!=null && !isStopped()){
    //若输入为end,则跳出循环
    if(line.equals("end")){
    return
    }else{
      this.store(line)
    }
  }

}
  //停止方法
  override def onStop(): Unit = {
    if(socket!=null){
      socket.close()
      socket=null
    }
  }


}
  • 使用自定义的数据源采集数据
object MyReceiverDemo{
 def main(args: Array[String]): Unit = {
 
 //1.初始化 Spark 配置信息
val sparkConf = new SparkConf().setMaster("local[*]")
.setAppName("StreamWordCount")

 //2.初始化 SparkStreamingContext
 val ssc = new StreamingContext(sparkConf, Seconds(5))

//3.创建自定义 receiver 的 Streaming
val lineStream = ssc.receiverStream(new CustomerReceiver("hadoopwei", 9999))

 //4.将每一行数据做切分,形成一个个单词
 val wordStream = lineStream.flatMap(_.split("\t"))
 
 //5.将单词映射成元组(word,1)
  val wordAndOneStream = wordStream.map((_, 1))
 
 //6.将相同的单词次数做统计
 val wordAndCountStream = wordAndOneStream.reduceByKey(_ + _)

 //7.打印
 wordAndCountStream.print()
 
 //8.启动 SparkStreamingContext
 ssc.start()
 ssc.awaitTermination()
 
 } 
 }

3、textFileStream文件输入流

  • 文件路径下事先存在的文件无效。
  • textFileStream路径如果是hdfs的路径 你直接hdfs dfs -put到你的监测路径就可以
  • 如果是本地目录如file:///D:\\IDEA\\Data\\streamstu\\in\\test,你不能直接在目录里创建文件或移动文件到这个目录,必须用流的形式写入到这个目录形成文件才能被监测到。可在其它地方创建一个文件然后另存到此本地目录下可以完成此项测试。
package cn.kgc.kb09.sparkstream

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreamFileDataSourceDemo2 {
  def main(args: Array[String]): Unit = {
   
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("fileDataSource1")
     val streamingContext = new StreamingContext(conf,Seconds(5))

//文件为HDFS文件
    val fileDStream: DStream[String] = streamingContext.textFileStream("hdfs://hadoopwei:9000/kb09file")
  //文件为本地Windows文件
   //val fileDStream: DStream[String] = streamingContext.textFileStream("file:///D:\\IDEA\\Data\\streamstu\\in\\test")
   
     val wordStream: DStream[String] = fileDStream.flatMap(line=>line.split("\\s+"))

    val mapStream: DStream[(String, Int)] = wordStream.map(word=>(word,1))

     val sumStream: DStream[(String, Int)] = mapStream.reduceByKey(_+_)

     sumStream.print()

    streamingContext.start()

    streamingContext.awaitTermination()

  }
}

4、 Kafka作为SparkStreaming的数据源

(1) 版本选型

  • ReceiverAPI:需要一个专门的 Executor 去接收数据,然后发送给其他的 Executor 做计算。存在的问题,接收数据的 Executor 和计算的 Executor 速度会有所不同,特别在接收数据的 Executor速度大于计算的 Executor 速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用。
  • DirectAPI:是由计算的 Executor 来主动消费 Kafka 的数据,速度由自身控制。

(2)Kafka 0-10 Direct模式

  • 需求:通过 SparkStreaming 从 Kafka 读取数据,并将读取过来的数据做简单计算,最终打印到控制台。
  • 导入依赖
    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
      <version>2.4.5</version>
    </dependency>

    <!-- 版本降维 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
      <version>2.6.6</version>
    </dependency>
  • 编写代码
package cn.kgc.kb09.sparkstream

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreamKafkaSource {
  def main(args: Array[String]): Unit = {
 
  //1.创建 SparkConf
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")

 //2.创建 StreamingContext
    val streamingContext = new StreamingContext(conf,Seconds(5))

//3.定义 Kafka 参数
    val kafkaParams: Map[String, String] = Map(
    //kafka集群有几台机器就写几台
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"), 
      //因为是消费topic,所以需要K-V反序列化
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
    //定义消费者组别
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup1")
    )
   
   //4.读取 Kafka 数据创建 DStream
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))

  //5.将每条消息的 KV 取出,key为null,value为值
 val valueDStream: DStream[String] = kafkaDStream.map(record => record.value())
 
 //6.计算 WordCount
 valueDStream.flatMap(_.split(" "))
 .map((_, 1))
 .reduceByKey(_ + _)
 .print()

//7.开启任务
    streamingContext.start()

    streamingContext.awaitTermination()

  }
}
  • 对上面topic生产消息
[root@hadoopwei ~]# kafka-console-producer.sh --broker-list hadoopwei:9092 --topic sparkKafkaDemo
>hello world
>nihao

  • 查看Kafka消费进度
[root@hadoopwei ~]# kafka-consumer-groups.sh --describe --bootstrap-server hadoopwei:9092 --group kafkaGroup1

TOPIC           PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID                                     HOST            CLIENT-ID
sparkKafkaDemo  0          40              40              0               consumer-1-1a2d9a4b-476d-42ec-9423-00f06afc607d /192.168.198.1  consumer-1

5、Spark Streaming整合Flume

  • SparkStreaming整合Flume有两种方式,一种是由Flume推送Push数据到Spark,需要先执行SparkStreaming的IDEA程序开启FlumeSink端端口这样Flume才知道往哪输出数据。另一种是由SparkStreaming主动从Flume拉取Poll数据,这种会需要先启动Flume,再执行Spark,因为Sink类型配置为org.apache.spark.streaming.flume.sink.SparkSink输出到Spark所以Flume启动时端口号不存在也不会报错。
  • 至于为什么SparkStreaming的IDEA应用程序需要打成Jar包上传到Linux上运行?其实是因为IDEA是Windows上的软件,由于Windows的防火墙等等原因,直接在IDEA上执行程序无法开启端口号,只有打成Jar包上传到Linux上才没有不同机器间无法通信的问题。
  • 那又为什么要打FatJar胖包呢?究其原因还不是因为胖包有POM文件导入的依赖,大大的减少了由于依赖带来的问题。
    在这里插入图片描述
  • POM依赖
       <!-- spark-flume -->
         <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-flume_2.11</artifactId>
            <version>2.4.4</version>
         </dependency>

①Flume推送Push数据给SparkStreaming

(1)编写Flume配置文件

vi sink_spark_push.conf
------------------------------------------

zhu.sources=s1
zhu.channels=c1
zhu.sinks=sk1

zhu.sources.s1.type=netcat
zhu.sources.s1.bind=localhost
zhu.sources.s1.port=5678

zhu.channels.c1.type=memory

zhu.sinks.sk1.type=avro
zhu.sinks.sk1.hostname=hadooptest1
zhu.sinks.sk1.port=9999


zhu.sources.s1.channels=c1
zhu.sinks.sk1.channel=c1

(2)编写IDEA程序

package cn.kgc.kb09.SparkStreaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.flume.{FlumeUtils, SparkFlumeEvent}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
  * 1.编写Flume程序,配置Avro sink
  * 2.导入依赖包
  * 3.编写Spark程序,使用FlumeUtils.createStream配置数据源
  * 4.打成jar包上传到集群,使用spark-submit提交
  * 5.启动Flume程序
  * 6.启动telnet
  */
object Demo05_FlumePushWordCount{
  def main(args: Array[String]): Unit = { 
    //1.加载数据源,三个参数:①主机名,②端口号,③储存级别
     val flumeStream: ReceiverInputDStream[SparkFlumeEvent] = FlumeUtils.createStream(ssc,"hadooptest1",9999)
      val lines: DStream[String] = flumeStream.map(x=>new String(x.event.getBody.array()))
    val result: DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_+_)
    result.print()
    //3.启动程序
     ssc.start()
    ssc.awaitTermination()
  }
  //定义一个样例类用于转换DataFrame
  case class Word(word:String)
}

(3)将IDEA程序打成FatJar包上传到集群运行开启FlumeSink端口9999
链接: 打胖包参考博客.

spark-submit --class cn.kgc.kb09.SparkStreaming.Demo05_FlumePushWordCount SparkLearn-1.0-SNAPSHOT-jar-with-dependecies.jar

(4)启动Flume

flume-ng agent -n zhu -f sink_spark_push.conf -Dflume.root.logger=INFO,console

(5)启动端口输入内容可以看到第三步下会输出结果

telnet localhost 5678

②SparkStreaming从Flume拉取Poll数据

(1)编写Flume的配置文件

vi sink_spark_pull.conf
----------------------------------
zhu.sources=s1
zhu.channels=c1
zhu.sinks=sk1

zhu.sources.s1.type=netcat
zhu.sources.s1.bind=localhost
zhu.sources.s1.port=5678

zhu.channels.c1.type=memory

#需要将 spark-Streaming-flume jar包拷贝到$FLUME_HOME/lib下
zhu.sinks.sk1.type=org.apache.spark.streaming.flume.sink.SparkSink
zhu.sinks.sk1.hostname=hadooptest1
zhu.sinks.sk1.port=9999

zhu.sources.s1.channels=c1
zhu.sinks.sk1.channel=c1

(2)拷贝Jar包[一共六个]到$FLUME_HOME/lib下并重命名冲突jar包
链接: 需要拷贝的Jar包请点击获取.提取码:n3ni

[root@hadooptest1 lib]# mv scala-library-2.10.5.jar scala-library-2.10.5.jar.bak
[root@hadooptest1 lib]# mv avro-1.7.4.jar avro-1.7.4.jar.bak
[root@hadooptest1 lib]# mv avro-ipc-1.7.4.jar avro-ipc-1.7.4.jar.bak

(3)编写IDEASpark程序

package cn.kgc.kb09.SparkStreaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.flume.{FlumeUtils, SparkFlumeEvent}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
  * 1.编写Flume程序,SparkSink
  * 2.导入依赖包
  * 3.编写Spark程序,使用FlumeUtils.createStream配置数据源
  * 4.启动FLume
  * 5.执行spark-submit
  * 6.启动telnet
  */
object Demo06_FlumePullWordCount{
  def main(args: Array[String]): Unit = {
    //模板代码:创建SparkConf,创建StreamingContext,第二个参数是批处理间隔
     val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
     val ssc = new StreamingContext(conf,Seconds(5))
    //1.加载数据源,三个参数:①主机名,②端口号,③储存级别
     val flumeStream: ReceiverInputDStream[SparkFlumeEvent] = FlumeUtils.createPollingStream(ssc,"hadooptest1",9999)
      val lines: DStream[String] = flumeStream.map(x=>new String(x.event.getBody.array()))
    val result: DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_+_)
    result.print()
    //3.启动程序
     ssc.start()
    ssc.awaitTermination()
  }
  //定义一个样例类用于转换DataFrame
  case class Word(word:String)
}

(4)启动Flume

flume-ng agent -n zhu -f sink_spark_pull.conf -Dflume.root.logger=INFO,console

(5)打成jar包上传到集群运行开启FlumeSink端口9999

spark-submit --class cn.kgc.kb09.SparkStreaming.Demo06_FlumePullWordCount SparkLearn-1.0-SNAPSHOT-jar-with-dependecies.jar

(6)启动telnet

telnet localhost 5678

6、Spark Streaming整合Spark SQL

         <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_2.11</artifactId>
            <version>2.4.4</version>
        </dependency>
package cn.kgc.kb09.SparkStreaming

import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object Demo04_SQLWorkWordCount {
  def main(args: Array[String]): Unit = {
    //模板代码:创建SparkConf,创建StreamingContext,第二个参数是批处理间隔
     val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
     val ssc = new StreamingContext(conf,Seconds(5))

    //1.加载数据源,三个参数:①主机名,②端口号,③储存级别
    val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadooptest1", 9999, StorageLevel.MEMORY_AND_DISK_SER_2)
    //2.对数据进行处理[WordCount]
    val words: DStream[String] = lines.flatMap(_.split("\\s+"))

    words.foreachRDD(rdd=>{
      val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
      import spark.implicits._
      if(rdd.count()!=0){
        val df: DataFrame = rdd.map(x=>Word(x)).toDF()
        df.createOrReplaceTempView("tb_word")
        spark.sql("select word,count(1) from tb_word group by word").show()
      }
    })
    //3.启动程序
     ssc.start()
    ssc.awaitTermination()
  }
  //定义一个样例类用于转换DataFrame
  case class Word(word:String)
}

foreachRDD(func)

  • 接收一个函数,并将该函数作用于DStream每个RDD上
  • 函数在Driver节点中执行,然而RDD是分布式数据集其调用的每个算子是在每个Worker节点上执行
  • 就会有一个冲突,Worker节点获取不到Driver节点的变量。如下:
----------------错误-----------------------
dstream.foreachRDD { rdd =>
	val connection = createNewConnection() // 在driver节点执行
	rdd.foreach { record =>
		connection.send(record) // 在worker节点执行
	}
}


----------------正确-----------------------
dstream.foreachRDD { rdd =>
	rdd.foreachPartition { partitionOfRecords =>
		val connection = createNewConnection()
		partitionOfRecords.foreach(record => 	
							connection.send(record))
	}
}

四 DStream 转换

  • DStream 上的操作与 RDD 的类似,分为 Transformations(转换)和 Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种 Window 相关的原语。

1、无状态转化操作

  • 无状态转化操作就是把简单的 RDD 转化操作应用到每个批次上,也就是转化 DStream 中的每一个 RDD。部分无状态转化操作列在了下表中。注意,针对键值对的 DStream 转化操作(比如reduceByKey())要添加 import StreamingContext._才能在 Scala 中使用。
    在这里插入图片描述
  • 需要记住的是,尽管这些函数看起来像作用在整个流上一样,但事实上每个 DStream 在内部是由许多 RDD(批次)组成,且无状态转化操作是分别应用到每个 RDD 上的。
  • 例如:reduceByKey()会归约每个时间区间中的数据,但不会归约不同区间之间的数据。

Transform

  • Transform 允许 DStream 上执行任意的 RDD-to-RDD 函数。即使这些函数并没有在 DStream的 API 中暴露出来,通过该函数可以方便的扩展 Spark API。该函数每一批次调度一次。其实也就是对 DStream 中的 RDD 应用转换。

案例一:参数为一个,即transformFunc: RDD[T] => RDD[U]

object Transform {
 def main(args: Array[String]): Unit = {
 
 //创建 SparkConf
val sparkConf: SparkConf = new 
SparkConf().setMaster("local[*]").setAppName("WordCount")
 
 //创建 StreamingContext
 val ssc = new StreamingContext(sparkConf, Seconds(3))
 
 //创建 DStream
 val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("hadoopwei", 
9999)
 
 //转换为 RDD 操作
 val wordAndCountDStream: DStream[(String, Int)] = lineDStream.transform(rdd => 
{
 val words: RDD[String] = rdd.flatMap(_.split(" "))
 val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
 val value: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
 value
 })

 //打印
 wordAndCountDStream.print
 
 //启动
 ssc.start()
 ssc.awaitTermination()
 }
  }

案例二:参数为两个,即transformFunc: (RDD[T], Time) => RDD[U]

package cn.kgc.kb09.sparkstream

import java.text.SimpleDateFormat

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkWindowDemo6{
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")
    val streamingContext = new StreamingContext(conf,Seconds(2))


    val kafkaParams: Map[String, String] = Map(
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"),
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup6")
    )
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))

    //业务需求需要更改数据结构是可以使用transform完成转化工作
    val numStream: DStream[((String, String), Int)] = kafkaStream.transform((rdd, timestamp) => {
      val format = new SimpleDateFormat("yyyyMMdd HH:mm:ss")
      val time: String = format.format(timestamp.milliseconds)
      val value: RDD[((String, String), Int)] = rdd.flatMap(x => x.value().toString.split("\\s+"))
        .map(x => ((x, time), 1))
          .reduceByKey(_+_)
          .sortBy(_._2,false)
      value
    })
    numStream.print()

    streamingContext.start()
    streamingContext.awaitTermination()
  }
}

join

  • 两个流之间的 join 需要两个流的批次大小一致,这样才能做到同时触发计算。计算过程就是对当前批次的两个流中各自的 RDD 进行 join,与两个 RDD 的 join 效果相同。
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}

object JoinTest {
 def main(args: Array[String]): Unit = {

 //1.创建 SparkConf
 val sparkConf: SparkConf = new 
SparkConf().setMaster("local[*]").setAppName("JoinTest")
 
 //2.创建 StreamingContext
 val ssc = new StreamingContext(sparkConf, Seconds(5))
 
 //3.从端口获取数据创建流
 val lineDStream1: ReceiverInputDStream[String] = 
ssc.socketTextStream("hadoopwei", 9999)
 val lineDStream2: ReceiverInputDStream[String] = 
ssc.socketTextStream("haoopwei", 8888)
 
 //4.将两个流转换为 KV 类型
 val wordToOneDStream: DStream[(String, Int)] = lineDStream1.flatMap(_.split(" ")).map((_, 1))
 val wordToADStream: DStream[(String, String)] = lineDStream2.flatMap(_.split(" ")).map((_, "a"))

 //5.流的 JOIN
 val joinDStream: DStream[(String, (Int, String))] = wordToOneDStream.join(wordToADStream)

//6.打印
 joinDStream.print()

 //7.启动任务
 ssc.start()
 ssc.awaitTermination()
 } 
 }

2、有状态转化操作

UpdateStateByKey

  • UpdateStateByKey 原语用于记录历史记录,有时,我们需要在 DStream 中跨批次维护状态(例如流计算中累加 wordcount)。针对这种情况,updateStateByKey()为我们提供了对一个状态变量的访问,用于键值对形式的 DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。
  • updateStateByKey() 的结果会是一个新的 DStream,其内部的 RDD 序列是由每个时间区间对应的(键,状态)对组成的。
  • updateStateByKey 操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,需要做下面两步:
    • 定义状态,状态可以是一个任意的数据类型。
    • 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
  • 使用 updateStateByKey 需要对检查点目录进行配置,会使用检查点来保存状态。

案例一:
1)编写代码

object WorldCount {
 def main(args: Array[String]) {
 
 // 定义更新状态方法,参数 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[*]").setAppName("NetworkWordCount")
 val ssc = new StreamingContext(conf, Seconds(3))
 ssc.checkpoint("./ck")
 
 // Create a DStream that will connect to hostname:port, like hadoopwei:8888
 val lines = ssc.socketTextStream("hadoopwei", 8888)
 
 // Split each line into words
 val words = lines.flatMap(_.split(" "))

 //import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
 
 // Count each word in each batch
 val pairs = words.map(word => (word, 1))
 
 // 使用 updateStateByKey 来更新状态,统计从运行开始以来单词总的次数
 val stateDstream = pairs.updateStateByKey[Int](updateFunc)

 stateDstream.print()

 ssc.start() // Start the computation
 ssc.awaitTermination() // Wait for the computation to terminate
 //ssc.stop()
 } 
 }

2)启动程序并向8888端口发送数据

[root@hadoopwei ~]# nc -lk 8888
hello world
hello scala


3)结果展示

-------------------------------------------
Time: 1608651258000 ms
-------------------------------------------
(world,1)
(hello,1)

-------------------------------------------
Time: 1608651261000 ms
-------------------------------------------
(scala,1)
(world,1)
(hello,2)

-------------------------------------------
Time: 1608651264000 ms
-------------------------------------------
(scala,1)
(world,1)
(hello,2)

案例二:

  • 编写代码
package cn.kgc.kb09.sparkstream

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkStreamKafkaSource {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")
    val streamingContext = new StreamingContext(conf,Seconds(5))


    val kafkaParams: Map[String, String] = Map(
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"),
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup1")
    )
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))

    val wordStream: DStream[String] = kafkaStream.flatMap(v=>v.value().toString.split("\\s+"))

  val mapStream: DStream[(String, Int)] = wordStream.map((_,1))

    //无状态
//    val sumStream: DStream[(String, Int)] = mapStream.reduceByKey(_+_)
   // sumStream.print()

   //有状态
    //设置检查点缓存路径
    streamingContext.checkpoint("checkpoint1")
    val stateSumStream: DStream[(String, Int)] = mapStream.updateStateByKey {
      case (seq, buffer) => {
        println(seq,seq.sum,buffer.getOrElse(0))
        val sum: Int = buffer.getOrElse(0) + seq.sum
        Option(sum)
      }
    }
    stateSumStream.print()


    streamingContext.start()

    streamingContext.awaitTermination()

  }
}
  • 开启生成者并生产消息
[root@hadoopwei ~]# kafka-console-producer.sh --broker-list 127.0.0.1:9092 --topic spakKafkaDemo
>hello world
>hello Scala
>

  • 结果展示
-------------------------------------------
Time: 1608650575000 ms
-------------------------------------------

(CompactBuffer(1),1,0)
(CompactBuffer(1),1,0)
-------------------------------------------
Time: 1608650580000 ms
-------------------------------------------
(world,1)
(hello,1)

(CompactBuffer(),0,1)
(CompactBuffer(1),1,1)
(CompactBuffer(1),1,0)
-------------------------------------------
Time: 1608650585000 ms
-------------------------------------------
(world,1)
(hello,2)
(Scala,1)

(CompactBuffer(),0,2)
(CompactBuffer(),0,1)
(CompactBuffer(),0,1)
-------------------------------------------
Time: 1608650590000 ms
-------------------------------------------
(world,1)
(hello,2)
(Scala,1)

(CompactBuffer(),0,2)
(CompactBuffer(),0,1)
(CompactBuffer(),0,1)

WindowOperations

  • Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前 Steaming 的允许状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。

    • 窗口时长:计算内容的时间范围;
    • 滑动步长:隔多久触发一次计算。
  • 注意:这两者都必须为采集周期大小的整数倍。

object WorldCount {
 def main(args: Array[String]) {
 
 val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
 val ssc = new StreamingContext(conf, Seconds(2))
 
 ssc.checkpoint("./ck")
 // Create a DStream that will connect to hostname:port, like localhost:9999
 val lines = ssc.socketTextStream("hadoopwei", 9999)
 
 // Split each line into words
 val words = lines.flatMap(_.split(" "))

// Count each word in each batch
 val pairs = words.map(word => (word, 1))

 //每过两秒统计一个结果,所以第一个两秒内输入的可以出现4次
//  val wordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(8), Duration.apply(2000))
 //每过六秒统计一个结果,所以第一个六秒内输入只能出现一次,因为下一次滑动六秒统计时已经不在原先的窗口。
 val wordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(8), Duration.apply(6000))
 
 // Print the first ten elements of each RDD generated in this DStream to the console
 wordCounts.print()
 ssc.start() // Start the computation
 ssc.awaitTermination() // Wait for the computation to terminate
 }
  }
  • 关于 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 任务的数量通过可选参数来配置。
      在这里插入图片描述
val ipDStream = accessLogsDStream.map(logEntry => (logEntry.getIpAddress(), 1))
val ipCountDStream = ipDStream.reduceByKeyAndWindow(
 {(x, y) => x + y},
 {(x, y) => x - y},
 Seconds(30),
 Seconds(10))
 //加上新进入窗口的批次中的元素 //移除离开窗口的老批次中的元素 //窗口时长// 滑动步长
  • countByWindow()和 countByValueAndWindow()作为对数据进行计数操作的简写。
    • countByWindow()返回一个表示每个窗口中元素个数的 DStream,而countByValueAndWindow()返回的 DStream 则包含窗口中每个值的个数。
val ipDStream = accessLogsDStream.map{entry => entry.getIpAddress()}
val ipAddressRequestCount = ipDStream.countByValueAndWindow(Seconds(30), Seconds(10)) 
val requestCount = accessLogsDStream.countByWindow(Seconds(30), Seconds(10))

3、DStream转为SparkSql语句

package cn.kgc.kb09.sparkstream

import java.text.SimpleDateFormat
import java.util.zip.InflaterOutputStream

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, Dataset, Row, SQLContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkWindowDemo7{
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")
    val streamingContext = new StreamingContext(conf,Seconds(2))


    val kafkaParams: Map[String, String] = Map(
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"),
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup7")
    )
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))


    val numStream: DStream[Row] = kafkaStream.transform(rdd => {
      val sqlContext: SQLContext = SQLContextSingleton.getInstance(rdd.sparkContext)
      import sqlContext.implicits._
      val words: RDD[String] = rdd.flatMap(_.value().toString.split("\\s+"))
      val tupple2RDD: RDD[(String, Int)] = words.map((_, 1))
      tupple2RDD.toDF("name", "cn")
        .createOrReplaceTempView("tbwordcount")

      val frame: DataFrame= sqlContext.sql("select name,count(cn) from tbwordcount group by name")
       frame.show()

      //DataFrame转DataSet
//      val DS1: Dataset[(String, Long)] = frame.map(row => {
//        val name: String = row.getAs[String]("name")
//        val cn: Long = row.getAs[Long]("cn")
//        (name, cn)
//      })
    // val DS2: Dataset[(String, Long)] = frame.as[(String,Long)]

      frame.rdd

    })

   numStream.print()
    streamingContext.start()
    streamingContext.awaitTermination()

  }
}

object SQLContextSingleton{
 @transient private var instance:SQLContext=_
  def getInstance(sparkContext: SparkContext):SQLContext={
    synchronized(
    if(instance==null){
      instance=new SQLContext(sparkContext)
    }
    )
    instance
  }
}

五 DStream输出

1、输出操作

  • 输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。与 RDD 中的惰性求值类似,如果一个 DStream 及其派生出的 DStream 都没有被执行输出操作,那么这些 DStream 就都不会被求值。如果 StreamingContext 中没有设定输出操作,整个 context 就都不会启动,会出现ERROR:ERROR StreamingContext: Error starting the context, marking it as stopped java.lang.IllegalArgumentException: requirement failed: No output operations registered, so nothing to execute
  • 输出操作如下:
    • print():在运行流程序的驱动结点上打印 DStream 中每一批次数据的最开始 10 个元素。这用于开发和调试。在 Python API 中,同样的操作叫 print()。
    • saveAsTextFiles(prefix, [suffix]):以 text 文件形式存储这个 DStream 的内容。每一批次的存储文件名基于参数中的 prefix 和 suffix。”prefix-Time_IN_MS[.suffix]”。
    • saveAsObjectFiles(prefix, [suffix]):以 Java 对象序列化的方式将 Stream 中的数据保存为SequenceFiles . 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。
    • saveAsHadoopFiles(prefix, [suffix]):将 Stream 中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。Python API 中目前不可用。
    • foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream 的每一个RDD。其中参数传入的函数 func 应该实现将每一个 RDD 中数据推送到外部系统,如将RDD 存入文件或者通过网络将其写入数据库。
  • 通用的输出操作foreachRDD(),它用来对 DStream 中的 RDD 运行任意计算。这和transform() 有些类似,都可以让我们访问任意 RDD。在 foreachRDD()中,可以重用我们在 Spark 中实现的所有行动操作。比如,常见的用例之一是把数据写到诸如 MySQL 的外部数据库中。
  • 注意:
    • 和数据库等的JDBC连接不能写在 driver 层面(序列化原因)
    • 如果写 foreach 则每个 RDD 中的每一条数据都创建连接,得不偿失;
    • 增加 foreachPartition,在分区创建(获取)JDBC连接,则每个分区只创建一次。但可能会造成OOM即内存溢出。
    • foreach...等这些是没有返回值的,或者说返回值为空,也就不需要.print(),所以输出结果没有时间戳界面
object SparkWindowDemo8{
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")
    val streamingContext = new StreamingContext(conf,Seconds(2))


    val kafkaParams: Map[String, String] = Map(
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"),
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup8")
    )
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))

 
 //foreachRDD,foreach,foreachPartition综合比较
     println("driver")
    kafkaStream.foreachRDD(
      (rdd)=> { //针对每个rdd
        println("bb")  //进来一个rdd就会输出一次,而一个rdd即是一个批处理时间段内的分布式数据集合

        //       val array: Array[ConsumerRecord[String, Int]] = rdd.collect() //收集到Driver端,实际上是不好实现的,因为跨机器传输需要实现序列化
        //        if(array.length>=1){
        //        array.foreach(x=>{//x为每个executor上的数据)
        //          println("cc")
        //         x.value().toString.foreach(y=>
        //           println("dd:",y)
        //         )
        //        }
        //        )}


        rdd.foreach(
          x => { //每个批数据中的每一行数据
            println("cc")  //有n行数据就打印n次
            val strings: Array[String] = x.value().toString.split("\\s+")
            println(x.key(), strings.toList)
          }
        )
                   //推荐使用,但可能会内存溢出
//                rdd.foreachPartition(  
//                  x=>{//这里的x为rdd中的每个分区
//                    println("cc")
//                    x.foreach( 
//                      y=>{ //每个分区的每条数据执行下面计算逻辑,由分区主导
//                        println("dd")
//                        println(y.value().toString.split("\\s+").toList)
//                      }
//                    )
//                  }
//                )

              }
            )
        streamingContext.start()
        streamingContext.awaitTermination()
      }
  }

2、Driver端和Executor端执行逻辑

package cn.kgc.kb09.sparkstream

import java.text.SimpleDateFormat

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkWindowDemo8{
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("kafkaSource")
    val streamingContext = new StreamingContext(conf,Seconds(2))


    val kafkaParams: Map[String, String] = Map(
      (ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "192.168.198.201:9092"),
      (ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> " org.apache.kafka.common.serialization.StringDeserializer"),
      (ConsumerConfig.GROUP_ID_CONFIG, "kafkaGroup9")
    )
    val kafkaStream: InputDStream[ConsumerRecord[String, Int]] = KafkaUtils.createDirectStream(
      streamingContext,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe(Set("sparkKafkaDemo"), kafkaParams))

         println("driver")  //主程序在driver端执行,只执行一次
    val wordStream: DStream[String] = kafkaStream.flatMap( //同一个批处理时间内,输入n行数据,下方计算逻辑会执行n次
      line => {  //当取得流数据时,计算逻辑在各自executor上执行,driver端和多个executor端往往不在一个节点上
        println("executor") //一个批处理时间内,有n行数据就执行n次
        line.value().toString.split("\\s+")
      }
    )
    wordStream.print()

//    println("driver")  //主程序只执行一次
//    val wordStream: DStream[String] = kafkaStream.transform(
//      (rdd) => {   //每过一个采集周期都会传过来组成stream的RDD分布式数据集(注:这里的RDD是一个采集周期的批数据而形成的分布式数据集合)
//        println("bb")   //来一个RDD执行一次
//        val value: RDD[String] = rdd.flatMap(  //没有数据就不会进入flatMap
//          x => { //这里的x是对应于这个分布式数据集的每一行数据
//            println("cc")  //计算逻辑在executor中执行,当有数据后才执行,有多少行执行多少次
//            x.value().toString.split("\\s+")
//          }
//        )
//        value
//      }
//    )
//    wordStream.print()

        streamingContext.start()
        streamingContext.awaitTermination()

      }
  }

六 优雅关闭SparkStream-使用HDFS系统做消息通知

  • 流式任务需要 7*24 小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。使用外部文件系统来控制内部程序关闭。

  • 在驱动程序中,加一段代码,这段代码的作用每隔一段时间可以是10秒也可以是3秒,扫描HDFS上某一个文件,如果发现这个文件存在,就调用StreamContext对象stop方法,自己优雅的终止自己,其实这里HDFS可以换成redis,zk,hbase,db都可以,这里唯一的问题就是依赖了外部的一个存储系统来达到消息通知的目的,如果使用了这种方式后。停止流程序就比较简单了,登录上有hdfs客户端的机器,然后touch一个空文件到指定目录,然后等到间隔的扫描时间到之后,发现有文件存在,就知道需要关闭程序了。

  • 生成文件后,n秒后程序就会自动停止hdfs dfs -touch stop.txt /stopSparkStream

  • 下次启动前,需要清空这个文件(上传空白文件后,这个指定目录会变为空白文件),否则程序启动后就会停止hdfs dfs -rmr /stopSparkStream

(1)编写关闭流的Scala类MonitorStop

package cn.kgc.kb09.stopStream

import java.net.URI
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.spark.streaming.{StreamingContext, StreamingContextState}

//编写关闭流的类
class MonitorStop(ssc: StreamingContext) extends Runnable {

  override def run(): Unit = {

    //获取文件系统为HDFS
    val fs: FileSystem = FileSystem.get(new URI("hdfs://hadoopwei:9000"), new Configuration(), "root")


    while (true) {
      try
        //每过5秒使当前线程休眠
        Thread.sleep(10000)
      catch {
        case e: InterruptedException =>
          e.printStackTrace()
      }

      //获取StreamContext的状态
      val state: StreamingContextState = ssc.getState

      //判读HDFS路径上目录下是否存在空文件
      val bool: Boolean = fs.exists(new Path("hdfs://hadoopwei:9000/stopSpark"))

      //存在空文件则进入停止程序
      if (bool) {
        //判断如果状态是活跃的就需要停止程序
        if (state == StreamingContextState.ACTIVE) {
          ssc.stop(stopSparkContext = true, stopGracefully = true)
          System.exit(0)
        }
      }
    }
  }
}

(2)编写Spark流计算逻辑

package cn.kgc.kb09.stopStream
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkTest {
  def createSSC(): _root_.org.apache.spark.streaming.StreamingContext = {

    val update: (Seq[Int], Option[Int]) => Some[Int] = (values: Seq[Int], status:
    Option[Int]) => {
      //当前批次内容的计算
      val sum: Int = values.sum
//取出状态信息中上一次状态
val lastStatu: Int = status.getOrElse(0)
      Some(sum + lastStatu)
    }

    val sparkConf: SparkConf = new SparkConf().setMaster("local[4]").setAppName("SparkTest")

    //设置优雅的关闭
    sparkConf.set("spark.streaming.stopGracefullyOnShutdown", "true")

    val ssc = new StreamingContext(sparkConf, Seconds(5))
    //计算逻辑
    ssc.checkpoint("./ck")
    val line: ReceiverInputDStream[String] = ssc.socketTextStream("hadoopwei", 7777)
    val word: DStream[String] = line.flatMap(_.split(" "))
    val wordAndOne: DStream[(String, Int)] = word.map((_, 1))
    val wordAndCount: DStream[(String, Int)] = wordAndOne.updateStateByKey(update)
    wordAndCount.print()
    ssc
  }
  def main(args: Array[String]): Unit = {
    val ssc: StreamingContext = StreamingContext.getActiveOrCreate("./ck", () =>
      createSSC())
    new Thread(new MonitorStop(ssc)).start()
    ssc.start()
    ssc.awaitTermination()

  }
}

(3)正常执行流,如果想要关闭流,则上传一个空白文件到hdfs://hadoopwei:9000/stopSpark目录下即可

[root@hadoopwei conf]# hdfs dfs -put stop.txt /stopSpark
20/12/24 16:02:54 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable

会出现下面结果:

-------------------------------------------
Time: 1608822175000 ms
-------------------------------------------
(hello,4)
(java,1)
(world,2)

20/12/24 23:02:56 ERROR ReceiverTracker: Deregistered receiver for stream 0: Stopped by driver
Exception in thread "receiver-supervisor-future-0" java.lang.Error: java.lang.InterruptedException: sleep interrupted
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1155)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at org.apache.spark.streaming.receiver.ReceiverSupervisor$$anonfun$restartReceiver$1.apply$mcV$sp(ReceiverSupervisor.scala:196)
	at org.apache.spark.streaming.receiver.ReceiverSupervisor$$anonfun$restartReceiver$1.apply(ReceiverSupervisor.scala:189)
	at org.apache.spark.streaming.receiver.ReceiverSupervisor$$anonfun$restartReceiver$1.apply(ReceiverSupervisor.scala:189)
	at scala.concurrent.impl.Future$PromiseCompletingRunnable.liftedTree1$1(Future.scala:24)
	at scala.concurrent.impl.Future$PromiseCompletingRunnable.run(Future.scala:24)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	... 2 more
-------------------------------------------
Time: 1608822180000 ms
-------------------------------------------
(hello,4)
(java,1)
(world,2)

-------------------------------------------
Time: 1608822185000 ms
-------------------------------------------
(hello,4)
(java,1)
(world,2)


Process finished with exit code 0

  • 可能出现的ERROR:Exception in thread “streaming-start” java.lang.StackOverflowError
    • 原因为:堆栈溢出,往往是我们在执行spark-submit程序时分配的driver-memory 资源不够,即JVM内存不够。
    • 解决方案:一种是增加JVM内存--driver-java-options "-Xss10m -XX:+UseConcMarkSweepGC" --conf spark.executor.extraJavaOptions="-Xss10m -XX:+UseConcMarkSweepGC"。另一种方案在本程序内是直接清空检查点路径目录,即把以往的缓存流信息全部删除,当然大大不提倡。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值