Spark:StructStreaming

分布式计算平台Spark:StructStreaming

一、回顾

  1. SparkStreaming基本原理

    • 本质还是SparkCore:基于RDD的离线批处理
    • 原理:划分微小时间单位的批处理
      • ReceiverTask:将源源不断的数据流划分Block:默认200ms
        • 将每个Block的数据缓存在Executor的内存中
        • 将位置反馈给Driver
      • Driver等到Batch时间:1s
        • 区别:Core按照触发函数来触发job的,按照时间来触发job的
  2. DStream:离散的数据流

    • DStream = Seq[RDD]
    • 基于时间有序的RDD的集合,符合RDD的所以特性
    • 函数
      • 转换函数:tranformRDD
        • 对每个RDD进行转换操作,有返回值
      • 输出函数:foreachRDD
        • 对每个RDD进行操作,没有返回值
      • 能使用RDD的处理就尽量使用RDD的处理
  3. 流式计算的业务模式

    • 无状态模式:每个批次的结果就是这个批次中最终的结果,与其他的批次无关
      • 一般用于实现ETL
    • 有状态模式:每个批次的结果不是这个批次的最终结果,这个批次的最终结果与其他的批次有关,一般需要聚合
      • 一般用于累计统计分析
      • updateStateByKey(更新逻辑):根据Key对Value进行函数更新Value的值
        • 如果Key的value没有发生,也会被更新,每次都将所有结果写入checkpoint
      • mapWithState(对象:key,当前的value,之前的value):根据Key对Value进行函数更新Value的值
        • 只对Key的Value发生变化的数据进行更新,基于checkpoint+缓存来实现的、
    • 窗口计算:按照一定的时间周期计算一定时间范围的数据
      • 一般用于热搜实时统计
      • 区别:不再是基于每个批次的计算,基于窗口的计算
      • Batch:批次时间 】
      • Window:计算的范围
  4. 与Kafka集成

    • 0.8x

      • receiver:启动receiver不断接受kafka发送过来的数据
        • RDD的分区数 = Block Interval的个数
        • 导致数据丢失的问题,通过WAL来解决数据丢失问题,导致性能较慢
        • 这种方式我们是不用的
      • direct:由SparkStreaming主动到Kafka中拉取数据
        • RDD的分区数 = Kafka的Topic的分区数
        • 拉取数据的流程
          • step1:SparkStreaming会到Kafka请求获取 每个分区最新的偏移量
          • step2:根据上个批次最后的偏移量和这个批次的最新偏移量构建拉取范围
          • step3:根据范围向Kafka请求消费每个分区的数据
    • 0.10.x

      • 类似于Direct方式来实现的
    • 问题:假设每3s统计一次到目前为止的订单总金额

      • 程序如果故障,重新启动程序

        • 问题1:程序第一次运行的状态会丢失

          12:00:00 ~ 12:00:03		1000万
          12:00:03 ~ 12:00:06		1000万	=》  2000万
          程序发生故障
          |
          重启程序
          12:00:06 ~ 12:00:09		500万	=》	500万
          
        • 问题2:消费Kafka的数据,重新启动以后,就不知道上一次消费的位置,Kafka会根据自己记录位置来继续提供消费

          • Kafka记录的位置与我们实际的位置是由差异的,会导致数据丢失或者数据重复的问题
    • 解决

      • 问题1

        • 方案一:checkpoint来实现
          • 在checkpoint中存储整个程序的所有信息
            • meta:配置信息、DStream的转换、上次处理的偏移量位置
            • data:数据之前的状态
          • 问题:如果对代码做了DStream级别的修改,程序是无法恢复
          • 要求:不能对代码进行DStream级别的修改
        • 方案二:自己存储上一次的状态
      • 问题2:手动管理偏移量

  5. 问题

    • SparkCore 中的累加器会发生跟java中的线程安全问题一样的情况吗
    • SparkStreaming可以看作是一个分布式队列,队列中的过期数据应该需要清理,那么垃圾回收机制是怎么样的 在有状态模式下,当前状态下产生异常,如何进行异常处理,程序继续运行还是异常退出 SparkStreaming中有流水线机制吗 例: 数据处理分为读取,ETL,分析,存储四个阶段,每个阶段需要10ms 非流水线:读取->ETL->分析->存储->读取->ETL->分析->存储->读取->ETL->分析->存储 每40ms吞吐一批数据,延迟为40ms 流水线: 时间增加方向----------> 读取->ETL->分析->存储 读取->ETL->分析->存储 读取->ETL->分析->存储 读取->ETL->分析->存储 读取->ETL->分析->存储 读取->ETL->分析->存储 完全进入流水线的时候,理论上每10ms就能吞吐一批数据,有四倍的吞吐量,同时延迟只有10ms 批处理是顺序执行,会有很大的延迟,使得程序运行时很多时候都是等待数据的状态 猜测:flink=微小的批处理+流水线?
    • 不太清楚BatchInterval与窗口计算中的Slide Interval的区别在哪

二、目标

  1. StructStreaming功能与应用场景
    • 介绍特点
    • 功能与应用场景
    • 基本原理
  2. 使用
    • 代码开发规则
    • 数据源
    • 特性:流式数据去重、持续处理【真实时计算】、基于事件时间的处理

三、StructStreaming的介绍

1、结构化流的设计

  • 诞生:源自于DataFlow
    • 新的流式计算特性:基于事件时间的处理等等
  • 理解:使用SparkSQL来开发流式计算
    • SparkCore:基于代码开发离线批处理
    • SparkSQL:基于结构化SQL开发的离线批处理
    • SparkStreaming:基于SparkCore的流式计算
      • 问题:本身基于计算时间的处理、微小时间的批处理,延迟性比较高、开发接口还是传统的编程
    • StructStreaming:基于SparkSQL的流式计算
    • 用一套API实现所有模块的编程:SQL
  • 基本原理
    • SparkSQL:数据结构:DataFrame/DataFrame:分布式的表
    • SparkStreaming:数据结构:DStream:无边界的数据流
    • |
    • StructStreaming:将无边界的数据流读取到一个DS/DF:无边界的分布式表
      • 所有数据源源不断的追加到这个表中
      • 最后处理的结果也是放入DS或者DF中:无边界的结果表
      • 每次处理的结果都不断追加到这个表中
      • 通过查询从结果表中查询我们自己想要的数据

2、官网介绍

Structured Streaming is a scalable and fault-tolerant stream processing engine built on the Spark SQL engine. You can express your streaming computation the same way you would express a batch computation on static data. The Spark SQL engine will take care of running it incrementally and continuously and updating the final result as streaming data continues to arrive. You can use the Dataset/DataFrame API in Scala, Java, Python or R to express streaming aggregations, event-time windows, stream-to-batch joins, etc. The computation is executed on the same optimized Spark SQL engine. Finally, the system ensures end-to-end exactly-once fault-tolerance guarantees through checkpointing and Write-Ahead Logs. In short, Structured Streaming provides fast, scalable, fault-tolerant, end-to-end exactly-once stream processing without the user having to reason about streaming.
  • 结构化流基于SparkSQL引擎之上

  • 对于流式计算或者离线批处理可以使用同一套API来实现

  • 正常代码

    val inputData:DataFrame = spark.readStream
    
    • 将流式数据放入一个无边界的表中:不断追加数据

      image-20201226104532753
    val rsData:DataFrame = inputData.filter.groupBy.count……
    
    • 不断对新到达的数据实现处理和转换

      image-20201226104654924

    • 最终将结果追加写入一个无边界的表

      image-20201226104725392

3、官网示例

  • 启动HDFS

    start-dfs.sh
    
  • 第一台机器运行nc

    nc -lk 9999
    
  • 第二台机器运行

    /export/server/spark/bin/run-example \
    --master local[2] \
    --conf spark.sql.shuffle.partitions=2 \
    org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount \
    node1.itcast.cn 9999
    
  • 发现特性:

    • StructStreaming中默认的间隔批次时间是ms级别,可以调整间隔时间的
    • 如果没有数据是不输出的
    • 有状态的计算

4、自定义开发

package bigdata.itcast.cn.spark.struct.wordcount

import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}

/**
  * @ClassName StructStreamingMode
  * @Description TODO 结构化流实现词频统计
  * @Date 2020/12/26 10:55
  * @Create By     Frank
  */
object StructStreamingWordCount {

  def main(args: Array[String]): Unit = {
    /**
      * step1:先创建SparkSession
      */
    val spark = SparkSession.builder()
      .appName(this.getClass.getSimpleName.stripSuffix("$"))
      .master("local[3]")
      .config("spark.sql.shuffle.partitions","2")
      .getOrCreate()
    //导入包
    import spark.implicits._
    import org.apache.spark.sql.functions._

    /**
      * step2:处理逻辑
      */
    //todo:1-读取数据
    val inputData: DataFrame = spark.readStream
      .format("socket")
      .option("host","node1")
      .option("port","9999")
      .load()

    //todo:2-处理
    val rsData = inputData
      //转换DataSet
      .as[String]
      .filter(line => null != line && line.trim.length > 0)
      .flatMap(line => line.trim.split("\\s+"))
      .groupBy($"value")
      .count()

    //todo:3-保存
    val query = rsData
      .writeStream
      .outputMode(OutputMode.Complete())
      .format("console")
      .option("numRows","10")//指定显示结果的条数
      .option("truncate","false")//如果某列的值过长,是否省略显示
      //启动
      .start()

    /**
      * step3:长久运行
      */
    query.awaitTermination()
    query.stop()


  }
}

四、StructStreaming输入

1、数据源

  • http://spark.apache.org/docs/2.4.5/structured-streaming-programming-guide.html#input-sources
image-20201224161339358
  • 工作中主要使用的还是Kafka Source

2、文件数据源

  • 所有的文件类型必须指定Schema

  • 监控目录:如果目录产生新的文件,就立即处理

  • 举例:读取文件

    package bigdata.itcast.cn.spark.struct.datasource
    
    import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
    import org.apache.spark.sql.types.{IntegerType, StringType, StructType}
    import org.apache.spark.sql.{DataFrame, SparkSession}
    
    /**
      * @ClassName StructStreamingMode
      * @Description TODO 读取文件:统计年龄在25岁以下的人,每种兴趣爱好对应的人数
      * @Date 2020/12/26 10:55
      * @Create By     Frank
      */
    object StructStreamingReadFile {
    
      def main(args: Array[String]): Unit = {
        /**
          * step1:先创建SparkSession
          */
        val spark = SparkSession.builder()
          .appName(this.getClass.getSimpleName.stripSuffix("$"))
          .master("local[3]")
          .config("spark.sql.shuffle.partitions","2")
          .getOrCreate()
        //导入包
        import spark.implicits._
    
        /**
          * step2:处理逻辑
          */
        //todo:1-读取数据:不断追加新的数据到DF中
        //构建Schema
        val schema = new StructType()
          .add("name",StringType,true)
          .add("age",IntegerType,true)
          .add("hobby",StringType,true)
    
    
        val inputData: DataFrame = spark.readStream
          //更改分隔符
          .option("sep",";")
          //指定目录中文件的Schema
          .schema(schema)
          //指定监控的目录
          .csv("datastruct/filesource")
    
        //todo:2-处理:不断的对每个批次的数据做处理聚合合并
        val rsData: DataFrame = inputData
          //过滤
          .filter($"age" < 25)
          .groupBy($"hobby")
          .count()
    
    
        //todo:3-保存
        val query: StreamingQuery = rsData
          .writeStream
          .outputMode(OutputMode.Complete())
          .format("console")
          .option("numRows","10")//指定显示结果的条数
          .option("truncate","false")//如果某列的值过长,是否省略显示
          //启动
          .start()
    
        /**
          * step3:长久运行
          */
        query.awaitTermination()
        query.stop()
    
    
      }
    }
    
    

3、Rate数据源【了解】

  • StructStream用于测试代码的数据

  • 会帮你模拟产生数据:时间和数值【从0开始编号】

    bigdata.itcast.cn.spark.struct.datasource.StructuredRateSource
    

五、Query选项

1、输出模式

  • http://spark.apache.org/docs/2.4.5/structured-streaming-programming-guide.html#output-modes

  • 三种

    • Append:用于追加输出
      • 一般用于没有聚合的场景
      • 如果要在聚合中使用,只能在基于事件的窗口中基于水印的聚合
    • Complete:完整的输出,用于将所有数据结果进行输出
      • 聚合的场景下全部结果的输出
    • Update:更新输出,只输出发生更新的数据
      • 一般用于各种场景下
  • 不同的操作需要使用不同的模式

    image-20201226114429826

2、查询器名称

  • 一般用于在监控中辨别不同的查询器

    val query: StreamingQuery = rsData
          .writeStream
          //保存模式
          .outputMode(OutputMode.Complete())
          .format("console")
          //指定查询器的名称
          .queryName("firstQuery")
          .option("numRows","10")//指定显示结果的条数
          .option("truncate","false")//如果某列的值过长,是否省略显示
          //启动
          .start()
    
    

3、触发间隔

  • 支持三种流式计算模式

    • 第一种:微小时间批处理,默认的模式
    • 第二种:只处理一次
    • 第三种:实时不断的数据流处理,数据产生一条,就处理一条
  • 实现

    • 每个批次间隔处理一次

      //设置处理的模式
            .trigger(Trigger.ProcessingTime("3 seconds"))
      

      image-20201226115344838

    • 只处理一次:一般不用

      //设置处理的模式
            .trigger(Trigger.Once())
      

      image-20201226115704405

4、检查点

  • http://spark.apache.org/docs/2.4.5/structured-streaming-programming-guide.html#recovering-from-failures-with-checkpointing

    In case of a failure or intentional shutdown, you can recover the previous progress and state of a previous query, and continue where it left off. This is done using checkpointing and write-ahead logs. You can configure a query with a checkpoint location, and the query will save all the progress information (i.e. range of offsets processed in each trigger) and the running aggregates (e.g. word counts in the quick example) to the checkpoint location. This checkpoint location has to be a path in an HDFS compatible file system, and can be set as an option in the DataStreamWriter when starting a query.
    
  • 配置方式一

        val query: StreamingQuery = rsData
          .writeStream
          //保存模式
          .outputMode(OutputMode.Complete())
          .format("console")
          //指定查询器的名称
          .queryName("firstQuery")
          //设置处理的模式
    //      .trigger(Trigger.Once())
          //配置检查点,存储程序的元数据信息,用于程序的恢复
          .option("checkpointLocation", "datastruct/output/chk1")
          .option("numRows","10")//指定显示结果的条数
          .option("truncate","false")//如果某列的值过长,是否省略显示
          //启动
          .start()
    
  • 配置方式二

        /**
          * step1:先创建SparkSession
          */
        val spark = SparkSession.builder()
          .appName(this.getClass.getSimpleName.stripSuffix("$"))
          .master("local[3]")
          .config("spark.sql.shuffle.partitions","2")
          //设置检查点方式二
          .config("spark.sql.streaming.checkpointLocation","datastruct/output/chk2")
          .getOrCreate()
    

5、输出终端

image-20201226120443600

http://spark.apache.org/docs/2.4.5/structured-streaming-programming-guide.html#output-sinks

  • Foreach Sink

    • 用于自定义保存的实现,一般用于保存至MySQL、Redis等数据库中

    • MySQL中建表

      drop table if exists `db_spark`.`tb_word_count`;
      CREATE TABLE `db_spark`.`tb_word_count` (
        `id` int NOT NULL AUTO_INCREMENT,
        `word` varchar(255) NOT NULL,
        `count` int NOT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `word` (`word`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
      
      REPLACE INTO `tb_word_count` (`id`, `word`, `count`) VALUES (NULL, ?, ?)
      
    • 自定义输出器代码实现

      package bigdata.itcast.cn.spark.struct.datasink
      
      import java.sql.{Connection, DriverManager, PreparedStatement}
      
      import org.apache.spark.sql.{ForeachWriter, Row}
      
      /**
        * @ClassName MySQLForeachSink
        * @Description TODO 用于自定义将数据写入MySQL
        * @Date 2020/12/26 12:07
        * @Create By     Frank
        */
      class MySQLForeachSink extends ForeachWriter[Row]{
      
        var conn:Connection = null
        var pstm:PreparedStatement = null
        val sql = "REPLACE INTO `tb_word_count` (`id`, `word`, `count`) VALUES (NULL, ?, ?)"
      
        //用于初始化资源
        override def open(partitionId: Long, epochId: Long): Boolean = {
            Class.forName("com.mysql.cj.jdbc.Driver")
          conn = DriverManager.getConnection(
            "jdbc:mysql://node1.itcast.cn:3306/db_spark?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true",
            "root",
          "123456"
          )
          pstm = conn.prepareStatement(sql)
          //构建完成
          true
        }
      
        //实现处理
        override def process(value: Row): Unit = {
          pstm.setString(1,value.getAs[String]("value"))
          pstm.setInt(2,value.getAs[Long]("count").toInt)
          pstm.execute()
        }
      
        //释放资源
        override def close(errorOrNull: Throwable): Unit = {
          pstm.close()
          conn.close()
        }
      }
      
      
    • 代码中调用

          //todo:3-保存
          val query: StreamingQuery = rsData
            .writeStream
            .outputMode(OutputMode.Complete())
            //自定义输出接口
            .foreach(new MySQLForeachSink)
            //启动
            .start()
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lM5WhBGO-1618234637266)(https://gitee.com/TuNan86/mapdeport2/raw/master/img/20210412213606.png)]

  • Foreach Batch Sink

    • 基于SparkSQL的批处理来实现输出

      df.write.jdbc()
      
    • 实现

          val query: StreamingQuery = rsData
            .writeStream
            .outputMode(OutputMode.Complete())
            //调用批处理的SparkSQL输出接口来实现输出
            //第一个参数是每个批次对应的DF,第二个是批次的ID
            .foreachBatch((df:DataFrame,batchId:Long) => {
                //打印批次id
                println("bacthId:"+batchId)
                //调用批处理输出的接口来保存到MySQL
                df.coalesce(1)
                  .write
                  .mode(SaveMode.Overwrite)
                  .format("jdbc")
                  .option("driver", "com.mysql.cj.jdbc.Driver")
                  .option("url", "jdbc:mysql://node1.itcast.cn:3306/?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true")
                  .option("user", "root")
                  .option("password", "123456")
                  .option("dbtable", "db_spark.tb_word_count2")
                  .save()
          })
            //启动
            .start()
      
      

6、容错机制

  • StructStreaming如何保证程序的可靠性,保证数据一次性语义
  • source:读取数据
    • 通过偏移量来实现精确的一次性消费
  • tranform:通过checkpoint和wal机制来实现
    • 记录整个程序处理过程中的所有元数据,来保证程序出现任何故障,重启程序,程序依旧能保证可靠性
  • sink:保证所有sink的幂等性

六、集成Kafka

1、集成方式

  • 读Kafka数据:做ETL实现、实现数据分析
  • 写Kafka数据:将ETL以后的结果写入Kafka、利用Kafka构建实时数据仓库
  • 注意:Structured Streaming + Kafka Integration Guide (Kafka broker version 0.10.0 or higher)

2、消费Kafka

  • 规则

    • 指定Kafka类型和属性配置

      .format("kafka")
      .option(Kafka地址)
      .option(topic)
      .load
      
    • 官方示例

      // 消费单个Topic的数据
      val df = spark
        .readStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
        .option("subscribe", "topic1")
        .load()
      df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
        .as[(String, String)]
      
      // // 消费多个Topic的数据
      val df = spark
        .readStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
        .option("subscribe", "topic1,topic2")
        .load()
      df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
        .as[(String, String)]
      
      //通过正则匹配消费Topic的名称
      val df = spark
        .readStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
        .option("subscribePattern", "topic.*")
        .load()
      df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
        .as[(String, String)]
      
    • 从Kafka中消费到的每条数据包含哪些列

      image-20201226145753730

      • key
      • value
      • topic:这条数据属于哪个topic
      • partition:这条数据属于哪个分区
      • offset:这条数据在这个分区的offset
  • 需求:从Kafka消费数据,实现词频统计

    • 创建Topic

      # 查看Topic信息
      kafka-topics.sh --list --zookeeper node1.itcast.cn:2181/kafka200
      # 创建topic
      kafka-topics.sh --create --zookeeper node1.itcast.cn:2181/kafka200 --replication-factor 1 --partitions 3 --topic wordsTopic
      # 模拟生产者
      kafka-console-producer.sh --broker-list node1.itcast.cn:9092 --topic wordsTopic
      
    • 代码实现

      package bigdata.itcast.cn.spark.struct.kafka.source
      
      import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
      import org.apache.spark.sql.{DataFrame, SparkSession}
      
      /**
        * @ClassName StructStreamingMode
        * @Description TODO 结构化流实现词频统计,消费Kafka数据
        * @Date 2020/12/26 10:55
        * @Create By     Frank
        */
      object StructStreamingKafkaSource {
      
        def main(args: Array[String]): Unit = {
          /**
            * step1:先创建SparkSession
            */
          val spark = SparkSession.builder()
            .appName(this.getClass.getSimpleName.stripSuffix("$"))
            .master("local[3]")
            .config("spark.sql.shuffle.partitions","2")
            .getOrCreate()
          //导入包
          import spark.implicits._
      
          /**
            * step2:处理逻辑
            */
          //todo:1-读取数据:不断追加新的数据到DF中
          val inputData: DataFrame = spark.readStream
            .format("kafka")
            .option("kafka.bootstrap.servers","node1:9092")
            .option("subscribe","wordsTopic")
            .load()
      
          //todo:2-处理:不断的对每个批次的数据做处理聚合合并
          val rsData: DataFrame = inputData
            //从Kafka的每条数据中获取value
            .selectExpr("CAST(value AS STRING)")
            //转换DataSet
            .as[String]
            .filter(line => null != line && line.trim.length > 0)
            .flatMap(line => line.trim.split("\\s+"))
            .groupBy($"value")
            .count()
      
          //todo:3-保存
          val query: StreamingQuery = rsData
            .writeStream
            .outputMode(OutputMode.Complete())
            .format("console")
            .option("numRows","10")//指定显示结果的条数
            .option("truncate","false")//如果某列的值过长,是否省略显示
            //启动
            .start()
      
          /**
            * step3:长久运行
            */
          query.awaitTermination()
          query.stop()
      
      
        }
      }
      
      
    • 注意:在实际工作的开发中,要自己管理偏移量

      • 每次处理,要将处理成功的偏移量写入MySQL
        • 可以,每条数据中都会记录这条数据对应的offset
      • 每次请求,从MySQL读取上次的位置
  • 常用属性

    image-20201226150604363

    image-20201226150739919

3、生产到Kafka

  • 规则

    • 要将数据写入Kafka,必须符合以下规则

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

      • 结果输出的DataSet或者DataFrame必须包含value

        • key可以没有
          • key决定分区规则
          • 没有:轮询
          • 有呢:按照Key的Hash
      • value:数据中必须包含一列,叫value

        • 如果你要将Wordcount的结果保存到Kafka

          rsData(String,Int)
          	.select(concat_ws(“ ”,value,count).as("value"))
          	
          
      • 如果在数据中不包含topic,在option中必须指定topic

        .option("topic", "topic1")
        
  • 需求:读Kafka的基站数据,将成功状态的数据写入Kafka中

    • 创建Topic

      ## ================================= stationTopic =================================
      kafka-topics.sh --create --zookeeper node1.itcast.cn:2181/kafka200 --replication-factor 1 --partitions 3 --topic stationTopic
      # 模拟生产者
      kafka-console-producer.sh --broker-list node1.itcast.cn:9092 --topic stationTopic
      # 模拟消费者
      kafka-console-consumer.sh --bootstrap-server node1.itcast.cn:9092 --topic stationTopic --from-beginning
      # 删除topic
      kafka-topics.sh --delete --zookeeper node1.itcast.cn:2181/kafka200 --topic stationTopic
      
      ## ================================= etlTopic =================================
      # 创建topic
      kafka-topics.sh --create --zookeeper node1.itcast.cn:2181/kafka200 --replication-factor 1 --partitions 3 --topic etlTopic
      # 模拟生产者
      kafka-console-producer.sh --broker-list node1.itcast.cn:9092 --topic etlTopic
      # 模拟消费者
      kafka-console-consumer.sh --bootstrap-server node1.itcast.cn:9092 --topic etlTopic --from-beginning
      
    • 代码实现

      package bigdata.itcast.cn.spark.struct.kafka.sink
      
      import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
      import org.apache.spark.sql.{DataFrame, SparkSession}
      
      /**
        * @ClassName StructStreamingMode
        * @Description TODO 结构化流实现词频统计,消费Kafka数据,实现ETL,将结果写入Kafka的另外一个Topic中
        * @Date 2020/12/26 10:55
        * @Create By     Frank
        */
      object StructStreamingKafkaSInk {
      
        def main(args: Array[String]): Unit = {
          /**
            * step1:先创建SparkSession
            */
          val spark = SparkSession.builder()
            .appName(this.getClass.getSimpleName.stripSuffix("$"))
            .master("local[3]")
            .config("spark.sql.shuffle.partitions","2")
            //设置检查点方式二
            .config("spark.sql.streaming.checkpointLocation","datastruct/output/chk2")
            .getOrCreate()
          //导入包
          import spark.implicits._
      
          /**
            * step2:处理逻辑
            */
          //todo:1-读取数据:不断追加新的数据到DF中
          val inputData: DataFrame = spark.readStream
            .format("kafka")
            .option("kafka.bootstrap.servers","node1:9092")
            .option("subscribe","stationTopic")
            .load()
      
          //todo:2-处理:不断的对每个批次的数据做处理聚合合并
          val rsData= inputData
            //从Kafka的每条数据中获取value
            .selectExpr("CAST(value AS STRING)")
            //转换DataSet
            .as[String]
            .filter(line => null != line && line.trim.length > 0 && line.trim.split(",")(3) == "success")
          rsData.printSchema()
      
          //todo:3-保存
          val query: StreamingQuery = rsData
            .writeStream
            .outputMode(OutputMode.Append())
            .format("kafka")
            .option("kafka.bootstrap.servers", "node1:9092")
            .option("topic", "etlTopic")
            //启动
            .start()
      
          /**
            * step3:长久运行
            */
          query.awaitTermination()
          query.stop()
      
      
        }
      }
      
      

4、常用属性

  • 看一下Kafka官网中常见的生产者和消费者的属性

七、物联网设备分析

1、需求分析

  • 数据格式

    {"device":"device_62","deviceType":"db","signal":31.0,"time":1608805779682}
    {"device":"device_32","deviceType":"kafka","signal":85.0,"time":1608805780208}
    {"device":"device_65","deviceType":"db","signal":73.0,"time":1608805780724}
    {"device":"device_98","deviceType":"bigdata","signal":58.0,"time":1608805780914}
    {"device":"device_43","deviceType":"route","signal":54.0,"time":1608805781028}
    {"device":"device_71","deviceType":"bigdata","signal":31.0,"time":1608805781320}
    {"device":"device_20","deviceType":"bigdata","signal":85.0,"time":1608805781481}
    {"device":"device_96","deviceType":"bigdata","signal":26.0,"time":1608805782002}
    {"device":"device_96","deviceType":"bigdata","signal":55.0,"time":1608805782411}
    {"device":"device_62","deviceType":"db","signal":21.0,"time":1608805782980}
    
  • 需求条件

    • 需求一:各种设备类型的设备数量
    • 需求二:各种设备类型的平均信号强度
    • 条件:信号强度必须大于30

2、创建Topic及模拟数据

  • 创建topic

    # 创建topic
    kafka-topics.sh --create --zookeeper node1.itcast.cn:2181/kafka200 --replication-factor 1 --partitions 3 --topic iotTopic
    # 模拟生产者
    kafka-console-producer.sh --broker-list node1.itcast.cn:9092 --topic iotTopic
    # 模拟消费者
    kafka-console-consumer.sh --bootstrap-server node1.itcast.cn:9092 --topic iotTopic --from-beginning
    # 删除topic
    kafka-topics.sh --delete --zookeeper node1.itcast.cn:2181/kafka200 --topic iotTopic
    
  • 模拟数据

    image-20201226154503229

3、DSL实现

package bigdata.itcast.cn.spark.struct.iot

import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
import org.apache.spark.sql.types.{DoubleType, LongType}
import org.apache.spark.sql.{DataFrame, SparkSession}

/**
  * @ClassName StructStreamingMode
  * @Description TODO 实现统计每种设备类型的设备个数,信号平均值,要求信号强度要大于30
  * @Date 2020/12/26 10:55
  * @Create By     Frank
  */
object StructStreamingIotDSL {

  def main(args: Array[String]): Unit = {
    /**
      * step1:先创建SparkSession
      */
    val spark = SparkSession.builder()
      .appName(this.getClass.getSimpleName.stripSuffix("$"))
      .master("local[3]")
      .config("spark.sql.shuffle.partitions","2")
      .getOrCreate()
    //导入包
    import spark.implicits._
    import org.apache.spark.sql.functions._

    /**
      * step2:处理逻辑
      */
    //todo:1-读取数据:不断追加新的数据到DF中
    val inputData: DataFrame = spark.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers","node1:9092")
      .option("subscribe","iotTopic")
      .load()

    //todo:2-处理:不断的对每个批次的数据做处理聚合合并
    val rsData: DataFrame = inputData
      //从Kafka的每条数据中获取value
      .selectExpr("CAST(value AS STRING)")
      //转换DataSet
      .as[String]
      //过滤空数据
      .filter(line => null != line && line.trim.length > 0)
      //将json数据的四个字段取出:{"device":"device_97","deviceType":"bigdata","signal":63.0,"time":1608968620018}
      .select(
        get_json_object($"value","$.device").as("device_id"),
        get_json_object($"value","$.deviceType").as("deviceType"),
        get_json_object($"value","$.signal").cast(DoubleType).as("signal"),
        get_json_object($"value","$.time").cast(LongType).as("time")
      )
      //过滤信号强度大于30
      .filter($"signal" > 30)
      //按照设备类型分组
      .groupBy($"deviceType")
      .agg(
        //统计每种设备类型的设备个数
        count($"device_id").as("cnt"),
        //统计每种设备类型的平均信号强度
        round(avg($"signal"),2).as("avgsignal")
      )

    //todo:3-保存
    val query: StreamingQuery = rsData
      .writeStream
      .outputMode(OutputMode.Complete())
      .format("console")
      .option("numRows","10")//指定显示结果的条数
      .option("truncate","false")//如果某列的值过长,是否省略显示
      //启动
      .start()

    /**
      * step3:长久运行
      */
    query.awaitTermination()
    query.stop()


  }
}

4、SQL实现

package bigdata.itcast.cn.spark.struct.iot

import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
import org.apache.spark.sql.types.{DoubleType, LongType}
import org.apache.spark.sql.{DataFrame, SparkSession}

/**
  * @ClassName StructStreamingMode
  * @Description TODO 实现统计每种设备类型的设备个数,信号平均值,要求信号强度要大于30
  * @Date 2020/12/26 10:55
  * @Create By     Frank
  */
object StructStreamingIotSQL {

  def main(args: Array[String]): Unit = {
    /**
      * step1:先创建SparkSession
      */
    val spark = SparkSession.builder()
      .appName(this.getClass.getSimpleName.stripSuffix("$"))
      .master("local[3]")
      .config("spark.sql.shuffle.partitions","2")
      .getOrCreate()
    //导入包
    import org.apache.spark.sql.functions._
    import spark.implicits._

    /**
      * step2:处理逻辑
      */
    //todo:1-读取数据:不断追加新的数据到DF中
    val inputData: DataFrame = spark.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers","node1:9092")
      .option("subscribe","iotTopic")
      .load()

    //todo:2-处理:不断的对每个批次的数据做处理聚合合并
    val etlData: DataFrame = inputData
      //从Kafka的每条数据中获取value
      .selectExpr("CAST(value AS STRING)")
      //转换DataSet
      .as[String]
      //过滤空数据
      .filter(line => null != line && line.trim.length > 0)
      //将json数据的四个字段取出:{"device":"device_97","deviceType":"bigdata","signal":63.0,"time":1608968620018}
      .select(
        get_json_object($"value","$.device").as("device_id"),
        get_json_object($"value","$.deviceType").as("deviceType"),
        get_json_object($"value","$.signal").cast(DoubleType).as("signal"),
        get_json_object($"value","$.time").cast(LongType).as("time")
      )

    //todo:将df注册为视图
    etlData.createOrReplaceTempView("tmp_view_iot")
    //todo:通过SQL来实现处理
    val rsData = spark.sql(
      """
        |select
        |   deviceType,
        |   count(device_id) as cnt,
        |   round(avg(signal),2) as avgsignal
        |from tmp_view_iot
        |where signal > 30
        |group by deviceType
      """.stripMargin)

    //todo:3-保存
    val query: StreamingQuery = rsData
      .writeStream
      .outputMode(OutputMode.Complete())
      .format("console")
      .option("numRows","10")//指定显示结果的条数
      .option("truncate","false")//如果某列的值过长,是否省略显示
      //启动
      .start()

    /**
      * step3:长久运行
      */
    query.awaitTermination()
    query.stop()


  }
}

八、数据去重Deduplication

1、需求及问题

  • 需求:实时的统计UV
    • 实时对每个用户ID进行去重
  • 问题:实时计算中,按照每条数据进行处理,怎么能将前后的数据进行去重
  • 实现
    • 主流的方案:不用流式计算框架来实现,用辅助存储框架来实现
      • 将实时的用户id写入Redis的Set集合
    • StructStreaming可以实现

2、测试实现

  • 数据

    {"eventTime": "2016-01-10 10:01:50","eventType": "browse","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:01:55","eventType": "browse","userID":"1"}
    {"eventTime": "2016-01-10 10:01:55","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "browse","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:02:00","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "browse","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:01:51","eventType": "click","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "browse","userID":"1"}
    {"eventTime": "2016-01-10 10:01:50","eventType": "click","userID":"3"}
    {"eventTime": "2016-01-10 10:01:51","eventType": "click","userID":"2"}
    
  • 代码实现

    package bigdata.itcast.cn.spark.struct.distinct
    
    import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
    import org.apache.spark.sql.{DataFrame, SparkSession}
    
    object StructuredDeduplication {
    
      def main(args: Array[String]): Unit = {
    
        // 构建SparkSession实例对象
        val spark: SparkSession = SparkSession.builder()
          .appName(this.getClass.getSimpleName.stripSuffix("$"))
          .master("local[2]")
          // 设置Shuffle分区数目
          .config("spark.sql.shuffle.partitions", "2")
          .getOrCreate()
        // 导入隐式转换和函数库
        import spark.implicits._
        import org.apache.spark.sql.functions._
    
        // 1. 从TCP Socket 读取数据
        val inputTable: DataFrame = spark.readStream
          .format("socket")
          .option("host", "node1.itcast.cn")
          .option("port", 9999)
          .load()
    
        // 2. 数据处理分析
        val resultTable: DataFrame = inputTable
          .as[String]
          .filter(line => null != line && line.trim.length > 0)
          // 样本数据:{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”}
          .select(
            get_json_object($"value", "$.eventTime").as("event_time"), //
            get_json_object($"value", "$.eventType").as("event_type"), //
            get_json_object($"value", "$.userID").as("user_id")//
          )
          // 按照UserId和EventType去重
          .dropDuplicates("user_id","event_type")
          .groupBy($"user_id", $"event_type")
          .count()
    
        // 3. 设置Streaming应用输出及启动
        val query: StreamingQuery = resultTable.writeStream
          .outputMode(OutputMode.Complete())
          .format("console")
          .option("numRows", "10")
          .option("truncate", "false")
          .start()
        query.awaitTermination() // 流式查询等待流式应用终止
        // 等待所有任务运行完成才停止运行
        query.stop()
      }
    
    }
    
    
    
  • 结果

    +-------+----------+-----+
    |user_id|event_type|count|
    +-------+----------+-----+
    |2      |click     |1    |
    |1      |click     |1    |
    |3      |click     |1    |
    |1      |browse    |1    |
    +-------+----------+-----+
    

九、Continue Process

1、需求及问题

  • 需求:实时数据量比较高,准实时计算的批处理会导致大量数据处理延迟,延迟会达到几百ms
  • 问题:怎么降低延迟性?
    • 需要使用真实时计算:数据产生一条就处理一条,接受处理和处理数据分为两个业务,实现降低延迟
      • 延迟可以控制在1ms
  • StructStreaming的触发时间三种方式
    • 微小时间的批处理
    • 处理一次
    • 持续性处理

2、测试实现

package bigdata.itcast.cn.spark.struct.continueprocess

import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, Trigger}
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}

/**
  * 从Spark 2.3版本开始,StructuredStreaming结构化流中添加新流式数据处理方式:Continuous processing
  *      持续流数据处理:当数据一产生就立即处理,类似Storm、Flink框架,延迟性达到100ms以下,目前属于实验开发阶段
  */
object StructuredContinuous {

  def main(args: Array[String]): Unit = {
    // 构建SparkSession实例对象
    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName.stripSuffix("$"))
      .master("local[3]")
      .config("spark.sql.shuffle.partitions", "3")
      .getOrCreate()
    // 导入隐式转换和函数库
    import spark.implicits._

    // 1. 从KAFKA读取数据
    val kafkaStreamDF: DataFrame = spark.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
      .option("subscribe", "stationTopic")
      .load()

    // 2. 对基站日志数据进行ETL操作
    // station_0,18600004405,18900009049,success,1589711564033,9000
    val etlStreamDF: Dataset[String] = kafkaStreamDF
      // 获取value字段的值,转换为String类型
      .selectExpr("CAST(value AS STRING)")
      // 转换为Dataset类型
      .as[String]
      // 过滤数据:通话状态为success
      .filter{log =>
      null != log && log.trim.split(",").length == 6 && "success".equals(log.trim.split(",")(3))
    }

    // 3. 针对流式应用来说,输出的是流
    val query: StreamingQuery = etlStreamDF.writeStream
      .outputMode(OutputMode.Append())
      .format("kafka")
      .option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
      .option("topic", "etlTopic")
      // 设置检查点目录
      .option("checkpointLocation", s"datastruct/output/checkpoint3")
      // TODO: 设置持续流处理 Continuous Processing, 指定CKPT时间间隔
      /*
          the continuous processing engine will records the progress of the query every second
              持续流处理引擎,将每1秒中记录当前查询Query进度状态
       */
      .trigger(Trigger.Continuous("1 second"))
      .start() // 流式应用,需要启动start
    // 查询器等待流式应用终止
    query.awaitTermination()
    query.stop() // 等待所有任务运行完成才停止运行
  }

}

十、基于事件时间的窗口

1、问题及需求

  • 问题:以双十一为例

    • 每3s处理一次,程序从2020-11-11 00:00:00

      2020-11-11 00:00:00	2020-11-11 00:00:03		100亿		100亿
      2020-11-11 00:00:03	2020-11-11 00:00:06		100亿		200亿
      
    • 有一个人,处于偏僻的山区

      • 2020-11-10 23:59:59下了一个订单:10亿
      • 2020-11-11 00:00:01到达后台的计算系统
    • 出现问题了:结果是不对的,第一个批次中有10亿的成交额是11-10号的,两种时间

      • 数据产生时间:事件时间
      • 数据计算时间:处理时间
  • 需求:数据延迟到达是非常正常的现象,数据处理的时间可以是任意,这个数据计算的结果应该属于数据的产生时间

  • 理解

    • 生活中数据从产生到得到结果,需要经过网络的传递,在网络传递中会有延迟
    • 事件时间:数据生成的时间
    • 到达时间:数据到达系统的时间
    • 处理时间:数据真正被计算的时间
    • 指的是不论数据什么时候到达,什么时候被计算,这个延迟数据的结果都只属于数据生成的时间的结果,不属于其他时间的结果
  • 官网中的解析

    image-20201226162751556

  • 目的

    2020-11-11 00:00:00	2020-11-11 00:00:03		100亿		100亿
    2020-11-11 00:00:03	2020-11-11 00:00:06		100亿		200亿
    

    |

    2020-11-10									10亿
    2020-11-11 00:00:00	2020-11-11 00:00:03		90亿			90亿
    2020-11-11 00:00:03	2020-11-11 00:00:06		100亿		190亿
    
  • 问题:我们怎么知道这个数据的结果对应的数据生成时间呢?

    • 就是数据中的那个时间字段

2、测试实现

  • 测试数据

    2019-10-12 09:00:02,cat dog
    2019-10-12 09:00:03,dog dog
    
    2019-10-12 09:00:07,owl cat
    
    2019-10-12 09:00:11,dog
    2019-10-12 09:00:13,owl
    
    
  • 代码测试

    package bigdata.itcast.cn.spark.struct.window
    
    import java.sql.Timestamp
    
    import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, Trigger}
    import org.apache.spark.sql.{DataFrame, SparkSession}
    
    /**
      * 基于Structured Streaming 模块读取TCP Socket读取数据,进行事件时间窗口统计词频WordCount,将结果打印到控制台
      *      TODO:每5秒钟统计最近10秒内的数据(词频:WordCount)
      *
      * EventTime即事件真正生成的时间:
      *  例如一个用户在10:06点击 了一个按钮,记录在系统中为10:06
      *  这条数据发送到Kafka,又到了Spark Streaming中处理,已经是10:08,这个处理的时间就是process Time。
      *
      */
    object StructuredWindow {
    
      def main(args: Array[String]): Unit = {
    
        // 1. 构建SparkSession实例对象,传递sparkConf参数
        val spark: SparkSession = SparkSession.builder()
          .appName(this.getClass.getSimpleName.stripSuffix("$"))
          .master("local[2]")
          .config("spark.sql.shuffle.partitions", "2")
          .getOrCreate()
        // 导入隐式转换及函数库
        import org.apache.spark.sql.functions._
        import spark.implicits._
    
        // 2. 使用SparkSession从TCP Socket读取流式数据
        val inputStreamDF: DataFrame = spark.readStream
          .format("socket")
          .option("host", "node1.itcast.cn")
          .option("port", 9999)
          .load()
    
        // 3. 针对获取流式DStream进行词频统计
        val resultStreamDF = inputStreamDF
          // 将DataFrame转换为Dataset操作,Dataset是类型安全,强类型
          .as[String]
          // 过滤无效数据
          .filter(line => null != line && line.trim.length > 0)
          // 将每行数据进行分割单词: 2019-10-12 09:00:02,cat dog
          .flatMap{line =>
          val arr = line.trim.split(",")
          arr(1).split("\\s+").map(word => (Timestamp.valueOf(arr(0)), word))
        }
    
          /**
            * 2019-10-12 09:00:02,cat
            * 2019-10-12 09:00:02,dog
            * 2019-10-12 09:00:03,dog
            * 2019-10-12 09:00:03,dog
            */
          // 设置列的名称
          .toDF("insert_timestamp", "word")
          // TODO:设置基于事件时间(event time)窗口 -> insert_timestamp, 每5秒统计最近10秒内数据
          /*
                1. 先按照窗口分组
                2. 再对窗口中按照单词分组
                3. 最后使用聚合函数聚合
           */
          .groupBy(
            window($"insert_timestamp", "10 seconds", "5 seconds"),
            $"word"
          )
          .count()
          // 按照窗口字段降序排序
          .orderBy($"window")
    
        /*
            root
             |-- window: struct (nullable = true)
             |    |-- start: timestamp (nullable = true)
             |    |-- end: timestamp (nullable = true)
             |-- word: string (nullable = true)
             |-- count: long (nullable = false)
         */
        //resultStreamDF.printSchema()
    
        // 4. 将计算的结果输出,打印到控制台
        val query: StreamingQuery = resultStreamDF.writeStream
          .outputMode(OutputMode.Complete())
          .format("console")
          .option("numRows", "100")
          .option("truncate", "false")
          .trigger(Trigger.ProcessingTime("5 seconds"))
          .start()  // 流式DataFrame,需要启动
        // 查询器一直等待流式应用结束
        query.awaitTermination()
        query.stop()
      }
    
    }
    
    
  • 结果

    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-10-12 08:59:55, 2019-10-12 09:00:05]|dog |3    |
    |[2019-10-12 08:59:55, 2019-10-12 09:00:05]|cat |1    |
    
    |[2019-10-12 09:00:00, 2019-10-12 09:00:10]|dog |3    |
    |[2019-10-12 09:00:00, 2019-10-12 09:00:10]|owl |1    |
    |[2019-10-12 09:00:00, 2019-10-12 09:00:10]|cat |2    |
    
    |[2019-10-12 09:00:05, 2019-10-12 09:00:15]|cat |1    |
    |[2019-10-12 09:00:05, 2019-10-12 09:00:15]|owl |2    |
    |[2019-10-12 09:00:05, 2019-10-12 09:00:15]|dog |1    |
    
    |[2019-10-12 09:00:10, 2019-10-12 09:00:20]|dog |1    |
    |[2019-10-12 09:00:10, 2019-10-12 09:00:20]|owl |1    |
    +------------------------------------------+----+-----+
    

3、窗口生成

2019-10-12 09:00:02,cat dog
2019-10-12 09:00:03,dog dog

2019-10-12 09:00:07,owl cat

2019-10-12 09:00:11,dog
2019-10-12 09:00:13,owl

  • 窗口时间:10

  • 滑动时间:5

  • 先计算初始窗口:event-time向上取 能整除 滑动步长的时间) - (最大窗口数×滑动步长)

    • 9:00:00 - (2 * 5) = 8:59:50
    • 初始窗口: 8:59:50 ~ 9:00:00
    • 不包含
  • 再按照滑动时间来计算运行窗口

    • 8:59:55 ~ 9:00:05
    • 9:00:00 ~ 9:00:10
    • 9:00:05~ 9:00:15
    • 9:00:10 ~ 9:00:20
  • 最后再计算结束窗口

    • 9:00:15 ~ 9:00:25:如果最大的事件时间不在这个窗口,不再计算,这个窗口作为结束窗口

    • 不包含

4、延迟数据处理:WaterMark:水印

  • 设计

    • 问题:如果数据迟到很久了,再计算也没有意义了,这条数据是否进行处理

    • 解决:通过水印来计算这条数据是否还进行处理

    • 举个栗子:

      • 对双十一的订单总金额做统计

        • 今天是:2020-11-12 00:05:00

          • 收到了一条数据

            2020-11-11 23:59:59 	orderid		100万
            
          • 还没有对外公布总的成交额,这条数据继续处理

        • 今天是:2020-11-13 00:00:00

          • 收到了一条数据

            2020-11-11 23:59:59 	orderid		100万
            
          • 12号已经对外公布了最终结果:2000亿

          • 从业务的角度来说,这条数据没有价值,不再处理

  • 方案:设置一个范围:水位,高于水位线的数据要,低于水位线的数据不要了

  • 官网的解释

    image-20201226170450751

    image-20201226170936916

    • 水位的计算

      Watermark = MaxEventTime - Threshod
      

      image-20201226171235393

  • 数据

    dog,2019-10-10 12:00:07
    owl,2019-10-10 12:00:08
    
    dog,2019-10-10 12:00:14
    cat,2019-10-10 12:00:09
    
    cat,2019-10-10 12:00:15
    dog,2019-10-10 12:00:08
    owl,2019-10-10 12:00:13
    owl,2019-10-10 12:00:21
    
    owl,2019-10-10 12:00:17
    
  • 代码测试

    package bigdata.itcast.cn.spark.struct.window
    
    import java.sql.Timestamp
    
    import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, Trigger}
    import org.apache.spark.sql.{DataFrame, SparkSession}
    
    /**
      * 基于Structured Streaming 读取TCP Socket读取数据,事件时间窗口统计词频,将结果打印到控制台
      *      TODO:每5秒钟统计最近10秒内的数据(词频:WordCount),设置水位Watermark时间为10秒
      */
    object StructuredWatermarkUpdate {
    
      def main(args: Array[String]): Unit = {
    
        // 1. 构建SparkSession实例对象,传递sparkConf参数
        val spark: SparkSession =  SparkSession.builder()
          .appName(this.getClass.getSimpleName.stripSuffix("$"))
          .master("local[2]")
          .config("spark.sql.shuffle.partitions", "2")
          .getOrCreate()
        // b. 导入隐式转换及函数库
        import org.apache.spark.sql.functions._
        import spark.implicits._
    
        // 2. 使用SparkSession从TCP Socket读取流式数据
        val inputStreamDF: DataFrame = spark.readStream
          .format("socket")
          .option("host", "node1.itcast.cn")
          .option("port", 9999)
          .load()
    
        // 3. 针对获取流式DStream设置EventTime窗口及Watermark水位限制
        val resultStreamDF = inputStreamDF
          // 将DataFrame转换为Dataset操作,Dataset是类型安全,强类型
          .as[String]
          // 过滤无效数据
          .filter(line => null != line && line.trim.length > 0)
          // 将每行数据进行分割单词: 2019-10-12 09:00:02,cat dog
          .map{line =>
          val arr = line.trim.split(",")
          (arr(0), Timestamp.valueOf(arr(1)))
        }
          // 设置列的名称
          .toDF("word", "time")
          // TODO:设置水位Watermark
          .withWatermark("time", "10 seconds")
          // TODO:设置基于事件时间(event time)窗口 -> time, 每5秒统计最近10秒内数据
          .groupBy(
          window($"time", "10 seconds", "5 seconds"),
          $"word"
        ).count()
    
        // 4. 将计算的结果输出,打印到控制台
        val query: StreamingQuery = resultStreamDF.writeStream
          .outputMode(OutputMode.Update())
          .format("console")
          .option("numRows", "100")
          .option("truncate", "false")
          .trigger(Trigger.ProcessingTime("5 seconds"))
          .start()  // 流式DataFrame,需要启动
        // 查询器一直等待流式应用结束
        query.awaitTermination()
        query.stop()
      }
    
    }
    
    
    
  • 结果

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-10-10 12:00:00, 2019-10-10 12:00:10]|dog |2    |
    |[2019-10-10 12:00:00, 2019-10-10 12:00:10]|cat |1    |
    |[2019-10-10 12:00:10, 2019-10-10 12:00:20]|cat |1    |
    |[2019-10-10 12:00:00, 2019-10-10 12:00:10]|owl |1    |
    |[2019-10-10 12:00:10, 2019-10-10 12:00:20]|dog |1    |
    |[2019-10-10 12:00:05, 2019-10-10 12:00:15]|dog |3    |
    |[2019-10-10 12:00:05, 2019-10-10 12:00:15]|cat |1    |
    |[2019-10-10 12:00:15, 2019-10-10 12:00:25]|cat |1    |
    |[2019-10-10 12:00:05, 2019-10-10 12:00:15]|owl |2    |
    |[2019-10-10 12:00:10, 2019-10-10 12:00:20]|owl |1    |
    |[2019-10-10 12:00:15, 2019-10-10 12:00:25]|owl |1    |
    |[2019-10-10 12:00:20, 2019-10-10 12:00:30]|owl |1    |
    +------------------------------------------+----+-----+
    

附录一: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>
        <jedis.version>3.2.0</jedis.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>
        <!-- Structured Streaming + Kafka  依赖 -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql-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>2.0.0</version>
        </dependency>
        <!-- MySQL Client 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- Jedis 依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>
        <!-- JSON解析库:fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</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
    

附录三:SparkStreaming实现Kafka手动管理偏移量

  • 设计

    • 如果程序故障,重新启动程序,程序不知道上次处理的位置,Kafka中记录的位置与实际的位置也会有偏差,怎么保证一次性语义
    • 方案
      • 不论程序是第一次运行还是故障重启,每次都向Kafka提交我们自己管理的偏移量
      • 每次消费处理成功,就将每个批次的最新的偏移量存储起来
      • 存储到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工具类

    • 读MySQL

      /**
          * 根据用户的id和用于要消费的Topic获取上次消费的偏移量
          * @param topics:指定消费的topic
          * @param groupId:指定哪个消费者消费的
          * @return
          */
        def readFromMySQL(topics: Array[String],groupId:String):Map[TopicPartition, Long] ={
          //先构建返回值
          val maps:mutable.Map[TopicPartition, Long] = mutable.Map.empty
          //读MySQL
          Class.forName("com.mysql.cj.jdbc.Driver")
          var conn:Connection = null
          var pstm:PreparedStatement = null
          try{
            //构建连接
            conn = DriverManager.getConnection(
              "jdbc:mysql://node1.itcast.cn:3306/?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true",
              "root",
              "123456"
            )
            //将topic数组转换为一个字符串:`topic1`,`topic2`……
            val topicName = topics.map(topic => s"\'${topic}\'").mkString(", ")
            val sql = s"select * from db_spark.tb_offset where topic in (${topicName}) and groupid = '${groupId}'"
            println(sql)
            pstm = conn.prepareStatement(sql)
            val rs = pstm.executeQuery()
            //取出数据
            while(rs.next()){
              val topic = rs.getString("topic")
              val part = rs.getInt("partition")
              val offset = rs.getLong("offset")
              //放入值
              maps += new TopicPartition(topic,part) -> offset
            }
          }catch {
            case e:Exception => e.printStackTrace()
          }finally {
            if(pstm != null) pstm.close()
            if(conn != null) conn.close()
          }
      
          //返回
          maps.toMap
        }
      
    • 写MySQL

        /**
          * 将每个处理成功的批次的offset写入mysql,用于下一次进行读取
          * @param offsetRanges
          * @param groupId
          */
        def writeToMySQL(offsetRanges: Array[OffsetRange],groupId:String):Unit ={
          Class.forName("com.mysql.cj.jdbc.Driver")
          var conn:Connection = null
          var pstm:PreparedStatement = null
          try{
            //构建连接
            conn = DriverManager.getConnection(
              "jdbc:mysql://node1.itcast.cn:3306/?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true",
              "root",
              "123456"
            )
            //定义SQL语句
            val sql = "replace into db_spark.tb_offset (`topic`, `partition`, `groupid`, `offset`) values(?, ?, ?, ?)"
            pstm = conn.prepareStatement(sql)
            //赋值
            offsetRanges.foreach(record => {
              pstm.setString(1,record.topic)
              pstm.setInt(2,record.partition)
              pstm.setString(3,groupId)
              pstm.setLong(4,record.untilOffset)
              //添加到批次中
              pstm.addBatch()
            })
            //执行批次
            pstm.executeBatch()
      
          }catch {
            case e:Exception => e.printStackTrace()
          }finally {
            if(pstm != null) pstm.close()
            if(conn != null) conn.close()
          }
        }
      
  • 代码实现

    • 读MySQL请求

      //从MySQL中读取我们上次消费的位置
            val offsets: Map[TopicPartition, Long] =  OffsetsUtils.readFromMySQL(topics,groupId)
            val consumerStrategy: ConsumerStrategy[String, String] = ConsumerStrategies.Subscribe(
              topics, kafkaParams,offsets
            )
      
    • 保存最新偏移量到MySQL

       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)}
              //处理完成:将这次最新的偏移量,写入MySQL
              OffsetsUtils.writeToMySQL(offsetRanges,groupId)
            }
          }
        }
      
  • 测试数据

    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例无症状相关情况
    
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

章鱼哥TuNan&Z

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

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

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

打赏作者

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

抵扣说明:

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

余额充值