Spark:Streaming

分布式计算平台Spark:Streaming

一、回顾

  1. 离线案例

    • 工作中开发代码流程或者方式
    • SparkCore + SparkSQL:熟悉代码开发
      • DSL:when(条件,成立的返回值).otherwise(不成立的返回值)
      • SQL:with 别名 as (SQL) select * from 别名
    • 工具类补充:配置文件解析、IP解析工具类
  2. 流式计算的介绍

    • 目的:实现实时数据流的处理,构建实时应用
      • 实时数据分析
      • 实时推荐系统
      • 实时风控系统
      • 实时物联网系统……
    • 保证:整个过程都是实时的
      • 数据生成
      • 数据采集:Flume
      • 数据存储:Kafka、HBASE、Redis
      • 数据计算:SparkStreaming、Flink
    • 分类
      • 真实时计算:以数据为单位,每产生一条,就实时计算一条数据
        • Spark StructStreaming【实验阶段】、Flink
      • 准实时计算:以微小批处理时间来模拟实时计算,效果类型
        • 每200ms处理一次数据
        • SparkStreaming
  3. 问题

    • 对封装方法不熟悉:不知道参数、返回值

    • DAG的作用

      • 目的:解析代码构建执行计划

      • DAG图:是附加产品,方便实现监控、优化、管理而显示的

二、目标

  1. SparkStreaming原理
    • 核心抽象:DStream
    • 本质:基于SparkCore封装了RDD,构建微小时间的批处理,模拟实时计算
  2. SparkStreaming使用
    • 流式计算 中三种业务模式
    • 读写数据
      • 读:Kafka
      • 写:Kafka、HBASE、Redis、MySQL
    • 案例:模拟百度热搜来实现三种业务场景下的实时计算开发
    • 核心:SparkStreaming消费Kafka的数据怎么保证数据一致性问题
      • Offset:掌握如何手动管理Offset

三、Streaming流式计算原理

1、SparkStreaming介绍

Spark Streaming is an extension of the core Spark API that enables scalable, high-throughput, fault-tolerant stream processing of live data streams. Data can be ingested from many sources like Kafka, Flume, Kinesis, or TCP sockets, and can be processed using complex algorithms expressed with high-level functions like map, reduce, join and window. Finally, processed data can be pushed out to filesystems, databases, and live dashboards. In fact, you can apply Spark’s machine learning and graph processing algorithms on data streams.
  • SparkStreaming是基于SparkCore,底层还是SparkCore

    • 区别:SparkStreaming是通过微小时间的批处理来模式实时计算

      • SparkCore:遇到触发函数才产生job,才会运行task

        • count、first:遇到对数据的使用就会触发task的运行
      • SparkStreaming:按照固定的时间周期触发job,运行task

        • 每1s执行一次,不论有没有数据,需不需要用数据

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLLsZLeh-1618212555381)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225093631272.png)]

        • 将源源不断的数据流进行切分,划分为多个批次
          • 切分规则:按照时间来切分Batch批次
        • 将每个Batch批次的数据交给SparkCore来实现每个批次的处理
          • 每个批次会触发一个job的运行
  • 可扩展、高容错、高吞吐:都是SparkCore特性

  • 支持多种流式的数据源

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7JZZD7Z-1618212555383)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225093615380.png)]

  • 可以直接通过RDD的编程方式来实现

  • ||

  • DStream

2、自定义开发测试

  • 基本规则

    • StreamingContext:基于SparkContext封装以后,用于实时计算中的Driver对象

      • SparkCore
        • SparkContext => RDD
      • SparkSQL
        • SparkSession => DataSet
      • SparkSrtreaming
        • StreamingContext => DStream
    • 程序的运行

      • 离线的批处理:数据是固定的,处理完程序自动结束
      • 实时数据流计算:数据是数据流【无边界的数据】,永远不断的7*24小时的计算处理,正常是不会手动停止的
        • 除非遇到故障或者需要变更,强制停止
          //启动数据流计算的运行
          ssc.start()
          //让程序持续运行,除非手动终止
          ssc.awaitTermination()
          //最后一句代码,只有程序意外终止,才会执行
          ssc.stop(true,true)
      
    • 开发模板

      package bigdata.itcast.cn.spark.stream.mode
      
      import org.apache.spark.SparkConf
      import org.apache.spark.streaming.{Seconds, StreamingContext}
      
      /**
        * @ClassName SparkStreaming
        * @Description TODO SparkStreaming计算的模板
        * @Date 2020/12/25 9:39
        * @Create By     Frank
        */
      object SparkStreamingMode {
      
        def main(args: Array[String]): Unit = {
          /**
            * step1:先构建Driver对象
            *    StreamingContext
            */
          //定义一个SparkConf
          val conf = new SparkConf()
            //设置程序名称
            .setAppName(this.getClass.getSimpleName.stripSuffix("$"))
            //设置运行的Master
            .setMaster("local[3]")
          //构建StreamingContext实例
          //第一个参数是Spark的配置对象,用于构建SparkContext对象
          //第二个参数表示每个批次的划分时间:Batch_Interval
          val ssc = new StreamingContext(conf,Seconds(3))
          //调整日志级别
      //    ssc.sparkContext.setLogLevel("WARN")
      
      
          /**
            * step2:实现数据的处理逻辑
            */
          //todo:1-读取数据
          //todo:2-转换数据
          //todo:3-保存结果
      
      
          /**
            * step3:释放资源,停止程序
            */
          //启动数据流计算的运行
          ssc.start()
          //让程序持续运行,除非手动终止
          ssc.awaitTermination()
          //最后一句代码,只有程序意外终止,才会执行
          ssc.stop(true,true)
      
        }
      }
      
      
  • WordCount实现

    • 代码

         /**
            * step2:实现数据的处理逻辑
            */
          //todo:1-读取数据
          //从Linux的第一台机器的9999端口上获取数据,然后处理分析
          //输入流中的每一个元素就是原始数据的每一行
          val inputStream: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9999,StorageLevel.MEMORY_AND_DISK_SER)
      
          //todo:2-转换数据
          val rsStream = inputStream
              .filter(line => line != null && line.trim.length > 0)
              .flatMap(line => line.trim.split("\\s+"))
              .map(word => (word,1))
              .reduceByKey(_+_)
      
          //todo:3-保存结果
          rsStream.print(10)//表示显示结果中的前10行
      
    • 结果

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YM37Uin-1618212555384)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101119401.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weuvhYwt-1618212555385)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101249568.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KHc1pQZ-1618212555386)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101557564.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nAqjFsoy-1618212555387)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101715514.png)]

3、Spark Streaming的原理

  • step1:启动流式程序

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vgZhlqSi-1618212555388)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101740321.png)]

    • 提交程序运行,构建Driver进程
    • Driver进程申请启动所有Executor进程
  • step2:接受数据

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DPGzg5pU-1618212555388)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225101842118.png)]

    • Driver会在Executor中使用1coreCPU来启动一个特殊的Task:ReceiverTask

    • ReceiverTask负责源源不断的接受数据:ReceiverTask会持久的运行在Executor中

    • 如果有多个数据源,每个数据源有一个Receiver

      //    val inputStream1: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9999,StorageLevel.MEMORY_AND_DISK_SER)
      //    val inputStream2: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9998,StorageLevel.MEMORY_AND_DISK_SER)
          //如果有多个数据源的数据,要进行合并
      //    val inputStream = inputStream1.union(inputStream2)
      
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hBpgjcz7-1618212555389)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225102208363.png)]

  • step3:receiver将接受到的数据进行缓存

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zWGlPq1N-1618212555389)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225102452159.png)]

    def socketTextStream(
          hostname: String,
          port: Int,
          //设置缓存的级别
          storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
        ): ReceiverInputDStream[String]
    
    • ReceiverTask会对接受到的数据流按照时间进行划分Block

      • BatchInterval:批次时间:job运行的时间

        • BatchInterval = 1:表示程序每1s,处理这一秒接受到的数据
        • |
        • 每一秒的数据就是一个RDD
      • BlockInterval:数据块的划分间隔:数据缓存的划分时间

        • BlockInterval=200ms:表示ReceiverTask每200ms就将这200ms的数据缓存在Executor的内存中

        • |

        • 每一个Block就是RDD的一个分区

        • 配置

          spark.streaming.blockInterval=200ms	Minimum recommended - 50 ms.
          
  • step4:会将每个Block的存储位置汇报给Drive,Driver等到BatchInterval

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0vteBwa-1618212555390)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225103449284.png)]

    • receive会将每个block的位置汇报给Driver,Driver不会处理数据
  • step5:对这个批次的Task解析,分配给Excutor运行所有的Task

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPzRop4E-1618212555390)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225103813066.png)]

    • Driver等到BatchInterval的时间到达,触发job的运行,对这1s内的所有Block的数据进行处理
      • 处理的时候
      • 将这个1s批次时间内的每个Block作为每个分区
      • 整个批次作为一个RDD
    • Driver解析整个Job ,基于数据构建Task,分配运行

四、DStream数据结构

1、本质及结构特点

  • 本质:DStream = Seq[RDD]

    • DStream知识对RDD构建了集合,底层本质还是RDD,所有转换处理都是RDD
    • DStream在时间上的一个RDD的集合,每隔一个BatchInterval的批次时间就会构建一个RDD,对这个RDD做转换处理

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GIGEwbo-1618212555391)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225105445248.png)]

  • 官网的介绍

    • Discretized Streams (DStreams)

      Discretized Stream or DStream is the basic abstraction provided by Spark Streaming. It represents a continuous stream of data, either the input data stream received from source, or the processed data stream generated by transforming the input stream. Internally, a DStream is represented by a continuous series of RDDs, which is Spark’s abstraction of an immutable, distributed dataset (see Spark Programming Guide for more details). Each RDD in a DStream contains data from a certain interval, as shown in the following figure.
      

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5SDFcUW-1618212555391)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225105610674.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ss0ZCcrq-1618212555391)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225105633434.png)]

  • 结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SGgeSKZ4-1618212555392)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225105937521.png)]

    • 红色线:代表一个DStream

    • 黄色线:代表一个RDD

      • 划分规则:BatchInterval
    • 绿色线:代表RDD的一个分区,就是一个Block

      • 划分规则:BlockInterval
    • 横向的箭头:DStream之间的转换就是RDD的转换,RDD转换就是每个分区间的转换

2、DStream函数

  • 转换函数:实现DStream之间的转换,本质还是底层RDD的转换函数

    image-20201225110455977
    • 用法基本一致:map/flatMap/filter/reduceByKey

    • transform:直接对DStream基于RDD编程,直接获取DStream中的每个RDD来处理

      • 使用类似于map函数,但是可以直接对RDD进行操作

      • 定义

        def transform[U: ClassTag](transformFunc: (RDD[T], Time) => RDD[U]): DStream[U]
        def transform[U: ClassTag](transformFunc: RDD[T] => RDD[U]): DStream[U]
        
      • 使用

            val rsStream: DStream[(String, Int)] = inputStream
                //直接调用Transform函数,对DStream中的每个RDD来操作
                .transform(rdd => {
                  //对内部的每个RDD做转换
                  rdd
                    .filter(line => line != null && line.trim.length > 0)
                    .flatMap(line => line.trim.split("\\s+"))
                    .map(word => (word,1))
                    .reduceByKey(_+_)
            })
        
      • 注意:由于DStream底层是对RDD封装,以后开发时,能用RDD实现的操作就不要直接对DStream

        • 因为底层基于RDD的解析层次更高,优化的性能会更好
  • 输出函数:注意:这里没有触发函数,因为SparkStreaming按照时间触发job的构建

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LZXlI0hr-1618212555392)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225110734050.png)]

    • 常用的函数

      • print(N):打印结果,N表显示结果重点 几条,默认显示10条

        • 直接对DStream操作的
      • saveAsTextFile:保存到文件系统

      • foreachRDD:对每个RDD进行foreach操作

        • 定义

          def foreachRDD(foreachFunc: (RDD[T]) => Unit): Unit
          def foreachRDD(foreachFunc: (RDD[T], Time) => Unit): Unit
          
        • 使用

              rsStream.foreachRDD((rdd,time) => {
                //判断rdd是否有数据,没有数据就不输出
                if(!rdd.isEmpty()){
                  val printTime = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").format(time.milliseconds)
                  println("=====================================")
                  println(printTime)
                  println("=====================================")
                  //对每个rdd进行打印
                  //      rdd.foreachPartition(part => part.foreach(println))
                  rdd.foreach(println)
                  println()
                }
          
              })
          

五、流式处理的三种业务模式

1、三种业务模式

  • 无状态模式:每次批次的计算与之前的批次的结果是没有关系的,一对一的场景,每个批次的结果就是最后的结果

    • 应用:实现实时ETL的操作

    • 举个栗子:实现Ip地址的实时解析,每3s处理一次

      12:00:00 ~ 12:00:03	
      	192.168.134.1
      	192.168.134.2
      	192.168.134.3
      12:00:03 ~ 12:00:06	
      	192.168.134.4
      	192.168.134.5
      	192.168.134.6
      	
      需求:解析IP地址,得到国家省份城市,存储到Hive表中【IP、国家、省份、城市】
      12:00:00 ~ 12:00:03	第一个批次运行
      	192.168.134.1		中国		上海		上海
      	192.168.134.2		中国		上海		上海
      	192.168.134.3		中国		上海		上海
      12:00:03 ~ 12:00:06	第二个批次运行
      	192.168.134.4		中国		上海		上海
      	192.168.134.5		中国		上海		上海
      	192.168.134.6		中国		上海		上海
      
  • 有状态计算

  • 窗口计算

2、有状态计算

  • 功能:当前这个批次的结果与之前批次的结果要实现聚合,聚合的结果才是这个批次的最终结果

  • 举个栗子

    • 双十一:实时订单总金额的累加统计,每3s处理一次

      00:00:00 ~ 00:00:03
      	这3秒内的订单金额:1亿		显示结果:1亿
      00:00:03 ~ 00:00:06
      	这3秒内的订单金额:1亿		显示结果:2亿
      ……
      
      23:59:57 ~ 00:00:00
      	这3秒内的订单金额:1000万		显示结果:这天所有结果的累加
      
    • 以词频统计为例,计算的过程

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ialjfPv-1618212555393)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225114902401.png)]

      • 问题:上一个批次的结果怎么存储,怎么记录上一个批次的结果状态

3、窗口计算

  • 功能:按照一定的时间周期,计算一定时间范围内的数据

    • 滑动时间:Slide Interval
      • 执行的周期
    • 窗口大小:Window Interval
      • 每次执行的数据时间范围
  • 举个栗子:每天统计最近7天的热门搜索

    • 每1天计算一次
    • 最近7天的数据:动态的
      • 针对于每一天,你的最近7天都是不一样的
    • 滑动时间:1天
    • 窗口时间:7天
  • 要求:窗口时间和滑动时间必须为BatchInter的倍数

  • 设计

    • 每3秒处理前6s的数据

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WP1OTh6-1618212555393)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225115938238.png)]

    • 每6秒处理前9s的数据

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7bssJ7zF-1618212555393)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225120253906.png)]

六、SparkStreaming与Kafka集成

1、集成方式

  • 流式计算在工作中的主要数据源是:Kafka

  • 官方文档:http://spark.apache.org/docs/2.4.5/streaming-kafka-integration.html

  • 集成版本

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cfiOB1G-1618212555394)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225121036889.png)]

    • spark-streaming-kafka-0-8

      • 0.8.x

      • 以前用的版本,现在基本不会用到的

      • 支持两种与SparkStreaming集成的方式

        • 方式一:Receiver

          • 使用Kafka的高级API来实现的,自动将offset存储在Zookeeper中

          • 类似于SparkStreaming的默认方式,需要启动ReceiverTask实现数据的接受

          • 数据由Kafka推送给SparkStreaming的Receiver

          • SparkStreaming中的RDD分区数 = Block个数

            [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwvD7hb6-1618212555394)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225143907246.png)]

        • 方式二:Direct方式

          • 基于Kafka SimpleAPI来实现的,可以由自己来实现Offset管理

          • 没有Receiver,所有数据由SparkStreaming到Kafka的Topic的每个分区中去拉取数据

          • 每次到达一个批次时间,就主动的到Kafka中获取当前每个分区的最新的offset,将上一次处理的最后的offset进行组合,得到这次要消费的范围,再向Kafka申请消费

            • 第一个批次:12:00:00 ~ 12:00:03

              • step1:查询Kafka每个分区的最新偏移量

                part0:20
                part1:19
                part2:30
                
              • step2:构建本次处理的范围

                part0:【0,21)
                part1:【0,20)
                part2:【0,31)
                
              • step3:按照范围向Kafka请求获取数据,进行计算处理

            • 第二个批次:12:00:03 ~ 12:00:06

              • step1:查询Kafka每个分区的最新偏移量

                part0:40
                part1:30
                part2:45
                
              • step2:基于上一次处理的范围结合最新的数据offset构建本次请求的范围

                part0:【21,41)
                part1:【20,31)
                part2:【31,46)
                
              • step3:按照范围向Kafka请求获取数据,进行计算处理

            • 优点

              • Simplified Parallelism:Kafka的每一个分区对应RDD的每个分区
              • Efficiency:高性能的,不用写WAL,如果数据丢失,直接重新请求offset即可
              • Exactly-once semantics:可以实现一次性语义,保证数据不丢失,不重复
                • 如果offset消费成功,获取下一个批次的数据,不会重复
                • 如果offset消费失败,重新消费失败的offset,不会丢失

            [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lVfwZ2Za-1618212555395)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225145346274.png)]

      • 不允许commit Offset,无法手动管理offset

    • spark-streaming-kafka-0-10

      • 0.10及以上版本
      • 现在用的版本
      • 只有一种方式与SparkStreaming集成
        • 类似于0.8版本中的direct方式来实现的
    • 允许commit Offset,可以实现手动管理,实现一次性语义

2、集成实现

  • 先参考附录一:启动Kafka集群,并创建Topic

  • 代码实现

    package bigdata.itcast.cn.spark.stream.kafka
    
    import org.apache.commons.lang.time.FastDateFormat
    import org.apache.kafka.clients.consumer.ConsumerRecord
    import org.apache.kafka.common.serialization.StringDeserializer
    import org.apache.spark.SparkConf
    import org.apache.spark.streaming.dstream.{DStream, InputDStream}
    import org.apache.spark.streaming.kafka010._
    import org.apache.spark.streaming.{Seconds, StreamingContext}
    
    /**
      * @ClassName SparkStreaming
      * @Description TODO SparkStreaming消费Kafka数据
      * @Date 2020/12/25 9:39
      * @Create By     Frank
      */
    object SparkStreamingReadKafka {
    
      def main(args: Array[String]): Unit = {
        /**
          * step1:先构建Driver对象
          *    StreamingContext
          */
        //定义一个SparkConf
        val conf = new SparkConf()
          //设置程序名称
          .setAppName(this.getClass.getSimpleName.stripSuffix("$"))
          //设置运行的Master
          .setMaster("local[3]")
        //构建StreamingContext实例
        //第一个参数是Spark的配置对象,用于构建SparkContext对象
        //第二个参数表示每个批次的划分时间:Batch_Interval
        val ssc = new StreamingContext(conf,Seconds(3))
        //调整日志级别
    //    ssc.sparkContext.setLogLevel("WARN")
    
    
        /**
          * step2:实现数据的处理逻辑
          */
        //todo:1-读取数据
        /**
          * def createDirectStream[K, V](
          * ssc: StreamingContext,
          * locationStrategy: LocationStrategy,
          * consumerStrategy: ConsumerStrategy[K, V]
          * ): InputDStream[ConsumerRecord[K, V]]
          */
        //Spark用于计算Kafka集群位置的本地优先计算的模式
        val locationStrategy: LocationStrategy = LocationStrategies.PreferConsistent
        //配置连接Kafka的属性的
        /**
          * def Subscribe[K, V](
          * topics: Iterable[jl.String],
          * kafkaParams: collection.Map[String, Object])
          */
        //用于指定消费哪些Topic的数据
        val topics = Array("sparkStream01")
        //用于指定Kafka的连接属性
        val kafkaParams = Map[String, Object](
          "bootstrap.servers" -> "node1:9092,node2:9092",
          "key.deserializer" -> classOf[StringDeserializer],
          "value.deserializer" -> classOf[StringDeserializer],
          "group.id" -> "group_test_spark01",
          "auto.offset.reset" -> "latest",
          "enable.auto.commit" -> (false: java.lang.Boolean)
        )
        val consumerStrategy: ConsumerStrategy[String, String] = ConsumerStrategies.Subscribe(
          topics,//指定消费的Topic
          kafkaParams//指定连接属性
        )
        /**
          * 消费Kafka的数据
          */
        //每个ConsumerRecord对象,代表Kafka中的一条数据
        val kafkaStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
          ssc,//StreamingContext对象
          locationStrategy,//本地优先计算配置
          consumerStrategy//消费Kafka的配置
        )
        //todo:2-转换数据
        //取出真正数据,value
        val inputStream: DStream[String] = kafkaStream
          //将Value取出
          .map(record => record.value())
        //对每行的value的数据做单词统计
        val rsStream: DStream[(String, Int)] = inputStream
          //直接调用Transform函数,对DStream中的每个RDD来操作
          .transform(rdd => {
          //对内部的每个RDD做转换
          rdd
            .filter(line => line != null && line.trim.length > 0)
            .flatMap(line => line.trim.split("\\s+"))
            .map(word => (word,1))
            .reduceByKey(_+_)
        })
        //todo:3-保存结果
        rsStream.foreachRDD((rdd,time) => {
          //判断rdd是否有数据,没有数据就不输出
          //      if(!rdd.isEmpty()){
          val printTime = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").format(time.milliseconds)
          println("=====================================")
          println(printTime)
          println("=====================================")
          //对每个rdd进行打印
          //      rdd.foreachPartition(part => part.foreach(println))
          rdd.foreach(println)
          println()
          //      }
    
        })
    
    
        /**
          * step3:释放资源,停止程序
          */
        //启动数据流计算的运行
        ssc.start()
        //让程序持续运行,除非手动终止
        ssc.awaitTermination()
        //最后一句代码,只有程序意外终止,才会执行
        ssc.stop(true,true)
    
      }
    }
    
    

3、获取偏移量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xrPdBM5F-1618212555395)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225152147337.png)]

  • 需求:获取我们每次处理的offset,自己存储起来,如果处理失败,重新提交这个offset,如果处理成功,就获取下一批offset

  • 获取offset

    • 每个批次就是一个RDD
    • 这个RDD中可以记录这个批次中对应Offset的位置
    val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
    
    //打印这个批次的offset
          offsetRanges.foreach(offsetRange => {
            //得到每个批次的offset的信息
            val topic = offsetRange.topic
            val part = offsetRange.partition
            val startOffset = offsetRange.fromOffset
            val endOffset = offsetRange.untilOffset
            println(topic+"\t"+part+"\t"+startOffset+"\t"+endOffset)
          })
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMutf7mK-1618212555396)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225153325447.png)]

    package bigdata.itcast.cn.spark.stream.offset
    
    import org.apache.commons.lang.time.FastDateFormat
    import org.apache.kafka.clients.consumer.ConsumerRecord
    import org.apache.kafka.common.serialization.StringDeserializer
    import org.apache.spark.SparkConf
    import org.apache.spark.streaming.dstream.InputDStream
    import org.apache.spark.streaming.kafka010._
    import org.apache.spark.streaming.{Seconds, StreamingContext}
    
    /**
      * @ClassName SparkStreaming
      * @Description TODO SparkStreaming消费Kafka数据,并打印偏移量
      * @Date 2020/12/25 9:39
      * @Create By     Frank
      */
    object SparkStreamingReadKafkaOffset{
    
      def main(args: Array[String]): Unit = {
        /**
          * step1:先构建Driver对象
          *    StreamingContext
          */
        //定义一个SparkConf
        val conf = new SparkConf()
          //设置程序名称
          .setAppName(this.getClass.getSimpleName.stripSuffix("$"))
          //设置运行的Master
          .setMaster("local[3]")
        //构建StreamingContext实例
        //第一个参数是Spark的配置对象,用于构建SparkContext对象
        //第二个参数表示每个批次的划分时间:Batch_Interval
        val ssc = new StreamingContext(conf,Seconds(3))
        //调整日志级别
    //    ssc.sparkContext.setLogLevel("WARN")
    
    
        /**
          * step2:实现数据的处理逻辑
          */
        //todo:1-读取数据
        /**
          * def createDirectStream[K, V](
          * ssc: StreamingContext,
          * locationStrategy: LocationStrategy,
          * consumerStrategy: ConsumerStrategy[K, V]
          * ): InputDStream[ConsumerRecord[K, V]]
          */
        //Spark用于计算Kafka集群位置的本地优先计算的模式
        val locationStrategy: LocationStrategy = LocationStrategies.PreferConsistent
        //配置连接Kafka的属性的
        /**
          * def Subscribe[K, V](
          * topics: Iterable[jl.String],
          * kafkaParams: collection.Map[String, Object])
          */
        //用于指定消费哪些Topic的数据
        val topics = Array("sparkStream01")
        //用于指定Kafka的连接属性
        val kafkaParams = Map[String, Object](
          "bootstrap.servers" -> "node1:9092,node2:9092",
          "key.deserializer" -> classOf[StringDeserializer],
          "value.deserializer" -> classOf[StringDeserializer],
          "group.id" -> "group_test_spark01",
          "auto.offset.reset" -> "latest",
          "enable.auto.commit" -> (false: java.lang.Boolean)
        )
        val consumerStrategy: ConsumerStrategy[String, String] = ConsumerStrategies.Subscribe(
          topics,//指定消费的Topic
          kafkaParams//指定连接属性
        )
        /**
          * 消费Kafka的数据
          */
        //每个ConsumerRecord对象,代表Kafka中的一条数据
        val kafkaStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
          ssc,//StreamingContext对象
          locationStrategy,//本地优先计算配置
          consumerStrategy//消费Kafka的配置
        )
        //todo:2-转换数据
        //定义offset对象
        var offsetRanges: Array[OffsetRange] = null
        //对每行的value的数据做单词统计
        val rsStream = kafkaStream
          //直接调用Transform函数,对DStream中的每个RDD来操作
          .transform(rdd => {
          //获取每个RDD中的Offset
          offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
          //对内部的每个RDD做转换
          rdd
            //获取value
            .map(record => record.value())
            .filter(line => line != null && line.trim.length > 0)
            .flatMap(line => line.trim.split("\\s+"))
            .map(word => (word,1))
            .reduceByKey(_+_)
        })
        //todo:3-保存结果
        rsStream.foreachRDD((rdd,time) => {
          //判断rdd是否有数据,没有数据就不输出
          //      if(!rdd.isEmpty()){
          val printTime = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").format(time.milliseconds)
          println("=====================================")
          println(printTime)
          println("=====================================")
          //对每个rdd进行打印
          //      rdd.foreachPartition(part => part.foreach(println))
          rdd.foreach(println)
          println("当前的offset")
          //打印这个批次的offset
          offsetRanges.foreach(offsetRange => {
            //得到每个批次的offset的信息
            val topic = offsetRange.topic
            val part = offsetRange.partition
            val startOffset = offsetRange.fromOffset
            val endOffset = offsetRange.untilOffset
            println(topic+"\t"+part+"\t"+startOffset+"\t"+endOffset)
          })
          println()
          //      }
    
        })
    
    
        /**
          * step3:释放资源,停止程序
          */
        //启动数据流计算的运行
        ssc.start()
        //让程序持续运行,除非手动终止
        ssc.awaitTermination()
        //最后一句代码,只有程序意外终止,才会执行
        ssc.stop(true,true)
    
      }
    }
    
    

七、实时热点应用案例

1、需求

  • 需求一:搜索日志数据存储Kafka,实时对日志数据进行ETL提取转换,解析省份城市,存储HDFS文件系统
  • 需求二:百度热搜排行榜Top10,累加统计所有用户搜索词次数,获取Top10搜索词及次数
    • 实时统计到目前为止每个搜索词出现的总次数,做排名,取前10
    • 有状态
  • 需求三:近期时间内热搜Top10,统计最近一段时间范围(比如,最近半个小时或最近2个小时)内用户搜索词次数,获取Top10搜索词及次数
    • 每3s计算前6s内的搜索词的次数

2、基础环境构建

  • 创建工程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2MHZ4HJs-1618212555396)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225154509710.png)]

  • 创建Topic

    ##创建Topic
    kafka-topics.sh --create --topic search-log-topic --partitions 3 --replication-factor 1 --zookeeper node1.itcast.cn:2181/kafka200
    
    ##列举
    kafka-topics.sh --list --zookeeper node1.itcast.cn:2181/kafka200
    
    ##创建生产者
    kafka-console-producer.sh --topic search-log-topic --broker-list node1.itcast.cn:9092
    
    ##创建消费者
    kafka-console-consumer.sh --topic search-log-topic --bootstrap-server node1.itcast.cn:9092 --from-beginning
    
  • 生成数据工具类

    • bigdata.itcast.cn.spark.stream.app.mock.MockSearchLogs

    • 不断的生成数据,将数据写入Kafka的Topic的中

      8c93c0d9a82a0b5f,139.213.51.72,20201225154742406,英国又发现一种变异新冠病毒
      aba039c0994fa040,36.57.141.25,20201225154742479,山东曲阜发生塌陷 学生有序避难
      826d0d05b43edb51,121.77.196.137,20201225154742550,一名北京赴浙人员新冠阳性
      92e54d435ec459f6,36.62.30.143,20201225154742618,高圆圆:我没有任何才艺可以展示
      bcfcdbf835bc5680,123.232.254.161,20201225154742669,永久冻土中发现57000年前小狼崽
      9b6f9ae90e1d86f5,171.15.28.96,20201225154742702,山东曲阜发生塌陷 学生有序避难
      a785f4cc367bf43d,36.60.54.44,20201225154742739,山东曲阜发生塌陷 学生有序避难
      8ba5ef08db285936,61.235.5.111,20201225154742767,阿里巴巴涉嫌垄断被立案调查
      992df47ab7976b7a,182.82.172.205,20201225154742835,网络招聘不得含有性别歧视性内容
      8a845a15da350103,182.87.183.254,20201225154742888,元旦起上海超市禁止提供塑料袋
      b435fe7dfc94aa34,139.215.69.203,20201225154742919,北京通报顺义1例无症状相关情况
      
  • StreamingContext工具类

    • bigdata.itcast.cn.spark.stream.app.StreamingContextUtils
    • 用于封装构建Streamingcontext的方法
    • 用于封装消费Kafka的代码

3、需求一实现

  • 分析

    • 无状态
      • 消费Kafka数据
      • 对Ip实现解析:省份,城市
      • 结果写入HDFS
  • 代码实现

    package bigdata.itcast.cn.spark.stream.app.etl
    
    import bigdata.itcast.cn.spark.stream.app.StreamingContextUtils
    import org.apache.kafka.clients.consumer.ConsumerRecord
    import org.apache.spark.rdd.RDD
    import org.apache.spark.streaming.dstream.DStream
    import org.lionsoul.ip2region.{DbConfig, DbSearcher}
    
    /**
      * @ClassName StreamingETLHdfs
      * @Description TODO   需求一:实现ETL,基于IP解析省份城市,将每个批次的结果写入HDFS
      * @Date 2020/12/25 15:49
      * @Create By     Frank
      */
    object StreamingETLHdfs {
      def main(args: Array[String]): Unit = {
        //step1:构建StreamingContext对象
        val ssc = StreamingContextUtils.getStreamingContext(this.getClass,5)
    
        //step2:数据的处理过程
        //todo:1-读取
        val kafkaStream: DStream[ConsumerRecord[String, String]] = StreamingContextUtils.consumerKafka(ssc,"search-log-topic")
        //todo:2-转换:解析IP得到省份城市,结果写入HDFS
        kafkaStream
            //对每个RDD实现转换
            .foreachRDD((rdd,time) => {
                //todo:过滤数据
                val etlRdd =   rdd
                    //获取对应的Value
                    .map(record => record.value())
                    //过滤空的数据或者不合法的数据
                    .filter(value => value != null && value.trim.split(",").length == 4)
                //todo:转换,解析Ip得到省份城市,返回
                val rsRdd: RDD[String] = etlRdd
                  .mapPartitions(
                    part => {
                      //构建Ip的解析器
                      val dbSearcher = new DbSearcher(new DbConfig(),"dataset/ip2region.db")
                      part.map(value => {
                        //获取IP
                        val ip = value.trim.split(",")(1)
                        //解析IP
                        val region = dbSearcher.btreeSearch(ip).getRegion
                        //得到省份城市
                        val Array(_,_,province,city,_) = region.split("\\|")
                        //返回结果
                        s"${value},${province},${city}"
                    })
                  })
                //保存
                if(!rsRdd.isEmpty())
                rsRdd.coalesce(1).saveAsTextFile("datas/output/streaming/etl-"+time.milliseconds)
            })
        //todo:3-输出
    
    
        //step3:启动和释放
        ssc.start()
        ssc.awaitTermination()
        ssc.stop(true,true)
      }
    }
    
    

4、需求二实现

  • 分析

    • 有状态的计算,当前批次的结果要加上上个批次的结果
  • 实现函数

    • updateStateByKey:按照Key进行聚合更新

      • 会对每个Key调用内部的参数函数来实现聚合
      • 处理的数据必须为二元组类型
    • mapWithState:用于实现数据更新

      • 区别:只更新发生变化的数据,没有发生变化的数据是不会重新写入Checkpoint重新输出
        • 结合内存中缓存机制+checkpiont来实现的
    • 两个函数的区别:如果将结果写入Redis

      • updateStateByKey:每次将所有数据重新更新写入Redis,哪怕数据没有发生改变
      • mapWithState:每次只将发生更新的结果写入Redis
    • 必须设置检查点:记录上一个批次的状态,用于这个批次的合并

      //设置检查点
          ssc.checkpoint("datas/output/checkpoint1")
      
  • 代码实现

    package bigdata.itcast.cn.spark.stream.app.state
    
    import bigdata.itcast.cn.spark.stream.app.StreamingContextUtils
    import org.apache.commons.lang.time.FastDateFormat
    import org.apache.kafka.clients.consumer.ConsumerRecord
    import org.apache.spark.rdd.RDD
    import org.apache.spark.streaming.{State, StateSpec}
    import org.apache.spark.streaming.dstream.DStream
    import org.lionsoul.ip2region.{DbConfig, DbSearcher}
    
    /**
      * @ClassName StreamingETLHdfs
      * @Description TODO   需求二:基于有状态的统计到目前位置每个搜索词出现的次数
      * @Date 2020/12/25 15:49
      * @Create By     Frank
      */
    object StreamingUpdateTopN {
      def main(args: Array[String]): Unit = {
        //step1:构建StreamingContext对象
        val ssc = StreamingContextUtils.getStreamingContext(this.getClass,5)
        //设置检查点
        ssc.checkpoint("datas/output/checkpoint1")
    
        //step2:数据的处理过程
        //todo:1-读取
        val kafkaStream: DStream[ConsumerRecord[String, String]] = StreamingContextUtils.consumerKafka(ssc,"search-log-topic")
        //todo:2-转换:解析IP得到省份城市,结果写入HDFS
        //todo:先过滤
        val etlStream: DStream[String] = kafkaStream
            .transform(rdd => {
              rdd.map(record => record.value())
                .filter(line => line != null && line.trim.split(",").length == 4)
            })
        //todo:对这个批次的数据做聚合
        val nowDStram: DStream[(String, Int)] = etlStream
            .transform(rdd => {
              rdd
                //将搜索词构建二元组
                .map(line => (line.trim.split(",")(3),1))
                //聚合得到每个搜索词搜索次数
                .reduceByKey(_+_)
            })
    
        //tood:加上前一个批次的结果
        /**
          * def updateStateByKey[S: ClassTag](
          * updateFunc: (Seq[V], Option[S]) => Option[S]
          * Seq[V]:这次计算中的结果的值
          * Option[S]:上次计算中的结果的值
          *
          * ): DStream[(K, S)]
          */
    //    val rsStream = nowDStram.updateStateByKey(
    //      //函数用于指定更新逻辑
    //      (value1:Seq[Int],value2:Option[Int]) => {
    //        //获取本次值:将这个词在这次出现的次数进行累加
    //        val nowData = value1.sum
    //        //获取上次值,因为上一次可以没有这个单词
    //        val lastData = value2.getOrElse(0)
    //        //得到这次的最终结果
    //        val current = nowData + lastData
    //        Some(current)
    //      }
    //    )
    
        // 状态更新函数,针对每条数据进行更新状态,如果数据未发生改变,则不更新
        val  spec: StateSpec[String, Int, Int, (String, Int)] = StateSpec.function(
          // (KeyType, Option[ValueType], State[StateType]) => MappedType
          (keyword: String, countOption: Option[Int], state: State[Int]) => {
            // a. 获取当前批次中搜索词搜索次数
            val currentState: Int = countOption.getOrElse(0)
            // b. 从以前状态中获取搜索词搜索次数
            val previousState = state.getOption().getOrElse(0)
            // c. 搜索词总的搜索次数
            val latestState = currentState + previousState
            // d. 更行状态
            state.update(latestState)
            // e. 返回最新搜索词个数
            (keyword, latestState)
          }
        )
    
        val rsStream = nowDStram.mapWithState(spec)
        //todo:3-输出
        rsStream.foreachRDD((rdd,time) => {
          //判断rdd是否有数据,没有数据就不输出
          //      if(!rdd.isEmpty()){
          val printTime = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").format(time.milliseconds)
          println("=====================================")
          println(printTime)
          println("=====================================")
          //对每个rdd进行打印
          //      rdd.foreachPartition(part => part.foreach(println))
          rdd
          //实现top10
            .sortBy(tuple => tuple._2,false)
            .take(10)
            .foreach(println)
          println()
          //      }
    
        })
    
        //step3:启动和释放
        ssc.start()
        ssc.awaitTermination()
        ssc.stop(true,true)
      }
    }
    
    
  • 结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFEgdcas-1618212555397)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225163006807.png)]

5、需求三实现

  • 分析

    • 基于窗口的计算统计
    • 窗口时间
    • 滑动时间
  • 代码实现

    • 方式一:先构建窗口,再基于窗口聚合

      //第一个参数是窗口时间,第二个参数是滑动时间
          val windowStream = etlStream.window(Seconds(WINDOW_INTERVAL),Seconds(SLIDE_INTERVAL))
      
          //todo:对这个批次的数据做聚合
          val rsStream: DStream[(String, Int)] = windowStream
              .transform(rdd => {
                rdd
                  //将搜索词构建二元组
                  .map(line => (line.trim.split(",")(3),1))
                  //聚合得到每个搜索词搜索次数
                  .reduceByKey(_+_)
              })
      
    • 方式二:聚合的时候根据窗口进行聚合

          //方式二:聚合的时候指定窗口再聚合
          val rsStream = mapStream.reduceByKeyAndWindow(
            //传递reduce 的函数
            (tmp:Int,item:Int) => tmp+item,
            Seconds(WINDOW_INTERVAL),
            Seconds(SLIDE_INTERVAL)
          )
      
      • 优化的写法:可以基于上一个批次重叠的部分进行计算,得到这个批次的一部分结果

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pMxMk2js-1618212555397)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225171401890.png)]

            val rsStream = mapStream.reduceByKeyAndWindow(
              //传递reduce 的函数
              (tmp:Int,item:Int) => tmp+item,
              //反聚合规则函数
              (tmp:Int,item:Int) => tmp - item,
              Seconds(WINDOW_INTERVAL),//窗口时间
              Seconds(SLIDE_INTERVAL),//滑动时间
              filterFunc = (tuple:(String,Int)) => tuple._2 != 0
            )
        

八、Kafka Offset管理

1、问题与需求

  • 问题1:程序如果运行出现故障 ,重启怎么恢复之前的状态
  • 问题2:怎么保证消费Kafka的数据的时候,实现一次性语义
    • 数据不丢失
      • step1:从Kafka中获取最新的数据,Kafka记录了这个位置
      • step2:实现处理,程序故障,需要重启。这部分数据处理失败了,程序这次不知道上一次处理的位置
        • Kafka会根据当初消费的偏移量来返回没有消费的数据
    • 数据不重复
      • step1:从Kafka中获取最新的数据,Kafka没有记录
      • step2:实现处理,处理成功了,程序故障,需要重启。这部分数据处理失败了,程序这次不知道上一次处理的位置
        • Kafka会根据当初消费的偏移量来返回没有消费的数据
    • 原因:自己不知道上次处理的位置,所以只能根据Kafka的记录来消费
    • 解决问题
      • 自己记录每次消费的位置,每次向Kafka请求,根据上次自己记录的位置来请求

2、checkpoint方式

  • 问题:有状态的统计,如果程序故障,重新启动程序,没有累加程序之前的状态

  • 原因:每次都重新构建driver的StreamingContext,所有配置都是全新

    • 不知道上一次的checkpoint目录在哪
  • 设计

    • 将程序的所有信息,都记录在文件系统中,如果程序遇到故障,从checkpoint记录的数据中恢复原来的程序

    • 存储的内容

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oWK23NmL-1618212555397)(Day44_分布式计算平台Spark:Streaming.assets/image-20201225173124793.png)]

      • 程序的元数据信息:Metadata checkpointing
        • Configuration - The configuration that was used to create the streaming application.
          • 当前的SparkStreaming程序的所有的配置信息
        • DStream operations - The set of DStream operations that define the streaming application.
          • 所有DStream的转换关系
        • Incomplete batches - Batches whose jobs are queued but have not completed yet.
          • 未完成的批次的信息,对于Kafka而言,就是上次消费成功的偏移量信息
      • 数据的信息:Data checkpointing
        • 用于记录上个批次的结果
  • 代码实现

    package bigdata.itcast.cn.spark.stream.manageroffset
    
    import java.util.Date
    
    import org.apache.commons.lang3.time.FastDateFormat
    import org.apache.kafka.clients.consumer.ConsumerRecord
    import org.apache.kafka.common.serialization.StringDeserializer
    import org.apache.spark.SparkConf
    import org.apache.spark.rdd.RDD
    import org.apache.spark.streaming.dstream.DStream
    import org.apache.spark.streaming.kafka010._
    import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext}
    
    /**
      * SparkStreaming流式应用模板Template,将从数据源读取数据、实时处理及结果输出封装到方法中。
      */
    object StreamingCheckpoint {
    
      /**
        * 抽象一个函数:专门从数据源读取流式数据,经过状态操作分析数据,最终将数据输出
        * @param ssc 流式上下文StreamingContext实例对象
        */
      def processData(ssc: StreamingContext): Unit ={
        // TODO: 1. 从Kafka Topic实时消费数据
        val kafkaDStream: DStream[ConsumerRecord[String, String]] = {
          // i.位置策略
          val locationStrategy: LocationStrategy = LocationStrategies.PreferConsistent
          // ii.读取哪些Topic数据
          val topics = Array("search-log-topic")
          // iii.消费Kafka 数据配置参数
          val kafkaParams = Map[String, Object](
            "bootstrap.servers" -> "node1.itcast.cn:9092",
            "key.deserializer" -> classOf[StringDeserializer],
            "value.deserializer" -> classOf[StringDeserializer],
            "group.id" -> "group_id_streaming_0002",
            "auto.offset.reset" -> "latest",
            "enable.auto.commit" -> (false: java.lang.Boolean)
          )
          // iv.消费数据策略
          val consumerStrategy: ConsumerStrategy[String, String] = ConsumerStrategies.Subscribe(
            topics, kafkaParams
          )
          // v.采用消费者新API获取数据
          KafkaUtils.createDirectStream(ssc, locationStrategy, consumerStrategy)
        }
    
        // TODO: 2. 词频统计,实时累加统计
        // 2.1 对数据进行ETL和聚合操作
        val reduceDStream: DStream[(String, Int)] = kafkaDStream.transform{ rdd =>
          val reduceRDD: RDD[(String, Int)] = rdd
            // 过滤不合格的数据
            .filter{ record =>
            val message: String = record.value()
            null != message && message.trim.split(",").length == 4
          }
            // 提取搜索词,转换数据为二元组,表示每个搜索词出现一次
            .map{record =>
            val keyword: String = record.value().trim.split(",").last
            keyword -> 1
          }
            // 按照单词分组,聚合统计
            .reduceByKey((tmp, item) => tmp + item)  // TODO: 先聚合,再更新,优化
          // 返回
          reduceRDD
        }
        // 2.2 使用mapWithState函数状态更新, 针对每条数据进行更新状态
        val  spec: StateSpec[String, Int, Int, (String, Int)] = StateSpec.function(
          // (KeyType, Option[ValueType], State[StateType]) => MappedType
          (keyword: String, countOption: Option[Int], state: State[Int]) => {
            // a. 获取当前批次中搜索词搜索次数
            val currentState: Int = countOption.getOrElse(0)
            // b. 从以前状态中获取搜索词搜索次数
            val previousState = state.getOption().getOrElse(0)
            // c. 搜索词总的搜索次数
            val latestState = currentState + previousState
            // d. 更行状态
            state.update(latestState)
            // e. 返回最新省份销售订单额
            (keyword, latestState)
          }
        )
        // 调用mapWithState函数进行实时累加状态统计
        val stateDStream: DStream[(String, Int)] =  reduceDStream.mapWithState(spec)
    
        // TODO: 3. 统计结果打印至控制台
        stateDStream.foreachRDD{(rdd, time) =>
          val batchTime: String = FastDateFormat.getInstance("yyyy/MM/dd HH:mm:ss")
            .format(new Date(time.milliseconds))
          println("-------------------------------------------")
          println(s"BatchTime: $batchTime")
          println("-------------------------------------------")
          if(!rdd.isEmpty()){
            rdd.coalesce(1).foreachPartition{_.foreach(println)}
          }
        }
      }
    
      // 应用程序入口
      def main(args: Array[String]): Unit = {
    
        //定义检查点目录
        val CHK_DIR = "datas/output/checkpoint4"
    
        // TODO: 构建流式上下文实例对象StreamingContext
          // a. 创建SparkConf对象,设置应用配置信息
          val ssc: StreamingContext = StreamingContext.getActiveOrCreate(
            //设置检查点目录,用于恢复StreamingContext,第一次运行时没有
            CHK_DIR,
            //如果是第一次运行,就要创建一个
            () => {
              val sparkConf = new SparkConf()
                .setAppName(this.getClass.getSimpleName.stripSuffix("$"))
                .setMaster("local[3]")
                // TODO: 设置消费最大数量
                .set("spark.streaming.kafka.maxRatePerPartition", "10000")
              // b. 传递SparkConf和BatchInterval创建流式上下对象
              val context = new StreamingContext(sparkConf, Seconds(5))
              //设置检查点
              context.checkpoint(CHK_DIR)
              //处理数据
              // TODO: 从数据源端消费数据,实时处理分析及最后输出
              processData(context)
              // c. 返回实例对象
              context
            }
          )
    
          // TODO: 启动流式应用,等待终止(人为或程序异常)
          ssc.start()
          ssc.awaitTermination() // 流式应用启动以后,一直等待终止,否则一直运行
          // 无论是否异常最终关闭流式应用(优雅的关闭)
          ssc.stop(stopSparkContext = true, stopGracefully = true)
    
      }
    
    }
    
  • 问题:只要使用Checkpoint来保证程序的可用性,你的代码就不能对DStream进行修改

    DStream operations - The set of DStream operations that define the streaming application.
    
    - 所有DStream的转换关系
    
  • 解决

    • 目标:程序如果重启,需要得到之前的结果的状态已经消费到的Kafka的offset
    • 只要能得到这两个东西,就可以不用checkpoint来实现程序故障恢复
  • 实现

    • 每个批次的结果自己手动的存储到HBASE、Redis
      • 每次运行从存储中获取上次对应的结果再进行聚合
    • 每次批次的offset自己手动的存储到系统中:MySQL、Zookeeper
      • 每次运行从存储中读取offset向Kafka请求,保证一次性语义的请求

3、MySQL存储偏移量

  • 设计

  • 创建Topic

    ##创建Topic
    kafka-topics.sh --create --topic search-log-topic2 --partitions 3 --replication-factor 1 --zookeeper node1.itcast.cn:2181/kafka200
    
    ##列举
    kafka-topics.sh --list --zookeeper node1.itcast.cn:2181/kafka200
    
    ##创建生产者
    kafka-console-producer.sh --topic search-log-topic2 --broker-list node1.itcast.cn:9092
    
    ##创建消费者
    kafka-console-consumer.sh --topic search-log-topic2 --bootstrap-server node1.itcast.cn:9092 --from-beginning
    
  • 创建MySQL表并初始化数据

    -- 1. 创建数据库的语句
    CREATE DATABASE IF NOT EXISTS db_spark DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    USE db_spark ;
    
    -- 2. 创建表的语句
    CREATE TABLE `tb_offset` (
      `topic` varchar(255) NOT NULL,
      `partition` int NOT NULL,
      `groupid` varchar(255) NOT NULL,
      `offset` bigint NOT NULL,
      PRIMARY KEY (`topic`,`partition`,`groupid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ;
    
    -- 3. 插入数据语句replace
    --语法
    replace into db_spark.tb_offset (`topic`, `partition`, `groupid`, `offset`) values(?, ?, ?, ?)
    --初始
    replace into tb_offset values('search-log-topic2', 0, 'group_id_10001', 0);
    replace into tb_offset values('search-log-topic2', 1, 'group_id_10001', 0);
    replace into tb_offset values('search-log-topic2', 2, 'group_id_10001', 0);
    
    -- 4. 查询数据语句select
    --语法
    select * from db_spark.tb_offset where topic in (?) and groupid = ? ;
    --测试
    select * from tb_offset where topic in ('search-log-topic2') AND groupid = 'group_id_10001' ;
    
  • 读写MySLQL工具类

  • 代码实现

附录一:Streaming Maven依赖

 <!-- 指定仓库位置,依次为aliyun、cloudera和jboss仓库 -->
    <repositories>
        <repository>
            <id>aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
        <repository>
            <id>cloudera</id>
            <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
        </repository>
        <repository>
            <id>jboss</id>
            <url>http://repository.jboss.com/nexus/content/groups/public</url>
        </repository>
    </repositories>

    <properties>
        <scala.version>2.11.12</scala.version>
        <scala.binary.version>2.11</scala.binary.version>
        <spark.version>2.4.5</spark.version>
        <hadoop.version>2.6.0-cdh5.16.2</hadoop.version>
        <hbase.version>1.2.0-cdh5.16.2</hbase.version>
        <kafka.version>2.0.0</kafka.version>
        <mysql.version>8.0.19</mysql.version>
    </properties>

    <dependencies>

        <!-- 依赖Scala语言 -->
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>
        <!-- Spark Core 依赖 -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_${scala.binary.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <!-- Spark SQL 依赖 -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_${scala.binary.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <!-- Spark Streaming 依赖 -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_${scala.binary.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <!-- Spark Streaming 与Kafka 0.10.0 集成依赖-->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-10_${scala.binary.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <!-- Hadoop Client 依赖 -->
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <!-- HBase Client 依赖 -->
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-server</artifactId>
            <version>${hbase.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-hadoop2-compat</artifactId>
            <version>${hbase.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>${hbase.version}</version>
        </dependency>
        <!-- Kafka Client 依赖 -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>${kafka.version}</version>
        </dependency>
        <!-- 根据ip转换为省市区 -->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
            <version>1.7.2</version>
        </dependency>
        <!-- MySQL Client 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
    </dependencies>

    <build>
        <outputDirectory>target/classes</outputDirectory>
        <testOutputDirectory>target/test-classes</testOutputDirectory>
        <resources>
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
            </resource>
        </resources>
        <!-- Maven 编译的插件 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

附录二、Kafka集群操作

  • 第一步:先启动三台机器的Zookeeper

    zookeeper-daemons.sh  start
    zookeeper-daemons.sh  status
    
  • 第二步:启动kafka server

    kafka-daemons.sh start
    
  • 创建一个topic

    kafka-topics.sh --create --topic sparkStream01 --partitions 3 --replication-factor 1 --zookeeper node1.itcast.cn:2181/kafka200
    
  • 查看topic信息

    #列举所有topic
    kafka-topics.sh --list --zookeeper node1.itcast.cn:2181/kafka200
    #查看topic详细信息
    kafka-topics.sh --describe --topic sparkStream01 --zookeeper node1:2181/kafka200
    
  • 创建一个生产者

    kafka-console-producer.sh --topic sparkStream01 --broker-list node1.itcast.cn:9092
    
  • 创建一个消费者

    kafka-console-consumer.sh --topic sparkStream01 --bootstrap-server node1.itcast.cn:9092 --from-beginning
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

章鱼哥TuNan&Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值