Structured Stream结构化流

Structured Stream


概述

Structured Stream是基于Spark SQL引擎构建的可伸缩且容错的流处理引擎。使得⽤户可以像使⽤Spark SQL操作静态批处理计算⼀样使⽤Structured Stream的SQL操作流计算。当流数据继续到达时,Spark SQL引擎将负责递增地,连续地运⾏它并更新最终结果。使⽤ 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

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

object WorldCountTest {
    def main(args: Array[String]): Unit = {
        //创建SparkSession
        val spark = SparkSession
        .builder()
        .master("local[*]")
        .appName("WorldCountTest")
        .getOrCreate()
        //导包
        spark.sparkContext.setLogLevel("ERROR")
        import spark.implicits._
        //创建输入流
        val lines:DataFrame = spark.readStream
        .format("socket")
        .option("host", "centos")
        .option("port", "9999")
        .load()
        //流转换
        val result:DataFrame = lines.as[String].flatMap(_.split(" "))
        .groupBy("value")
        .count()
        //将结束流输出
        val query = result.writeStream
        .outputMode(OutputMode.Complete())
        .format("console")
        .start()
        query.awaitTermination()
    }
}

引入日志文件

log4j.rootLogger = FATAL,stdout
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} %c %m%n

Programming Model


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

Basic Concepts

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

输⼊查询将⽣成“Result Table”。在每个触发间隔(例如,每1秒钟),新⾏将附加到输⼊表中,并最终更新“Result Table”(输⼊查询是⼀种持续查询)。⽆论何时更新“Result Table”,我们都希望将更改后的结果⾏写⼊外部接收器。

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

Complete Mode - 整个更新的Result Table将被写⼊外部存储器。由存储连接器决定如何处理整个表的写⼊。

Update Mode - 仅⾃上次触发以来在Result Table中已更新的⾏将被写⼊外部存储(⾃Spark 2.1.1起可⽤)。请注意,这与Complete Mode 的不同之处在于此模式仅输出⾃上次触发以来已更改的⾏。

以上两种模式⼀般⽤于存在聚合(状态)算⼦的计算中。因为只有在Complete Mode 或者Update Mode系统才会存储计算的中间结果。如果使⽤Update模式中不存在聚合操作,该模式等价于Append模式。

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

第⼀⾏DataFrame是输⼊表,最后的wordCounts DataFrame是结果表。请注意,在流lines DataFrame上⽣成wordCounts的查询与静态DataFrame完全相同。但是,启动此查询时,Spark将连续检查Socket连接中是否有新数据。如果有新数据,Spark将运⾏⼀个“增量”查询,该查询将先前的运⾏计数与新数据结合起来以计算更新的计数。

请注意,Structured Stream不会存储Input Table。它从流数据源读取最新的可⽤数据,对其进⾏增量处理以更新结果,然后丢弃该源数据。它仅保留更新结果所需的最⼩中间状态数据(例如,前⾯示例中的中间计数)。

此模型与许多其他流处理引擎明显不同。许多流系统要求⽤户⾃⼰维护运⾏中的聚合,因此必须考虑容错和数据⼀致性(⾄少⼀次,最多⼀次或精确⼀次)。在此模型中,Spark负责在有新数据时更新结果表,从⽽使⽤户免于推理。系统通过 检查点 和 预写⽇志 来确保端到端(end to end)的精准⼀次容错保证。

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

容错语义

提供端到端的精确⼀次语义是结构化流设计背后的主要⽬标之⼀。

为此,我们设计了 结构化流源 , 接收器 和 执⾏引擎 ,以可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。

假定每个 流源 都有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。引擎使⽤ 检查点 和 预写⽇志 来记录每个触发器中正在处理的数据的偏移范围。流接收器被设计为是 幂等 的,⽤于处理后处理。结合使⽤ 可重播的源 和 幂等的接收器 ,结构化流可以确保在发⽣任何故障时端到端的⼀次精确语义。

Dataset和DataFrame API

从Spark 2.0开始,DataFrame和Dataset可以表示静态的有界数据以及流式⽆界数据。与静态Dataset/DataFrame类似,您可以使⽤公共⼊⼝点SparkSession从流源Dataset/DataFrame,并对它们应⽤与静态Dataset/DataFrame相同的操作。可以通过SparkSession.readStream()返回的DataStreamReader接⼝(Scala / Java / Python⽂档)创建流式DataFrame

Input Sources

File source (⽀持故障容错)

读取写⼊⽬录的⽂件作为数据流。⽀持的⽂件格式为text,csv,json,orc,parquet。

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
//name,age,salary,sex,job,deptNo
val userSchema = new StructType()
 .add("name", StringType)
 .add("age", IntegerType)
 .add("salary", DoubleType)
 .add("sex", BooleanType)
 .add("job", StringType)
 .add("deptNo", IntegerType)
 //1.创建输⼊流(表)
 val lines:DataFrame = spark.readStream
 .schema(userSchema)
 .option("sep", ",")
 .option("header", "true")           //表头
 .csv("hdfs://CentOS:9000/results/csv")

Socket Source(不⽀持故障容错)

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

Rate Source(⽀持故障容错)

以每秒指定的⾏数⽣成数据,每个输出⾏包含⼀个时间戳和⼀个值。其中timestamp是包含消息分发时间的Timestamp类型,⽽值是包含消息计数的Long类型,从第⼀⾏的0开始。此源旨在进⾏测试和基准测试。

val rate:DataFrame = spark.readStream
 .format("rate")
 .option("rowsPerSecond","1000")
 .load()

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>
val lines:DataFrame = spark.readStream
 .format("kafka")
 .option("kafka.bootstrap.servers", "CentOS:9092")
 .option("subscribe", "topic01")    //option("subscribePattern", "topic.*")
 .load()
 .selectExpr("CAST(key AS STRING)","CAST(value AS STRING)","partition","offset")
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()
 import spark.implicits._
 spark.sparkContext.setLogLevel("ERROR")
 //从kafka中读取数据流
 val inputs:DataFrame = spark.readStream
 .format("kafka")
 .option("kafka.bootstrap.servers", "CentOS:9092")
 .option("subscribePattern", "topic.*")
 .load()
 .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 = inputs.writeStream
 .outputMode(OutputMode.Complete())
 .format("console")
 .start()
 query.awaitTermination()

也可直接使用 SQL 语句直接查询动态流数据中的内容 (相对上面方法来说更好理解)

val spark = SparkSession
 .builder
 .appName("StructuredNetworkWordCount")
 .master("local[*]")
 .getOrCreate()
 import spark.implicits._
 spark.sparkContext.setLogLevel("ERROR")
 //从kafka队列中读数据
 spark.readStream
 .format("kafka")
 .option("kafka.bootstrap.servers", "CentOS:9092")
 .option("subscribePattern", "topic.*")
 .load()
 .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 = results.writeStream
 .outputMode(OutputMode.Complete())
 .format("console")
 .start()
 query.awaitTermination()

结果

+-------+----------+-----------+
| device|deviceType|avg(signal)|
+-------+----------+-----------+
|BYQ-002| BY| 4500.0|
Output Sinks

File sink (Append)

将数据输出到目录

result.writeStream
 .outputMode(OutputMode.Append())
 .format("csv")
 .option("sep", ",")
 .option("header", "true")
 .option("inferSchema", "true")
 .option("path", "hdfs://CentOS:9000/structured/json")
 .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints")
 .start()

注意:File Sink只允许⽤在Append模式,并且⽀持精准⼀次的写⼊。

Kafka sink(Append, Update, Complete)

将输出存储到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>
result.writeStream
 .outputMode(OutputMode.Update())
 .format("kafka")
 .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints-kafka")
 .option("kafka.bootstrap.servers", "CentOS:9092")
 .option("topic", "topic01")//覆盖DF中topic字段
 .start()

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

完整案例

// 1,zhangsan,1,4.5    
val inputs = spark.readStream                //从 nc -lk 9999中读取数据流
                 .format("socket")
                 .option("host", "CentOS")
                 .option("port", "9999")
                 .load()
 import org.apache.spark.sql.functions._
 val results = 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 = results.writeStream
             .outputMode(OutputMode.Update())
             .format("kafka")
             .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints-kafka")
             .option("kafka.bootstrap.servers", "CentOS:9092")
             .option("topic", "topic01")//覆盖DF中topic字段
             .start()

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

Console sink (for debugging)

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

result.writeStream
     .outputMode(OutputMode.Update())
     .format("console")
     .option("numRows", "2")    //最多显示两行
     .option("truncate", "true")//是否截断数据
     .start()

Memory sink (for debugging)

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

result.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和foreachBatch操作,您可以在流查询的输出上应⽤任意操作并编写逻辑。它们的⽤例略有不同-虽然foreach允许在每⼀⾏上使⽤⾃定义写逻辑,但是foreachBatch允许在每个微批处理的输出上进⾏任意操作和⾃定义逻辑。

ForeachBatch

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

result.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 = {
        //value为类的对象,含有word和count属性,反解类对象Row,并为word和count赋值
        var Row(word,count)=value
        println(word,count)
    }
    override def close(errorOrNull: Throwable): Unit = {
        // println("释放资源")
    }
}
result.writeStream
 .outputMode(OutputMode.Complete())
 .foreach(new WordCountWriter)
 .start()

处理事件时间和延迟数据

事件时间是嵌⼊数据本身的时间。对于许多应⽤程序,您可能希望在此事件时间进⾏操作。例如,如果要获取每分钟由IoT设备⽣成的事件数,则可能要使⽤⽣成数据的时间(即数据中的事件时间),⽽不是Spark收到的时间。事件时间在此模型中⾮常⾃然地表示-设备中的每个事件是表中的⼀⾏,⽽事件时间是该⾏中的列值。这样⼀来,基于窗⼝的聚合(例如,每分钟的事件数)就可以成为事件时间列上⼀种

特殊的分组和聚合类型-每个时间窗⼝都是⼀个组,每⾏可以属于多个窗⼝/组。因此,可以在静态数据集(例如从收集的设备事件⽇志中)以及在数据流上⼀致地定义这样的基于事件-时间-窗⼝的聚合查询。

此外,此模型⾃然会根据事件时间处理⽐预期晚到达的数据。由于Spark正在更新结果表,因此它具有完全控制权,可以在有较晚数据时更新旧聚合,并可以清除旧聚合以限制中间状态数据的⼤⼩。从Spark 2.1开始,我们⽀持⽔印功能,该功能允许⽤户指定最新数据的阈值,并允许引擎相应地清除旧状态。这些将在后⾯的“窗⼝操作”部分中详细介绍。

Window Operations

窗口操作

object WindowsWordCount {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession
            .builder()
            .master("local[*]")
            .appName("WindowsWordCount")
            .getOrCreate()
        import spark.implicits._
        spark.sparkContext.setLogLevel("ERROR")

        // 读取数据流
        val lines = spark.readStream
            .format("socket")
            .option("host", "CentOS")
            .option("port", 9999)
            .load()

        //数据处理  =>   (单词,时间戳)
        val words = lines.as[String]
            .map(t => (t.split(",")))
            .map(ts => (ts(0), new Timestamp(ts(1).toLong)))
            .toDF("word", "timestamp")

        import org.apache.spark.sql.functions._
        //窗口计算
        val wordCounts = words.groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
        	.count()
        //  .printSchema()   可直接打印看一下数据的组成
            .map(row => {
                val start = row.getStruct(0).getTimestamp(0)
                val end = row.getStruct(0).getTimestamp(1)
                val word = row.getString(1)
                val 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")

        //打印数据流结果
        val query = wordCounts.writeStream
            .outputMode(OutputMode.Complete())
            .format("console")
            .start()
        query.awaitTermination()
    }
}

获取当前时间的时间戳

object GetCurrrentTime {
    def main(args: Array[String]): Unit = {
        println(System.currentTimeMillis())
        val sdf = new SimpleDateFormat("HH:mm:ss")
        println(sdf.format(1583206193256L))     // 11:29:53
    }
}

结果

+--------+--------+-----+-----+
|   start|     end| word|count|
+--------+--------+-----+-----+
|11:29:52|11:29:56|apple|    1|
|11:29:50|11:29:54|apple|    1|
+--------+--------+-----+-----+
Late Data & Watermarking

在spark2.1版本引⼊watermarkering概念,⽤于告知计算节点,何时丢弃窗⼝聚合状态。因为流计算是⼀个⻓时间运⾏任务,系统不可能⽆限制存储⼀些过旧的状态值。使⽤watermarkering机制,系统可以删除那些过期的状态数据,⽤于释放内存。每个触发的窗⼝都有 start time 和 end time 属性,计算引擎会保留计算引擎所看到最⼤ event time

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

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

注意:引⼊watermarker以后,⽤户只能使⽤ update 、 append 模式,系统才会删除过期数据。

update-⽔位线没有没过窗⼝的end time之前,如果有数据落⼊到该窗⼝,该窗⼝会重复触发。

val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .getOrCreate()
import spark.implicits._
spark.sparkContext.setLogLevel("ERROR")
// 创建输入流
val lines = spark.readStream
    .format("socket")
    .option("host", "CentOS")
    .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())    //Update()为每次有数据进入该窗口都进行一次输出
    .format("console")
    .start()
query.awaitTermination()

Append–⽔位线没有没过窗⼝的end time之前,如果有数据落⼊到该窗⼝,该窗⼝不会触发,只会默默的计算,只有当⽔位线没过窗⼝end time的时候,才会做出最终输出。

与上面的代码一致,只有最后输出显示部分的显示方式不同
val query:StreamingQuery = wordCounts.writeStream
    .outputMode(OutputMode.Append())    //Update()为每次有数据进入该窗口都进行一次输出
    .format("console")
    .start()
    query.awaitTermination()

Semantic Guarantees of Aggregation with Watermarking

⽔印延迟(使⽤withWatermark设置)为“ 2⼩时”可确保引擎永远不会丢弃任何少于2⼩时的数据。换句话说,任何在此之前处理的最新数据⽐事件时间少2⼩时(以事件时间计)的数据都可以保证得到汇总。

但是,保证仅在⼀个⽅向上严格。延迟超过2⼩时的数据不能保证被删除;它可能会或可能不会聚合。数据延迟更多,引擎处理数据的可能性越⼩。

Join Operations

⾃Spark-2.0Structured Streaming引⼊的Join的概念(inner和⼀些外连接)。⽀持和静态或者动态的Dataset/DataFrame做join操作。

Stream-static Joins

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredStreamAndStaticJoin")
    .master("local[*]")
    .getOrCreate()
spark.sparkContext.setLogLevel("FATAL")
import spark.implicits._
//静态数据 id name
val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
.toDF("id", "name")
//创建流数据
val lineDF=spark.readStream
    .format("socket")
    .option("host","CentOS")
    .option("port",9999)
    .load()
//1 apple 1 45
val 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"))
val query = joinDF.writeStream.format("console")
    .outputMode(OutputMode.Update())
    .start()
query.awaitTermination()
}

注意 stream-static joins 并不是状态的,所以不需要做状态管理,⽬前还有⼀些外连接还不⽀持,⽬前只⽀持 inner 和 left outer 。

Stream-stream Joins

在spark-2.3添加了streaming-streaming的⽀持 ,实现两个流的join最⼤的挑战是在于找到⼀个时间点实现两个流的join,因为这两个流都没有结束。任意⼀个接受的流可以匹配另外⼀个流中即将被接受的数据。所以在任意⼀个流中我们需要接收并将这些数据进⾏缓存,然后作为当前stream的状态,然后去匹配另外⼀个的流的后续接收数据,继⽽⽣成相应的join的结果集。和Streaming的聚合很类似我们使⽤watermarker处理late,乱序的数据,限制状态的使⽤。

此处可将join理解为先将一个流数据缓存在内存中,当有另一个与此流在同一时间的流数据时,再将两个流数据进行join。

内连接可以使⽤任意⼀些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案例)—时间范围界定

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .getOrCreate()
spark.sparkContext.setLogLevel("FATAL")
import spark.implicits._
//001 apple 1 4.5 1566113401000   第一个流数据
//2.创建⼀个DataFrame
val orderDF = spark
    .readStream
    .format("socket")
    .option("host", "CentOS")
    .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 userDF = spark
    .readStream
    .format("socket")
    .option("host", "CentOS")
    .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.expr
//系统分别会对 user 和 order 缓存 最近 1 秒 和 2秒 数据,⼀旦时间过去,系统就⽆法保证数据状态继续保留
val loginWatermarker=userDF.withWatermark("login_time","1 second")
val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")
//计算订单的时间 & ⽤户 登陆之后的0~1秒 关联 数据 并且进⾏join
val joinDF = loginWatermarker.join(orderWatermarker,
    expr("uid=id and order_time >= login_time and order_time <= login_time + interval 1 second"))
val query=joinDF.writeStream
    .format("console")
    .outputMode(OutputMode.Append())     //仅⽀持Append模式输出
    .start()
query.awaitTermination()

案例⼆(event time window)

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .getOrCreate()
spark.sparkContext.setLogLevel("FATAL")
import spark.implicits._
//001 apple 1 4.5 1566113401000
//2.创建⼀个DataFrame
val orderDF = spark
    .readStream
    .format("socket")
    .option("host", "CentOS")
    .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"))
import org.apache.spark.sql.functions._
//001 zhangsan 1566113400000
val userDF = spark
    .readStream
    .format("socket")
    .option("host", "CentOS")
    .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"))
import org.apache.spark.sql.functions.expr
//系统分别会对 user 和 order 缓存 最近 1 秒 和 2秒 数据,⼀旦时间过去,系统就⽆法保证数据状态继续保留
val loginWatermarker=userDF.withWatermark("login_time","1 second")
val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")
//计算订单的时间 & ⽤户 登陆之后的0~1⼩时 关联 数据 并且进⾏join
val joinDF = loginWatermarker.join(orderWatermarker,expr("uid=id and leftWindow = rightWindow"))
val query=joinDF.writeStream
    .format("console")
    .outputMode(OutputMode.Append())      //仅⽀持Append模式输出
    .start()
query.awaitTermination()

猜你喜欢

转载自blog.csdn.net/origin_cx/article/details/104640520