Scala 在Spark结构化流媒体中,计算跨越批次的滚动度量的更好方法是什么?

Scala 在Spark结构化流媒体中,计算跨越批次的滚动度量的更好方法是什么?,scala,apache-spark,spark-structured-streaming,Scala,Apache Spark,Spark Structured Streaming,我正在使用Azure上的事件中心的事件,并希望计算不是基于单个事件而是基于成对事件的度量。我发现了大量关于如何处理聚合的信息,但没有太多顺序相关的度量(例如超前、滞后)。我知道Spark Streaming中没有这种现成的功能 确定问题范围的假设: 我正在接收的设备的事件以正确的顺序出现。从同一设备发送的两条消息中,偏移量较大的一条表示最近的事件 事件中心中的唯一设备多于分区(即分区上的消息序列不能假定属于同一设备) 目标: 能够批量计算所有消息的超前和滞后列,即仅在新的增量到达时计算 +---

我正在使用Azure上的事件中心的事件,并希望计算不是基于单个事件而是基于成对事件的度量。我发现了大量关于如何处理聚合的信息,但没有太多顺序相关的度量(例如超前、滞后)。我知道Spark Streaming中没有这种现成的功能

确定问题范围的假设:

  • 我正在接收的设备的事件以正确的顺序出现。从同一设备发送的两条消息中,偏移量较大的一条表示最近的事件
  • 事件中心中的唯一设备多于分区(即分区上的消息序列不能假定属于同一设备)
  • 目标:

    能够批量计算所有消息的超前和滞后列,即仅在新的增量到达时计算

    +---------+-----------------+--------+-----------+------------+
    | batchId | deviceReference | offset | lagOffset | leadOffset |
    +---------+-----------------+--------+-----------+------------+
    |     100 | a               |      1 | null      | 3          |
    |     100 | b               |      2 | null      | 5          |
    |     100 | a               |      3 | 1         | 6          |
    |     200 | c               |      4 | null      | 7          |
    |     200 | b               |      5 | 2         | null       |
    |     300 | a               |      6 | 3         | null       |
    |     300 | c               |      7 | 4         | null       |
    +---------+-----------------+--------+-----------+------------+
    
    lagOffset和leadOffset是我要计算的列,batchId是在给定时间从事件中心消耗的事件的标识符。显然,如果不访问deviceReference的上一个和下一个偏移量,每个batchId中的第一个和最后一个记录都将具有lag和leadOffset null,因此我想我应该尝试缓存

    尝试

    创建空DF并持久化(仅当第一次运行时,否则缓存存在)

    创建窗口函数,用于提前期/滞后期计算,并用于排除最后一条记录(因为它将缺少提前期值,并将在下一批中保留(如果提前期已到达)

    工艺流程

    反射:

    • 这感觉非常脆弱,就像我搞乱了batchCache DF中的状态一样,我会搞得一团糟。我本想使用内置功能作为检查点,但当我需要更改批处理范围以排除每个设备的最后一条记录时,我不知道如何使它们对我起作用
    • 显然,我可以将超前滞后应用于流作业的目标表,但这会使计算变得更大、冗余,并且还会引入数据更新,除非每个设备的最后一条记录被切断(超前为空的记录)
    问题:

    • 我想我遗漏了一些重要的东西。查找事件的相邻事件对于我要计算的度量非常重要,我相信我不是唯一的一个。如果所有设备都有自己的分区,我想问题会得到解决,但由于事件中心中的分片机制是散列,我不知道如何实现有人能告诉我我是不是在错误的地方攻击这个吗?
      • 如果我没有在错误的地方攻击它,那么我应该使用哪些Spark特性来增强它的健壮性呢
    干杯

    if (!positionCacheExists)
      spark.createDataFrame(sc.emptyRDD[Row], batchCacheSchema)
        .write
        .partitionBy("deviceReference")
        .format("json")
        .mode(org.apache.spark.sql.SaveMode.Append)
        .save(batchCachePath)
    
        val w = Window.partitionBy("deviceReference").orderBy(desc("offset"))
        val leadLag = Window.partitionBy("deviceReference").orderBy("offset")
    
      incomingDeviceStream.writeStream
                          .foreachBatch { (batchDF: DataFrame, batchId: Long) =>
    
                                //empty if first time, otherwise contains the second latest events per device from previous batch
                                val batchCache = spark.read.schema(lastBatchSchema).format("json").load(positionCachePath)
                                val modBatch = batchDF.withColumn("maxRow", rank().over(w))
    
                                modBatch.join(batchCache, 
                                              modBatch("deviceReference")===batchCache("deviceReference") &&
                                              modBatch("offset")>batchCache("offset"))
                                      .withColumn("last", batchCache("offset"))
                                      .union(modBatch.join(batchCache,
                                              modBatch("deviceReference")===batchCatche("deviceReference"),
                                              "left_anti")
                                      .withColumn("last", lit(null)))
                                      .withColumn("lagOffset", coalesce(lag(col("offset"),1).over(leadLag), col("last")).as("lagOffset"))
                                      .withColumn("leadOffset", lead(col("offset"),1).over(leadLag).as("leadOffset"))
                                      .filter(col("maxRow")>1)
                                      .write
                                      .partitionBy("deviceReference")
                                      .format("parquet")
                                      .mode(Append)
                                      .save(deviceEventPath)
    
                                //Store the second latest event per device as the last will have null as lead and will be set when the next event comes
                                modBatch.filter(col("maxRow")===2)
                                      .write
                                      .partitionBy("deviceReference")
                                      .format("json")
                                      .mode(Overwrite)
                                      .save(batchCachePath)
                                           }
                          .trigger(Trigger.Once())
                          .start()
                          .awaitTermination()