Spark Day14:Structured Streaming

01-[了解]-上次课程内容回顾

继续讲解:StructuredStreaming,以结构化方式处理流式数据,底层分析引擎SparkSQL引擎。

0、数据源(Source)
	支持4种数据源:TCP Socket(最简单)、Kafka Source(最常用)
		- File Source:监控某个目录,当目录中有新的文件时,以流的方式读取数据
		- Rate Source:自动每秒生成一定数量数据

1、StreamingQuery基本设置
	- 设置查询名称:queryName
	- 设置触发时间间隔
		默认值:Trigger.Processing("0 seconds"),一有数据,立即处理
	- 检查点Checkpoint目录
		sparkConf.conf("spark.sql.streaming.checkpointLocation", "xx")
		option("checkpointLocation", "xx")
	- 输出模式OutputMode
		Append,追加,数据都是新的
		Update,更新数据输出
		Complete,所有数据输出

2、Sink终端
	表示处理流式数据结果输出地方,比如Console控制台,也可以输出到File Sink
	自定义输出
		- foreach,表示针对每条数据的输出
		- foreachBatch,表示针对每批次数据输出,可以重用SparkSQL中数据源的输出

3、集成Kafka(数据源Source和数据终端Sink)
	既可以从Kafka消费数据,也可以向Kafka写入数据
	- 数据源Source:从Kafka消费数据,其他参数可以设置
val df = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  // .option("subscribe", "topic1,topic2") // .option("subscribePattern", "topic.*")
  .option("subscribe", "topic1") 
  .load()
df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]		

	- 数据终端Sink:将流式数据集DataFrame数据写入到Kafka 中,要求必须value字段值,类型为String
val ds = df
  .selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .writeStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("topic", "topic1")
  .start()

02-[掌握]-集成Kafka之实时增量ETL(DSL)

需求:使用DataFrame DSL进行ETL转换,要求定义UDF函数

/* ============================= 基于Dataset 转换操作 ====================*/
val etlStreamDF: Dataset[String] = kafkaStreamDF
    .selectExpr("CAST(value AS STRING)") // 提取value字段值,并且转换为String类型
    .as[String] // 转换为Dataset
    .filter{msg =>
	    null != msg && 
		    msg.trim.split(",").length == 6 && 
		    "success".equals(msg.trim.split(",")(3))
    }


/* ============================= 基于 DataFrame DSL操作 ====================*/
val filter_udf: UserDefinedFunction = udf(
	(msg: String) => {
		null != msg &&
			msg.trim.split(",").length == 6 &&
			"success".equals(msg.trim.split(",")(3))
	}
)
val etlStreamDF: Dataset[Row] = kafkaStreamDF
    .select($"value".cast(StringType))
    .filter(filter_udf($"value"))

完整代码如下:

package cn.itcast.spark.kafka

import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, Trigger}
import org.apache.spark.sql.types.StringType
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 实时从Kafka Topic消费基站日志数据,过滤获取通话转态为success数据,再存储至Kafka Topic中
	 * 1、从KafkaTopic中获取基站日志数据
	 * 2、ETL:只获取通话状态为success日志数据
	 * 3、最终将ETL的数据存储到Kafka Topic中
 */
object _01StructuredEtlKafka {
	
	def main(args: Array[String]): Unit = {
		
		// 构建SparkSession实例对象
		val spark: SparkSession = SparkSession.builder()
			.appName(this.getClass.getSimpleName.stripSuffix("$"))
			.master("local[3]")
			// 设置Shuffle分区数目
			.config("spark.sql.shuffle.partitions", "3")
			.getOrCreate()
		// 导入隐式转换和函数库
		import spark.implicits._
	
		// TODO: 1. 从Kafka Topic中获取基站日志数据(模拟数据,文本数据)
		val kafkaStreamDF: DataFrame = spark
			.readStream
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("subscribe", "stationTopic")
			.option("maxOffsetsPerTrigger", "10000")
			.load()
		
		// TODO: 2. ETL:只获取通话状态为success日志数据
		/*
		val etlStreamDF: Dataset[String] = kafkaStreamDF
		    .selectExpr("CAST(value AS STRING)") // 提取value字段值,并且转换为String类型
		    .as[String] // 转换为Dataset[String]
		    .filter{msg =>
			    null != msg &&
				    msg.trim.split(",").length == 6 &&
				    "success".equals(msg.trim.split(",")(3))
		    }
		 */
		val filter_udf: UserDefinedFunction = udf(
			(msg: String) => {
				null != msg &&
					msg.trim.split(",").length == 6 &&
					"success".equals(msg.trim.split(",")(3))
			}
		)
		val etlStreamDF: DataFrame = kafkaStreamDF
			// 选择value字段,值转换为String类型
    		.select($"value".cast(StringType))
			// 过滤数据:status为success
    		.filter(filter_udf($"value"))
		
		
		// TODO: 3. 最终将ETL的数据存储到Kafka Topic中
		val query: StreamingQuery = etlStreamDF
			.writeStream
			.queryName("query-state-etl")
			.outputMode(OutputMode.Append())
    		.trigger(Trigger.ProcessingTime(0))
			// TODO:将数据保存至Kafka Topic中
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("topic", "etlTopic")
    		.option("checkpointLocation", "datas/ckpt-kafka/10001")
			.start()
		query.awaitTermination()
		query.stop()
	}
	
}

运行流式应用程序,查看Checkpoint目录数据结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukL1lpQ2-1620545056100)(/img/image-20210508150803575.png)]


需求:修改上述代码,将ETL后数据转换为JSON数据,存储到Kafka Topic中。

station_6,18600007723,18900006663,success,1620457646879,10000
					|
{
    "stationId": "station_6",
    "callOut": "18600007723",
    "callIn": "18900006663",
    "callStatus": "success",
    "callTime": "1620457646879",
    "duration": "10000"
}

step1、分割文本数据,获取各个字段的值
step2、给以Schema,就是字段名称
step3、转换为JSON字符串

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hAwQT9nM-1620545056101)(/img/image-20210508152803845.png)]

package cn.itcast.spark.kafka

import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery, Trigger}
import org.apache.spark.sql.types.StringType
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 实时从Kafka Topic消费基站日志数据,过滤获取通话转态为success数据,再存储至Kafka Topic中
	 * 1、从KafkaTopic中获取基站日志数据
	 * 2、ETL:只获取通话状态为success日志数据
	 * 3、最终将ETL的数据存储到Kafka Topic中
 */
object _01StructuredEtlKafka {
	
	def main(args: Array[String]): Unit = {
		
		// 构建SparkSession实例对象
		val spark: SparkSession = SparkSession.builder()
			.appName(this.getClass.getSimpleName.stripSuffix("$"))
			.master("local[3]")
			// 设置Shuffle分区数目
			.config("spark.sql.shuffle.partitions", "3")
			.getOrCreate()
		// 导入隐式转换和函数库
		import spark.implicits._
	
		// TODO: 1. 从Kafka Topic中获取基站日志数据(模拟数据,文本数据)
		val kafkaStreamDF: DataFrame = spark
			.readStream
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("subscribe", "stationTopic")
			.option("maxOffsetsPerTrigger", "10000")
			.load()
		
		// TODO: 2. ETL:只获取通话状态为success日志数据
		/*
		val etlStreamDF: Dataset[String] = kafkaStreamDF
		    .selectExpr("CAST(value AS STRING)") // 提取value字段值,并且转换为String类型
		    .as[String] // 转换为Dataset[String]
		    .filter{msg =>
			    null != msg &&
				    msg.trim.split(",").length == 6 &&
				    "success".equals(msg.trim.split(",")(3))
		    }
		 */
		val filter_udf: UserDefinedFunction = udf(
			(msg: String) => {
				null != msg &&
					msg.trim.split(",").length == 6 &&
					"success".equals(msg.trim.split(",")(3))
			}
		)
		val etlStreamDF: DataFrame = kafkaStreamDF
			// 选择value字段,值转换为String类型
    		.select($"value".cast(StringType))
			// 过滤数据:status为success
    		.filter(filter_udf($"value"))
			// 将每行数据进行分割
    		.as[String]
    		.map{msg =>
			    val Array(stationId,callOut,callIn,callStatus,callTime,duration) = msg.trim.split(",")
			    // 返回6元组
			    (stationId,callOut,callIn,callStatus,callTime,duration)
		    }
			// 调用toDF函数,指定列名称
    		.toDF("stationId", "callOut", "callIn", "callStatus", "callTime", "duration")
			// 将所有字段合并为JSON字符串
    		.select(
			    to_json(struct($"*")).as("value")
		    )
		
		// TODO: 3. 最终将ETL的数据存储到Kafka Topic中
		val query: StreamingQuery = etlStreamDF
			.writeStream
			.queryName("query-state-etl")
			.outputMode(OutputMode.Append())
    		.trigger(Trigger.ProcessingTime(0))
			// TODO:将数据保存至Kafka Topic中
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("topic", "etlTopic")
    		.option("checkpointLocation", "datas/ckpt-kafka/10001")
			.start()
		query.awaitTermination()
		query.stop()
	}
	
}

03-[了解]-今日课程内容提纲

继续讲解StructuredStreaming结构化流中知识点:

1、高级特性
	本质上还是微批处理,增量查询,每次处理数据是1条或者多条
	- Spark 2.3开始,数据处理模式:
		Continues Processing,持续流处理,来一条数据处理一条数据,做到真正的实时处理
		目前功能属于测试阶段
	- 对流式数据进行去重
		批处理分析时:UV,唯一访客数


2、案例:物联网数据实时分析
	模拟产生监控数据
	DSL和SQL进行实时流式数据分析
		熟悉SparkSQL中数据分析API或函数使用


3、窗口统计分析:基于事件时间EvnetTime窗口分析
	原理和案例演示
	延迟数据处理,使用Watermark水位线

04-[掌握]-高级特性之Continuous Processing

连续处理(Continuous Processing)是Spark 2.3中引入的一种新的实验性流执行模式,可实现低的(~1 ms)端到端延迟,并且至少具有一次容错保证。

连续处理(Continuous Processing)是“真正”的流处理,通过运行一个long-running的operator用来处理数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VC4mb4G4-1620545056103)(/img/image-20210508155621190.png)]

continuous mode 处理模式只要一有数据可用就会进行处理,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvvZvwKB-1620545056105)(/img/image-20210508155812014.png)]

范例演示:从Kafka实时消费数据,经过ETL处理后,将数据发送至Kafka Topic。

目前(Spark2.4.5版本)仅仅支持从Kafka消费数据,向Kafka写入数据,当前ContinuesProcessing处理模式

package cn.itcast.spark.continuous

import java.util.concurrent.TimeUnit

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 _02StructuredContinuous {
	
	def main(args: Array[String]): Unit = {
		
		// 构建SparkSession实例对象
		val spark: SparkSession = SparkSession.builder()
			.appName(this.getClass.getSimpleName.stripSuffix("$"))
			.master("local[3]")
			// 设置Shuffle分区数目
			.config("spark.sql.shuffle.partitions", "3")
			.getOrCreate()
		// 导入隐式转换和函数库
		import spark.implicits._
	
		// TODO: 1. 从KafkaTopic中获取基站日志数据(模拟数据,文本数据)
		val kafkaStreamDF: DataFrame = spark
			.readStream
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("subscribe", "stationTopic")
			.load()
		
		// TODO: 2. ETL:只获取通话状态为success日志数据
		val etlStreamDF: Dataset[String] = kafkaStreamDF
			// 提取value值,并转换为String类型,最后将DataFrame转换为Dataset
			.selectExpr("CAST(value AS STRING)")
			.as[String]
			// 进行数据过滤 -> station_2,18600007445,18900008443,success,1606466627272,2000
    		.filter(msg => {
			    null != msg && msg.trim.split(",").length == 6 && "success".equals(msg.trim.split(",")(3))
		    })
		
		// TODO: 3. 最终将ETL的数据存储到Kafka Topic中
		val query: StreamingQuery = etlStreamDF
			.writeStream
			.queryName("query-state-etl")
			.outputMode(OutputMode.Append())
			// TODO: 设置连续处理Continuous Processing,其中interval时间间隔为Checkpoint时间间隔
    		.trigger(Trigger.Continuous(1, TimeUnit.SECONDS))
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("topic", "etlTopic")
    		.option("checkpointLocation", "data/structured/station-etl-1002")
			.start()
		query.awaitTermination()
		query.stop()
	}
	
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iFoxcaPP-1620545056106)(/img/image-20210508160532284.png)]

05-[掌握]-高级特性之Streaming Deduplication

在StructuredStreaming结构化流中,可以对流式数据进行去重操作,提供API函数:deduplication

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ty91fp0-1620545056107)(/img/image-20210508161129628.png)]

演示范例:对网站用户日志数据,按照userId和eventType去重统计,网站代码如下。

package cn.itcast.spark.deduplication

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

/**
 * StructuredStreaming对流数据按照某些字段进行去重操作,比如实现UV类似统计分析
 */
object _03StructuredDeduplication {
	
	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 org.apache.spark.sql.functions._
		import spark.implicits._
		
		// 1. 从TCP Socket 读取数据
		val inputTable: DataFrame = spark.readStream
			.format("socket") // 列名称为:value,数据类型为:String类型
			.option("host", "node1.itcast.cn")
			.option("port", 9999)
			.load()
		
		// 2. 数据处理分析: {"eventTime": "2016-01-10 10:01:50","eventType": "browse","userID":"1"}
		val resultTable: DataFrame = inputTable
			// 需要从JSON字符串中,提取字段的之
    		.select(
			    get_json_object($"value", "$.userID").as("userId"), //
			    get_json_object($"value", "$.eventType").as("eventType") //
		    )
			// 按照userId和EventType去重
    		.dropDuplicates("userId", "eventType")
			// 分组统计
    		.groupBy($"userId", $"eventType").count()
		
		// 3. 设置Streaming应用输出及启动
		val query: StreamingQuery = resultTable.writeStream
			.outputMode(OutputMode.Complete())
			.format("console")
			.option("numRows", "100")
			.option("truncate", "false")
			.start()
		query.awaitTermination() // 流式查询等待流式应用终止
		// 等待所有任务运行完成才停止运行
		query.stop()
	}
	
}

06-[掌握]-物联网数据实时分析之需求概述及准备

物联网IoT:Internet of Things

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gIy5MJXB-1620545056107)(/img/image-20210508161936735.png)]

​ 模拟一个智能物联网系统的数据统计分析,产生设备数据发送到Kafka,结构化流Structured Streaming实时消费统计。对物联网设备状态信号数据,实时统计分析:

1)、信号强度大于30的设备; 
2)、各种设备类型的数量;
3)、各种设备类型的平均信号强度;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ksUWmB0H-1620545056108)(/img/image-20210508162232609.png)]

运行数据模拟生成器,产生设备监控数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jKE2Lmj3-1620545056108)(/img/image-20210508162557596.png)]

07-[掌握]-物联网数据实时分析之基于DSL实现

按照业务需求,从Kafka消费日志数据,基于DataFrame数据结构调用函数分析,代码如下:

package cn.itcast.spark.iot.dsl

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

/**
 * 对物联网设备状态信号数据,实时统计分析:
	 * 1)、信号强度大于30的设备
	 * 2)、各种设备类型的数量
	 * 3)、各种设备类型的平均信号强度
 */
object _04IotStreamingOnlineDSL {
	
	def main(args: Array[String]): Unit = {
		
		// 1. 构建SparkSession会话实例对象,设置属性信息
		val spark: SparkSession = SparkSession.builder()
			.appName(this.getClass.getSimpleName.stripSuffix("$"))
			.master("local[3]")
			.config("spark.sql.shuffle.partitions", "3")
			.getOrCreate()
		// 导入隐式转换和函数库
		import org.apache.spark.sql.functions._
		import spark.implicits._
		
		// 2. 从Kafka读取数据,底层采用New Consumer API
		val iotStreamDF: DataFrame = spark.readStream
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("subscribe", "iotTopic")
			// 设置每批次消费数据最大值
			.option("maxOffsetsPerTrigger", "100000")
			.load()
		
		// 3. 对流式数据进行提取字段
		val etlStreamDF: DataFrame = iotStreamDF
			.selectExpr( "CAST(value AS STRING)")
			// {"device":"device_10","deviceType":"db","signal":86.0,"time":1620462343550}
    		.select(
			    get_json_object($"value", "$.device").as("deviceId"), //
			    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") //
		    )
		
		// 4. 按照需求进行分析
		val resultStreamDF: DataFrame = etlStreamDF
			// 信号强度大于30的设备
    		.filter($"signal".gt(30))
			// 各种设备类型的数量 和 各种设备类型的平均信号强度
    		.groupBy($"deviceType")
    		.agg(
			    count($"deviceId").as("total"), //类型的数量
			    round(avg($"signal"), 2).as("avg_signal") // 平均信号强度
		    )
   
		// 5. 启动流式应用,结果输出控制台
		val query: StreamingQuery = resultStreamDF.writeStream
			.outputMode(OutputMode.Complete())
			.format("console")
			.option("numRows", "10")
			.option("truncate", "false")
			.start()
		query.awaitTermination()
		query.stop()
	}
	
}

08-[掌握]-物联网数据实时分析之基于SQL实现

​ 按照业务需求,从Kafka消费日志数据,提取字段信息,将DataFrame注册为临时视图,编写SQL执行分析,代码如下:

package cn.itcast.spark.iot.sql

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

/**
 * 对物联网设备状态信号数据,实时统计分析:
	 * 1)、信号强度大于30的设备
	 * 2)、各种设备类型的数量
	 * 3)、各种设备类型的平均信号强度
 */
object _05IotStreamingOnlineSQL {
	
	def main(args: Array[String]): Unit = {
		
		// 1. 构建SparkSession会话实例对象,设置属性信息
		val spark: SparkSession = SparkSession.builder()
			.appName(this.getClass.getSimpleName.stripSuffix("$"))
			.master("local[3]")
			.config("spark.sql.shuffle.partitions", "3")
			.getOrCreate()
		// 导入隐式转换和函数库
		import org.apache.spark.sql.functions._
		import spark.implicits._
		
		// 2. 从Kafka读取数据,底层采用New Consumer API
		val iotStreamDF: DataFrame = spark.readStream
			.format("kafka")
			.option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
			.option("subscribe", "iotTopic")
			// 设置每批次消费数据最大值
			.option("maxOffsetsPerTrigger", "100000")
			.load()
		
		// 3. 对流式数据进行提取字段
		val schema: StructType = new StructType()
    		.add("device", StringType, nullable = true)
    		.add("deviceType", StringType, nullable = true)
    		.add("signal", DoubleType, nullable = true)
    		.add("time", LongType, nullable = true)
		
		val etlStreamDF: DataFrame = iotStreamDF
			// {"device":"device_42","deviceType":"route","signal":10.0,"time":1620463866721}
    		.select($"value".cast(StringType))
			// 解析JSON格式数据
    		.select(
			   from_json($"value", schema).as("device")
		    )
			// 选取结构类型中所有字段
    		.select($"device.*")
		
		// 4. 按照需求进行分析
		// step1. 注册DataFrame为临时视图
		etlStreamDF.createOrReplaceTempView("view_temp_iot")
		
		// step2. 编写SQL并执行
		val resultStreamDF: DataFrame = spark.sql(
			"""
			  |SELECT
			  |  deviceType, COUNT(1) AS total, ROUND(AVG(signal), 2) AS avg_signal
			  |FROM
			  |  view_temp_iot
			  |WHERE
			  |  signal > 30
			  |GROUP BY
			  |  deviceType
			  |""".stripMargin)
		
		// 5. 启动流式应用,结果输出控制台
		val query: StreamingQuery = resultStreamDF.writeStream
			// 输出模式
			.outputMode(OutputMode.Complete())
			// 每个微批次输出
    		.foreachBatch{(batchDF: DataFrame, _: Long) =>
			    batchDF.coalesce(1).show(10, truncate = false)
		    }
			.start()
		query.awaitTermination()
		query.stop()
	}
	
}

09-[掌握]-事件时间窗口分析之原理剖析

在Streaming流式数据处理中,按照时间处理数据,其中时间有三种概念:

1)、事件时间EventTime,表示数据本身产生的时间,该字段在数据本身中
2)、注入时间IngestionTime,表示数据到达流式系统时间,简而言之就是流式处理系统接收到数据的时间;
3)、处理时间ProcessingTime,表示数据被流式系统真正开始计算操作的时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-11eStHn8-1620545056109)(/img/image-20210508171538191.png)]

实际项目中往往针对【事件时间EventTime】进行数据处理操作,更加合理化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnUB7bRl-1620545056110)(/img/image-20210508172209942.png)]

基于事件时间窗口分析:
	第一点、按照窗口大小和滑动大小对流式数据进行分组,划分为一个个组(窗口)
	第二点、按照业务,对每个组(窗口)中数据进行聚合统计分析

StructuredStreaming中,窗口代码如何编写呢??
	dataframe.groupBy(
		window("envetTime: orderTime", "1 hour", "1 hour")// 划分窗口,分组
	)

10-[掌握]-事件时间窗口分析之案例演示

​ 修改词频统计程序,数据流包含每行数据以及生成每行行的时间。希望在10分钟的窗口内对单词进行计数,每5分钟更新一次,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-udIV98eA-1620545056110)(/img/image-20210508180157471.png)]

基于事件时间窗口统计有两个参数索引:分组键(如单词)和窗口(事件时间字段)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tAciRXa0-1620545056111)(/img/image-20210508180206911.png)]

​ 为了演示案例,将上述案例中的每5分钟统计最近10分钟窗口改为每5秒统计最近10秒窗口数
据,测试数据集:

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 cn.itcast.spark.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。
 *
 * 测试数据:
	 * 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
 **/
object _06StructuredWindow {
	
	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 etlStreamDF: DataFrame = inputStreamDF
			// 将DataFrame转换为Dataset操作,Dataset是类型安全,强类型
			.as[String]
			.filter(line => null != line && line.trim.split(",").length == 2)
			// 将每行数据进行分割单词: 2019-10-12 09:00:02,cat dog
			// 使用flatMap函数以后 -> (2019-10-12 09:00:02, cat)  ,  (2019-10-12 09:00:02, dog)
			.flatMap{line =>
				val arr = line.trim.split(",")
				arr(1).split("\\s+").map(word => (Timestamp.valueOf(arr(0)), word))
			}
			// 设置列的名称
			.toDF("insert_timestamp", "word")
		
		val resultStreamDF = etlStreamDF
			// TODO:设置基于事件时间(event time)窗口 -> insert_timestamp, 每5秒统计最近10秒内数据
			/*
				1. 先按照窗口分组、2. 再对窗口中按照单词分组、 3. 最后使用聚合函数聚合
			*/
			.groupBy(
				// 先按照窗口分组数据
				window($"insert_timestamp", "10 seconds", "5 seconds"),
				// 在每个窗口内,再按照单词word分组
				$"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()
		query.awaitTermination()
		query.stop()
		
	}
	
}

11-[了解]-事件时间窗口分析之event-time 窗口生成

Structured Streaming中如何依据EventTime事件时间生成窗口的呢?

基于事件时间窗口分析,第一个窗口时间依据第一条流式数据的事件时间EventTime计算得到的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DworkDxy-1620545056111)(/img/image-20210508180425283.png)]

直接查看源码:org.apache.spark.sql.catalyst.analysis.TimeWindowing

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1dkqPWSH-1620545056112)(/img/image-20210508181426553.png)]

12-[掌握]-水位Watermark引入及延迟数据

基于事件时间窗口分析,数据延迟到达,先产生的数据,后到达流式应用系统。

重新运行上面的流式计算程序,当数据延迟达到以后,发现数据会被继续处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3o7MmX2R-1620545056112)(/img/image-20210508181813082.png)]

此时发现应用程序逻辑处理,不合理,存在如下2个问题:
- 问题一:
	延迟的数据,真的有必要在处理吗????
		很多应用场景,都是没有必要处理,延迟性太高,没有实时性

- 问题二:
	实时窗口统计,内存中一直保存所有窗口统计数据,真的有必要吗??
		不需要的,窗口分析:统计的最近数据的状态,以前的状态几乎没有任何作用
	如果流式应用程序运行很久,此时内存被严重消费,性能低下

StructuredStreaming中为了解决上述问题,提供一种机制:Watermark水位线机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dw3BRI3E-1620545056113)(/img/image-20210508182630077.png)]

13-[掌握]-水位Watermark计算及案例演示

如下方式设置阈值Threshold,计算每批次数据执行时的水位Watermark:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v5Vfz0hb-1620545056113)(/img/image-20210508182800640.png)]

看一下官方案例:词频统计WordCount,设置阈值Threshold为10分钟,每5分钟触发执行一次。

package cn.itcast.spark.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秒
			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
 */
object _07StructuredWatermarkUpdate {
	
	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. 针对获取流式DataFrame设置EventTime窗口及Watermark水位限制
		val etlStreamDF: DataFrame = 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")
			
		val resultStreamDF = etlStreamDF
			// 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()
	}
	
}

附录一、创建Maven模块

1)、Maven 工程结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQ7BXquK-1620545056114)(/img/image-20210508110222525.png)]

2)、POM 文件内容

​ Maven 工程POM文件中内容(依赖包):

    <!-- 指定仓库位置,依次为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>
        <!-- Kafka Client 依赖 -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.0.0</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>
        <!-- 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>

${project.basedir}/src/main/resources





org.apache.maven.plugins
maven-compiler-plugin
3.0

1.8
1.8
UTF-8



net.alchim31.maven
scala-maven-plugin
3.2.0



compile
testCompile