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)
/* ============================= 基于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)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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个问题:
- 问题一:
延迟的数据,真的有必要在处理吗????
很多应用场景,都是没有必要处理,延迟性太高,没有实时性
- 问题二:
实时窗口统计,内存中一直保存所有窗口统计数据,真的有必要吗??
不需要的,窗口分析:统计的最近数据的状态,以前的状态几乎没有任何作用
如果流式应用程序运行很久,此时内存被严重消费,性能低下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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