Structured Stream-结构化流

Structured Stream-结构化流

概述

是基于Spark SQL引擎构建的可伸缩且容错的流处理引擎,它可以使用SQL操作流处理,像使用SparkSQL操作静态批处理计算一样.SparkSQL引擎负责递增,连续的运行持续到达的流数据并更新最终结果,使用Dataset/DataFrame API 实现对实时数据的聚合 event-time窗口计算以及流到批处理的join操作.最后,系统通过检查点预写日志来确保端到端(end to end)的一次容错保证

简而言之 结构化流提供了快速,可伸缩,容错,端到端的精确一次流处理,该引擎将数据流作为一系列小批量作业进行处理,从而实现了低至100毫秒的端到端延迟以及一次精确的容错保证

预写日志:在状态更新前,通常会将更新的操作做日志

快速入门

  • 引依赖
<dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-sql_2.11</artifactId>
 <version>2.4.5</version>
</dependency>
  • 编写Driver
val spark = SparkSession
  .builder()
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._

//1.创建输入流(表) 动态表
val lines:DataFrame = spark.readStream
  .format("socket")
  .option("host","hbase")
  .option("port",9999)
  .load()

//2.对动态表执行 SQL 查询
val wordCounts:DataFrame = lines.as[String].flatMap(_.split(" "))
  .groupBy("value").count()

//产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
  .outputMode(OutputMode.Update())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()

测试结果

-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache|    2|
| spark|    2|
|hadoop|    4|
+------+-----+

-------------------------------------------
Batch: 2
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
|hello|    1|
|world|    1|
+-----+-----+

Programming Model

Structured Stream中的关键思想是将实时数据流视为被连续追加的表 这导致了一个新的流处理模型 该模型与批处理模型非常相似 使用者将像在静态表上一样将流计算表示为类似于批处理的标准查询 Spark在无界输入表上将其作为增量查询运行

Basic Concepts

Consider the input data stream as the “Input Table”. Every data item that is arriving on the stream is like a new row being appended to the Input Table.

将输入数据流视为"Input Table" 流上到达的每个数据项都像是新Row附加到输入表中

在这里插入图片描述

A query on the input will generate the “Result Table”. Every trigger interval (say, every 1 second), new rows get appended to the Input Table, which eventually updates the Result Table. Whenever the result table gets updated, we would want to write the changed result rows to an external sink.

输入查询将生成"Result Table" 在每个触发间隔(例如,每1秒钟) 新行将附加到输入表中 并最终更新"Result Table"(输入查询是一种持续查询) 无论何时更新"Result Table" 我们都希望将更改后的结果行写入外部接受器

在这里插入图片描述

"Output"定义为写道外部存储器的内容 可以在不同的模式下定义输出:

  • Complete Mode-整个更新的Result Table将被写入外部存储器 由存储连接器决定如何处理整个表的写入
  • Update Mode-筋自上次触发以来在Result Table中已更新的行将被写入外部存储(自Spark2.1.1起可用) 请注意 这与Complete Mode的不同之处在于此模型仅输出自上次触发以来已更改的行

以上两种模式一般用于存在聚合状态算子的计算中 只有在Complete Mode或 Update Mode系统才会存储计算的中间结果 如果使用Update模式中不存在聚合操作 it will be equivalent to Append mode.

  • Append Mode- ⾃上次触发以来,仅将追加在Result Table中的新⾏写⼊到外部存储中。这仅适⽤于预期结果表中现有⾏不会更改的查询(不会使⽤历史数据,做状态更新)

为了说明此模型的⽤法,让我们在上⾯的 快速⼊⻔” 的上下⽂中了解该模型。第⼀⾏DataFrame是输⼊表,最后的wordCounts DataFrame是结果表。请注意,在流lines’DataFrame上⽣成wordCounts的查询与静态DataFrame完全相同。但是,启动此查询时,Spark将连续检Socket连接中是否有新数据。如果有 新数据,Spark将运⾏⼀个“增量”查询,该查询将先前的运⾏计数与新数据结合起来以计算更新的计数, 如下所示

在这里插入图片描述

请注意,Structured Stream不会存储Input Table。它从流数据源读取最新的可⽤数据,对其进⾏增量处 理以更新结果,然后丢弃该源数据。它仅保留更新结果所需的最⼩中间状态数据(例如,前⾯示例中的中间计数)此模型与许多其他流处理引擎明显不同 许多流系统要求⽤户⾃⼰维护运⾏中的聚合,因此必须考虑容 错和数据⼀致性(⾄少⼀次,最多⼀次或精确⼀次) 在此模型中 Spark负责在有新数据时更新结果 表,从⽽使⽤户免于推理 系统通过 检查点 和 预写⽇志 来确保端到端(end to end)的精准⼀次容错保证

  • 最多一次:如果计算过程中 存在失败 自动忽略 一般只考虑性能 不考虑安全(丢数据)
  • 至少一次:在故障时候 系统因为重试或者重新计算 导致记录重复参与计算(不丢数据 重复更新 计算不准确)
  • 精确一次:在故障时候 系统因为重试或者重新计算 不会导致数据丢失或者重复计算(对数据精确性要求比较高的实时计算场景)

Fault Tolerance Semantics-面试

提供端到端的精确一次语义是结构化流设计背后的主要目标之一 为此 我们设计了结构化流源 接收器执行引擎 以可靠地跟踪处理的确切进度 以便它可以通过重新启动和/或重新处理来处理任何类型的故障 假定每个流源都有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟着流中的读取位置 引擎使用检查点预写日志来记录每个触发器中正在处理的数据的偏移范围 流接收器被设计为是幂等的 用于处理后处理 结合使用可重播的源幂等的接收器 结果化流可以确保发生任何故障时端到端的一次精确语义

处理时间时间和延迟数据

事件时间是嵌入数据本身的时间 对于许多应用程序 你可能希望在此事件时间进行操作 例如 如果要获取每分钟由IOT设备生成的事件数 则可能要使用生成数据的时间(即数据中的事件时间) 而不是Spark收到的时间 事件时间在此模型中非常自然地表示-设备中的每个事件是表中的一行 而事件时间是该行中的列值 这样一来 基于窗口的聚合(例如 每分钟的事件数) 就可以成为事件时间列上一种特殊的分组和聚合类型-每个时间窗口都是一个组 每行可以属于多个窗口/组 因此 可以在静态数据集(例如从收集的设备时间日志中)以及在数据流上一致地定义这样的基于事件-时间-窗口的聚合查询

此外 此模型自然会根据事件时间处理比预期晚到达的数据 由于Spark正在更新结果表 因此它具有完全控制权 可以在有较晚数据时更新旧聚合 并可以清楚旧聚合以限制中间状态数据的大小 从Spark2.1开始 我们支持水印功能 该功能允许用户指定最新数据的阈值 并允许引擎相应地清楚旧状态 这些将在后面地窗口操作部分中详细介绍

Dataset和DataFrame API

Since Spark 2.0 DataFrame和Dataset可以表示静态的有界数据以及流式无界数据 与静态Dataset/DataFrame类似 可以使用公共入口的SparkSession从流源Dataset/DataFrame 并对它们应用 与静态Dataset/DataFrame相同操作 可以通过SparkSession.readStream()返回的DataStreamReader接⼝(Scala)创建流式DataFrame

Input Sources

File source(支持故障容错)

将写入目录中的文件作为数据流读取。支持的文件格式有文本,csv, json, orc, parquet

准备数据-上传到hdfs上

name,age,salary,sex,job,deptNo
Michael,29,20000.0,true,MANAGER,1
Andy,30,15000.0,true,SALESMAN,1
Justin,19,8000.0,true,CLERK,1
val spark = SparkSession
  .builder()
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._

//name,age,salary,sex,depname,dept
val userSchema = new StructType()
  .add("name", StringType)
  .add("age",IntegerType)
  .add("salary", DoubleType)
  .add("sex", BooleanType)
  .add("depname", StringType)
  .add("dept", IntegerType)

//1.创建输入流(表)
val lines:DataFrame = spark.readStream
  .schema(userSchema)
  .option("sep",",")
  .option("header",true)
  .csv("hdfs://hbase:9000/results/csv")

//3.产生StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Update())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
-------------------------------------------
Batch: 0
-------------------------------------------
+------+---+-------+-----+--------+----+
|  name|age| salary|  sex| depname|dept|
+------+---+-------+-----+--------+----+
| chael| 29|20000.0| true| MANAGER|   1|
|  Andy| 30|15000.0| true|SALESMAN|   1|
|Justin| 19| 8000.0| true|   CLERK|   1|
| Kaine| 20|20000.0| true| MANAGER|   2|
|  Lisa| 19|18000.0|false|SALESMAN|   2|
+------+---+-------+-----+--------+----+

Socket Source(不支持故障容错)

-用于测试

//1.创建输入流(表)
val lines:DataFrame = spark.readStream
  .format("socket")
  .option("host","hbase")
  .option("port",9999)
  .load()
//2.对动态表执行 SQL 查询
val wordCounts:DataFrame = lines.as[String].flatMap(_.split(" "))
  .groupBy("value").count()

//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
  .outputMode(OutputMode.Update())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
[root@hbase ~]# nc -lk 9999
spark hadoop kafka
hive hbase

-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
| hbase|    1|
| kafka|    1|
| spark|    1|
|hadoop|    1|
|  hive|    1|
+------+-----+

Rate Source(支持故障容错)

以每秒指定的行数生成数据 每个输出行包含一个时间戳和一个值 其中timestamp是包含消息分发时间的Timestamp类型 而值是包含消息计数的Long类型 从第一行的0开始 此源只在进行测试和基准测试

//1.创建输入流(表)
val lines:DataFrame = spark.readStream
  .format("rate")
  .option("rowsPerSecond","1000")
  .load()

//3.产生StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
-------------------------------------------
Batch: 1
-------------------------------------------
+--------------------+-----+
|           timestamp|value|
+--------------------+-----+
|2020-03-02 21:47:...|    0|
|2020-03-02 21:47:...|    4|
|2020-03-02 21:47:...|    8|

Kafka Source(支持故障恢复)-重要

参考:http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html

引依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql-kafka-0-10_2.11</artifactId>
    <version>2.4.5</version>
</dependency>
//1.创建输入流(表)
val lines:DataFrame = spark.readStream
   .format("kafka")
   .option("kafka.bootstrap.servers","hbase:9092")
   .option("subscribe","topic03")
   .load()

//2.对动态表执行 SQL 查询
lines.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") //对值做类型转换
  .as[(String,String)]
//3.产生StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Append())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
+----+-------------+-------+---------+------+--------------------+-------------+
| key|        value|  topic|partition|offset|           timestamp|timestampType|
+----+-------------+-------+---------+------+--------------------+-------------+
|null|[74 68 69 73]|topic03|        0|     4|2020-03-02 22:32:...|            0|
+----+-------------+-------+---------+------+--------------------+-------------+

Basic Operations

在这里插入图片描述

设备 设备类型 信号量 时间
BYQ-001,BY,1000,2020-03-02 14:56:00
BYQ-002,BY,1000,2020-03-02 14:56:00
case class DeviceData(device: String, deviceType: String, signal: Double, time: String) {}
val spark = SparkSession
  .builder()
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
//1.创建输入流(表)
val lines:DataFrame = spark.readStream
   .format("kafka")
   .option("kafka.bootstrap.servers","hbase:9092")
   .option("subscribePattern","topic.*")
   .load()

//2.对动态表执行 SQL 查询
  .selectExpr("CAST(value AS STRING)") //对值做类型转换
  .as[String].map(line=>{
  var ts = line.split(",")
  DeviceData(ts(0),ts(1),ts(2).toDouble,ts(3))
}).toDF()
  .groupBy("deviceType","device")
  .mean("signal")

//3.产生StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
  • DataFream SQL
val spark = SparkSession
  .builder()
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
//1.创建输入流(表)
val lines:DataFrame = spark.readStream
   .format("kafka")
   .option("kafka.bootstrap.servers","hbase:9092")
   .option("subscribePattern","topic.*")
   .load()

//2.对动态表执行 SQL 查询
lines.selectExpr("CAST(value AS STRING)") //对值做类型转换
  .as[String].map(line=>{
  var ts = line.split(",")
  DeviceData(ts(0),ts(1),ts(2).toDouble,ts(3))
}).toDF().createOrReplaceTempView("t_device")

var sql =
"""
   select device,deviceType,avg(signal)
   from t_device
   group by deviceType,device

"""
val results = spark.sql(sql)

//3.产生StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()

//等待流计算关闭
query.awaitTermination()
-------------------------------------------
+----------+-------+-----------------+
|deviceType| device|      avg(signal)|
+----------+-------+-----------------+
|        BY|BYQ-002|            800.0|
|        BY|BYQ-001|933.3333333333334|
+----------+-------+-----------------+

Output Sinks

File sink(Append)

将输出存储到目录

//3.产生StremQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Append())
  .format("csv")
  .option("seq",",")
  .option("header","true") //去除表头
  .option("inferSchema","true")
  .option("path","hdfs://hbase:9000/structured/json")
  //精准一次性容错需要设置检查点目录
  .option("checkpointLocation","hdfs://hbase:9000/structured-checkpoint")
  .start()

在这里插入图片描述

注意:File Sink只允许用在Append模式 并且支持精准一次的写入

Kafka sink(Append,Update,Complete)-重要

参考:http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html

将输出存储到Kafka中的一个或多主题 在这里 我们描述了将流查询和批查询写入Apache Kafka的支持 请注意 Apache Kafka仅支持至少一次写入语义 因此 在向Kafka写入流查询或批处理查询时 某些记录可能会重复 例如 如果Kafka需要重试Broker未确认的消息(即使Broker已经收到并写⼊了消 息记录) 就会发生这种情况 由于这些Kafka写入语义 结构化流无法阻止此类重复项的发生 写入kafka的dataframe在构架中包含以下几例:

Column Type
key(optional) string or binary
value(required) string or binary
topic(*optional) string

如果未指定topic配置选项 则topic列为必填项

-引依赖

<dependency>
 <groupId>org.apache.spark</groupId>
 <artifactId>spark-sql-kafka-0-10_2.11</artifactId>
 <version>2.4.5</version>
</dependency>
val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

// 1,zhangsan,1,4.5
val inputs = spark.readStream
  .format("socket")
  .option("host", "hbase")
  .option("port", "9999")
  .load()
import org.apache.spark.sql.functions._
val lines = inputs.as[String].map(_.split(","))
  .map(ts => (ts(0).toInt, ts(1), ts(2).toInt * ts(3).toDouble))
  .toDF("id", "name", "cost")
  .groupBy("id", "name")
  .agg(sum("cost") as "cost" )
  .as[(Int,String,Double)]
  .map(t=>(t._1+":"+t._2,t._3+""))
  .toDF("key","value")  //必须保证出现key,value字段

//3.产⽣StreamQuery对象
val query:StreamingQuery = lines.writeStream
  .outputMode(OutputMode.Update())
  .format("kafka")
  .option("checkpointLocation", "hdfs://hbase:9000/structured-checkpoints-kafka")
  .option("kafka.bootstrap.servers", "hbase:9092")
  .option("topic", "topic01")//覆盖DF中topic字段
  .start()

//4.等待流计算关闭
query.awaitTermination()

必须保证写入的DF中仅仅含有value字符串类型的字段key可选 如果没有系统认为是null topic可选 前提是必须在option中配置topic 否则必须出现在字段列中

Console sink(for debugging)

每次有触发器时 将输出打印到控制台/stdout 支持追加和完整输出模式 由于每次触发后都会收集全部输出并将其存储在驱动程序的内存中 因此应在数量较小时用于调试目的

writeStream
.outputMode(OutputMode.Update())
.format("console")
.option("numRows", "2")
.option("truncate", "true")
.start()

Memory sink(for debugging)

输出作为内存表存储在内存中 支持追加和完整输出模式 当整个输出被收集并存储在驱动程序的内存中时 应将其用于调试低数据量的目的 因此 请谨慎使用

writeStream
 .outputMode(OutputMode.Complete())
 .format("memory")
 .queryName("t_word")
 .start()
new Thread(){
 override def run(): Unit = {
 while(true){
 Thread.sleep(1000)
 spark.sql("select * from t_word").show()
 }
 }
}.start()

Foreach|ForeachBatch sink

对输出中的记录运行任意输出 使用foreach和foreachBacth操作 您可以在流查询的输出上应用任意操作并编写逻辑 它们的用例略有不同-虽然foreach允许在每一行使用自定义逻辑 但是foreachBatch允许在每个微批处理的输出上进行任意操作和自定义逻辑

ForeachBatch

foreachBatch(…)允许您指定在流查询的每个微批处理的输出数据上执⾏的函数

writeStream
 .outputMode(OutputMode.Complete())
 .foreachBatch((ds,batchID)=>{
 	ds.write //使⽤静态批处理的API
 	.mode(SaveMode.Overwrite).format("json")
 	.save("hdfs://CentOS:9000/results/structured-json")
 })
 .start()

Foreach

class WordCountWriter extends ForeachWriter[Row]{
 override def open(partitionId: Long, epochId: Long): Boolean = {
 // println("打开了链接")
 true //执⾏process
 }
 override def process(value: Row): Unit = {
 var Row(word,count)=value
 println(word,count)
 }
 override def close(errorOrNull: Throwable): Unit = {
 // println("释放资源")
 }
}
writeStream
 .outputMode(OutputMode.Complete())
 .foreach(new WordCountWriter)
 .start()

Window Operations

val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

//单词 时间戳
val lines = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()

var words = lines.as[String]
  .map(line=>line.split(","))
  .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
  .toDF("word","timestamp")

import org.apache.spark.sql.functions._
var wordCounts= words.groupBy(window($"timestamp", "4 seconds", "2 seconds"),
  $"word")
    .count()
    .map(row=>{
      var start = row.getStruct(0).getTimestamp(0)
      var end = row.getStruct(0).getTimestamp(1)

      var word = row.getString(1)
      var count = row.getLong(2)

      val sdf = new SimpleDateFormat("HH:mm:ss")
      (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
    })
    .toDF("start","end","word","count")

//3.产生query对象
val query:StreamingQuery = wordCounts.writeStream
    .outputMode(OutputMode.Complete())
    .format("console")
    .start()

//4.等待流计算关闭
query.awaitTermination()
-------------------------------------------
Batch: 1
-------------------------------------------
+--------+--------+----+-----+
|   start|     end|word|count|
+--------+--------+----+-----+
|13:08:36|13:08:40|   a|    1|
|13:08:34|13:08:38|   a|    1|
+--------+--------+----+-----+

快速入门

Late Data & Watermaking-难点 面试

To enable this, in Spark 2.1, we have introduced watermarking, which lets the engine automatically track the current event time in the data and attempt to clean up old state accordingly. You can define the watermark of a query by specifying the event time column and the threshold on how late the data is expected to be in terms of event time. For a specific window ending at time T, the engine will maintain state and allow late data to update the state until (max event time seen by the engine - late threshold > T) 在Spark2.1版本引入watermarkering概念 用于告知计算节点 何时丢弃窗口聚合状态 因为流计算是一个长时间运行任务 系统不可能无限制存储一些过久的状态值 使用watermarkering机制 系统可以删除那些过期的状态数据 用于释放内存 每个触发的窗口都有start timeend time属性 计算引擎会保留计算引擎所看到最大event time

watermark时间 = max event time seen by the engine -late threshold

如果watermarker时间T>窗口的end time时间T 则认为该窗口的计算状态可以丢弃

注意:引入watermarker以后 用户只能使用update append模式 系统才会删除过期数据

update-水位线没有没过窗口end time之前 如果有数据落入到该窗口 该窗口会重复触发

val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

val lines = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()
import org.apache.spark.sql.functions._

var words = lines.as[String]
    .map(line=>line.split(","))
    .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
    .toDF("word","timestamp")

var wordCounts = words
    .withWatermark("timestamp","1 second")
    .groupBy(window($"timestamp","2 seconds","2 seconds"),$"word")
    .count()
    .map(row =>{
      //获取window对象
      var start = row.getAs[Row]("window").getAs[Timestamp]("start")
      var end = row.getAs[Row]("window").getAs[Timestamp]("end")
      //获取word字段
      var word = row.getAs[String]("word")
      //获取计数
      var count = row.getAs[Long]("count")

      val sdf = new SimpleDateFormat("HH:mm:ss")

      (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
    })
    .toDF("start","end","word","count")

//3.产生streamquery对象
val query:StreamingQuery = wordCounts.writeStream
    .outputMode(OutputMode.Update())
    .format("console")
    .start()

//4.等待流计算关闭
query.awaitTermination()
-水位线23 水位线超过最后时间数据删除
-------------------------------------------
Batch: 5
-------------------------------------------
+--------+--------+----+-----+
|   start|     end|word|count|
+--------+--------+----+-----+
|18:58:24|18:58:26|   a|    3|
+--------+--------+----+-----+

-------------------------------------------
Batch: 15
-------------------------------------------
+--------+--------+----+-----+
|   start|     end|word|count|
+--------+--------+----+-----+
|19:00:00|19:00:02|   b|    2|
+--------+--------+----+-----+

**Append-**水位线没有没过窗口的end time之前 如果有数据落入到该窗口 该窗口不会触发 只会默默的计算 只有当水位线没过窗口end time的时候 才会做出最终输出

val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

val lines = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()
import org.apache.spark.sql.functions._

var words = lines.as[String]
    .map(line=>line.split(","))
    .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
    .toDF("word","timestamp")

var wordCounts = words
    .withWatermark("timestamp","1 second")
    .groupBy(window($"timestamp","2 seconds","2 seconds"),$"word")
    .count()
    .map(row =>{
      //获取window对象
      var start = row.getAs[Row]("window").getAs[Timestamp]("start")
      var end = row.getAs[Row]("window").getAs[Timestamp]("end")
      //获取word字段
      var word = row.getAs[String]("word")
      //获取计数
      var count = row.getAs[Long]("count")

      val sdf = new SimpleDateFormat("HH:mm:ss")

      (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
    })
    .toDF("start","end","word","count")

//3.产生streamquery对象
val query:StreamingQuery = wordCounts.writeStream
    .outputMode(OutputMode.Append())
    .format("console")
    .start()

//4.等待流计算关闭
query.awaitTermination()
a,1583238621367
a,1583238631367
a,1583238641367

-------------------------------------------
Batch: 4
-------------------------------------------
+--------+--------+----+-----+
|   start|     end|word|count|
+--------+--------+----+-----+
|20:30:20|20:30:22|   a|    1|
+--------+--------+----+-----+

-------------------------------------------
Batch: 5
-------------------------------------------
+-----+---+----+-----+
|start|end|word|count|
+-----+---+----+-----+
+-----+---+----+-----+

-------------------------------------------
Batch: 6
-------------------------------------------
+--------+--------+----+-----+
|   start|     end|word|count|
+--------+--------+----+-----+
|20:30:30|20:30:32|   a|    1|
+--------+--------+----+-----+

Semantic Guarantees of Aggregation with Watermarking

  • 水位延迟(使用withWatermark设置)为 2小时 可确保引擎永远不会丢弃任何小于2小时的数据 换句话说 任何在此之前处理的最新数据比时间时间少2小时(以时间时间计) 的数据都可以保证得到汇总
  • 但是 保证仅在一个方向上严格 延迟超过2小时的数据不能保证被删除 它可能会或可能不会聚合 数据延迟更多 引擎处理数据的可能性越小

Join Operations

自Spark2.0Structured Streaming引入的join的概念(inner和一些外连接) 支持和静态或者动态的Dataset/DataFrame做join操作

Stream-static Joins

//创建spark对象
val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
  .toDF("id", "name")

// 1 apple 1 4.5
val lineDF = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()


var orderItemDF = lineDF.as[String]
    .map(t=>{
      val tokens = t.split("\\s+")
      (tokens(0).toInt,tokens(1),(tokens(2).toInt) * (tokens(3).toDouble))
    }).toDF("uid","item","cost")

import org.apache.spark.sql.functions._

var joinDF=orderItemDF
	.join(userDF,$"id"===$"uid")
    .groupBy("uid","name")
 	.agg(sum("cost").as("total_cost"))

//3.产生streamquery对象
val query:StreamingQuery = joinDF.writeStream
    .outputMode(OutputMode.Update())
    .format("console")
    .start()

//4.等待流计算关闭
query.awaitTermination()
1 apple 1 4.5
1 apple 1 4.5
2 pear 1 6.8
3 banana 2 4.0

-------------------------------------------
Batch: 1
-------------------------------------------
+---+----+----------+
|uid|name|total_cost|
+---+----+----------+
|  1|  zs|       4.5|
+---+----+----------+

-------------------------------------------
Batch: 2
-------------------------------------------
+---+----+----------+
|uid|name|total_cost|
+---+----+----------+
|  1|  zs|       9.0|
+---+----+----------+

-------------------------------------------
Batch: 3
-------------------------------------------
+---+----+----------+
|uid|name|total_cost|
+---+----+----------+
|  2|lisi|       6.8|
+---+----+----------+

**注意:**stream-static joins并不是状态的 所以不需要做状态管理 目前还有一些外连接还不支持 目前只支持innerleft outer

Stream-stream Joins

在Spark-2.3添加了streaming-streaming的支持 实现两个流的join最大的挑战是在于找到一个时间点实现两个流的join 因为这两个流都没有结束 任意 一个接受的流可以匹配另一个流中即将被接受的数据 所以在任意一个流中我们需要接收并将这些数据进行缓存 然后作为当前stream的状态 然后去匹配另外一个流的后续接收数据 继而生成相应的join的结果集 和Streaming的聚合类似我们使用watermarker处理late 乱序的数据 限制状态的使用 Inner Joins with optional Watemarking

内连接可以使用任意一些column作为连接条件 然而在stream计算开始运行的时候 流计算的状态会持续地增长 因为必须存储所有传递过来地状态数据 然后和后续地新接收地数据做匹配 为了避免无限制地状态存储 一般需要定义额外地join条件 例如限制一些old数据如果和新数据时间间隔大于某个阈值就不能匹配 因此可以删除这些陈旧地状态 简单来说需要以下步骤

  • 两边流计算需要定义watermarker延迟 这样系统可以知道两个流的时间差值
  • 定制以下event time地限制条件 这样引擎可以计算出那些数据old地不再需要了 可以使用以下两种方式定制
    • 时间范围界定例如:JOIN ON leftTime BETWEEN rightTime AND rightTime+INTERVAL 1 HOUR
    • 基于Event-time Window例如JOIN ON leftTimeWindow = rightTimeWindow
案例1(Range)
//创建spark对象
val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
  .toDF("id", "name")

//001 apple 1 4.5 1566113401000
//创建一个dataframe
val orderDF = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()
  .map(row =>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),(t(2).toInt * t(3).toDouble),new Timestamp(t(4).toLong)))
  .toDF("uid","item","cost","order_time")

//001 zhangsan 1566113400000
val useDF = spark
  .readStream
  .format("socket")
  .option("host", "hbase")
  .option("port", 8888)
  .load()
  .map(row=>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),new Timestamp(t(2).toLong)))
  .toDF("id","name","login_time")

import org.apache.spark.sql.functions._

//系统分别会对 user 和 order 缓存 最近1秒和2秒数据 一旦时间过去 系统就无法保证数据状态继续保留
val loginWatermarker=useDF.withWatermark("login_time","1 second")
val orderWatermarker = orderDF.withWatermark("order_time","2 second")

//计算订单地时间&用户 登陆之后地0-1小时 关联数据并且join
val joinDF = loginWatermarker.join(orderWatermarker,
  expr("uid=id and order_time >= login_time and order_time <= login_time + interval 1 second")
)

//创建query对象
val query=joinDF.writeStream
    .format("console")
    .outputMode(OutputMode.Append())
    .start()


//4.等待流计算关闭
query.awaitTermination()
001 apple 1 4.5 1566113401000
001 zhangsan 1566113400000

-------------------------------------------
Batch: 2
-------------------------------------------
+---+--------+-------------------+---+-----+----+-------------------+
| id|    name|         login_time|uid| item|cost|         order_time|
+---+--------+-------------------+---+-----+----+-------------------+
|001|zhangsan|2019-08-18 15:30:00|001|apple| 4.5|2019-08-18 15:30:01|
+---+--------+-------------------+---+-----+----+-------------------+
案例2(event time window)
//创建spark对象
val spark = SparkSession
  .builder
  .appName("FileSink")
  .master("local[*]")
  .getOrCreate()

import spark.implicits._

spark.sparkContext.setLogLevel("ERROR")

val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
  .toDF("id", "name")

import org.apache.spark.sql.functions._

//001 apple 1 4.5 1566113401000
//创建一个dataframe
val orderDF = spark.readStream
    .format("socket")
    .option("host","hbase")
    .option("port",9999)
    .load()
  .map(row =>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),(t(2).toInt * t(3).toDouble),new Timestamp(t(4).toLong)))
  .toDF("uid","item","cost","order_time")
  .withColumn("leftWindow",window($"order_time","5 seconds"))

//001 zhangsan 1566113400000
val useDF = spark
  .readStream
  .format("socket")
  .option("host", "hbase")
  .option("port", 8888)
  .load()
  .map(row=>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),new Timestamp(t(2).toLong)))
  .toDF("id","name","login_time")
  .withColumn("rightWindow",window($"login_time","5 seconds"))


//系统分别会对 user 和 order 缓存 最近1秒和2秒数据 一旦时间过去 系统就无法保证数据状态继续保留
val loginWatermarker=useDF.withWatermark("login_time","1 second")
val orderWatermarker = orderDF.withWatermark("order_time","2 second")

//计算订单地时间&用户 登陆之后地0-1小时 关联数据并且join
val joinDF = loginWatermarker.join(orderWatermarker,
  expr("uid=id and leftWindow = rightWindow")
)

//创建query对象
val query=joinDF.writeStream
    .format("console")
    .outputMode(OutputMode.Append()) //仅支持Append模式输出
    .start()


//4.等待流计算关闭
query.awaitTermination()
002 lisi 1566113400000
002 pear 2 6.5 1566113401000

-------------------------------------------
Batch: 1
-------------------------------------------
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
| id|name|         login_time|         rightWindow|uid|item|cost|         order_time|          leftWindow|
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
|002|lisi|2019-08-18 15:30:00|[2019-08-18 15:30...|002|pear|13.0|2019-08-18 15:30:01|[2019-08-18 15:30...|
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
derDF.withWatermark("order_time","2 second")

//计算订单地时间&用户 登陆之后地0-1小时 关联数据并且join
val joinDF = loginWatermarker.join(orderWatermarker,
  expr("uid=id and leftWindow = rightWindow")
)

//创建query对象
val query=joinDF.writeStream
    .format("console")
    .outputMode(OutputMode.Append()) //仅支持Append模式输出
    .start()


//4.等待流计算关闭
query.awaitTermination()
002 lisi 1566113400000
002 pear 2 6.5 1566113401000

-------------------------------------------
Batch: 1
-------------------------------------------
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
| id|name|         login_time|         rightWindow|uid|item|cost|         order_time|          leftWindow|
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
|002|lisi|2019-08-18 15:30:00|[2019-08-18 15:30...|002|pear|13.0|2019-08-18 15:30:01|[2019-08-18 15:30...|
+---+----+-------------------+--------------------+---+----+----+-------------------+--------------------+
发布了15 篇原创文章 · 获赞 2 · 访问量 439

猜你喜欢

转载自blog.csdn.net/weixin_45106430/article/details/104928770