基于 Watermark 处理延迟数据

本文介绍了StructuredStreaming在Spark中如何利用Watermark机制解决数据延迟问题,包括事件时间窗口、状态管理、不同输出模式下的影响,以及水印在去重和数据时效性控制中的应用。

在数据分析系统中, Structured Streaming 可以持续的按照 event-time 聚合数据, 然而在此过程中并不能保证数据按照时间的先后依次到达. 例如: 当前接收的某一条数据的 event-time 可能远远早于之前已经处理过的 event-time. 在发生这种情况时, 往往需要结合业务需求对延迟数据进行过滤.

现在考虑如果事件延迟到达会有哪些影响. 假如, 一个单词在 12:04(event-time) 产生, 在 12:11 到达应用. 应用应该使用 12:04 来在窗口(12:00 - 12:10)中更新计数, 而不是使用 12:11. 这些情况在我们基于窗口的聚合中是自然发生的, 因为结构化流可以长时间维持部分聚合的中间状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYsvLZTy-1666402864047)(Structured Streaming.assets/1565742945.png)]

但是, 如果这个查询运行数天, 系统很有必要限制内存中累积的中间状态的数量. 这意味着系统需要知道何时从内存状态中删除旧聚合, 因为应用不再接受该聚合的后期数据.

为了实现这个需求, 从 spark2.1, 引入了 watermark(水印), 使用引擎可以自动的跟踪当前的事件时间, 并据此尝试删除旧状态.

通过指定 event-time 列和预估事件的延迟时间上限来定义一个查询的 watermark. 针对一个以时间 T 结束的窗口, 引擎会保留状态和允许延迟时间直到(max event time seen by the engine - late threshold > T). 换句话说, 延迟时间在上限内的被聚合, 延迟时间超出上限的开始被丢弃.

可以通过withWatermark() 来定义watermark

watermark 计算: watermark = MaxEventTime - Threshhod

而且, watermark只能逐渐增加, 不能减少

总结:

Structured Streaming 引入 Watermark 机制, 主要是为了解决以下两个问题:

  1. 处理聚合中的延迟数据
  2. 减少内存中维护的聚合状态.

在不同输出模式(complete, append, update)中, Watermark 会产生不同的影响.

complete模式下使用 watermark

package com.strive.ss

import java.sql.Timestamp

import org.apache.spark.sql._
import org.apache.spark.sql.streaming.{StreamingQuery, Trigger}

object WordCountWatermark1 {
    def main(args: Array[String]): Unit = {

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

        import spark.implicits._
        val lines: DataFrame = spark.readStream
            .format("socket")
            .option("host", "localhost")
            .option("port", 10000)
            .load

        // 输入的数据中包含时间戳, 而不是自动添加的时间戳
        val words: DataFrame = lines.as[String].flatMap(line => {
            val split = line.split(",")
            split(1).split(" ").map((_, Timestamp.valueOf(split(0))))
        }).toDF("word", "timestamp")

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

        val wordCounts: Dataset[Row] = words
            // 添加watermark, 参数 1: event-time 所在列的列名 参数 2: 延迟时间的上限.
            .withWatermark("timestamp", "2 minutes")
            .groupBy(window($"timestamp", "10 minutes", "2 minutes"), $"word")
            .count()
            .sort("window")  // 只在complete模式支持

        val query: StreamingQuery = wordCounts.writeStream
            .outputMode("complete")
            .trigger(Trigger.ProcessingTime(1000))
            .format("console")
            .option("truncate", "false")
            .start
        query.awaitTermination()
    }
}

注意: 初始化wartmark 是 0

有以下几条数据:

测试:

  1. 输入数据:2019-08-14 10:55:00,dog

    这个条数据作为第一批数据. 按照window($"timestamp", "10 minutes", "2 minutes")得到 5 个窗口. 由于是第一批, 所有的窗口的结束时间都大于 wartermark(0), 所以 5 个窗口都显示.

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |1    |
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |1    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    然后根据当前批次中最大的 event-time, 计算出来下次使用的 watermark. 本批次只有一个数据(10:55), 所有: watermark = 10:55 - 2min = 10:53

  2. 输入数据:2019-08-14 11:00:00,dog

    这条数据作为第二批数据, 计算得到 5 个窗口. 此时的watermark=10:53, 所有的窗口的结束时间均大于 watermark. 在 complete模式下, 数据全部输出.

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |1    |
    ---------------------新增数据---------------------------------
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |2    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |2    |
    |[2019-08-14 10:56:00, 2019-08-14 11:06:00]|dog |1    |
    |[2019-08-14 10:58:00, 2019-08-14 11:08:00]|dog |1    |
    |[2019-08-14 11:00:00, 2019-08-14 11:10:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    此时的改变 watermark = 11:00 - 2min = 10:58

  3. 输入数据:2019-08-14 10:55:00,dog

    相当于一条延迟数据.

    这条数据作为第 3 批次, 计算得到 5 个窗口. 此时的 watermark = 10:58 当前内存中有两个窗口的结束时间已经低于 10: 58.

    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    

    则立即删除这两个窗口在内存中的维护状态. 同时, 当前批次中新加入的数据所划分出来的窗口, 如果窗口结束时间低于 11:58, 则窗口会被过滤掉.

    所以这次输出结果:

    理论输出
    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |2    |
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |3    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |3    |
    |[2019-08-14 10:56:00, 2019-08-14 11:06:00]|dog |1    |
    |[2019-08-14 10:58:00, 2019-08-14 11:08:00]|dog |1    |
    |[2019-08-14 11:00:00, 2019-08-14 11:10:00]|dog |1    |
    +------------------------------------------+----+-----+
    
    实际输出
    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |2    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |2    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |2    |
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |3    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |3    |
    |[2019-08-14 10:56:00, 2019-08-14 11:06:00]|dog |1    |
    |[2019-08-14 10:58:00, 2019-08-14 11:08:00]|dog |1    |
    |[2019-08-14 11:00:00, 2019-08-14 11:10:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    complete模式要求保留所有聚合数据,因此不能使用水印删除中间状态。

update 模式下使用 watermark

在 update 模式下, 仅输出与之前批次的结果相比, 涉及更新或新增的数据.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hROXttvj-1666402864050)(Structured Streaming.assets/1565747738.png)]

import java.sql.Timestamp

import org.apache.spark.sql._
import org.apache.spark.sql.streaming.{StreamingQuery, Trigger}

object WordCountWatermark1 {
    def main(args: Array[String]): Unit = {

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

        import spark.implicits._
        val lines: DataFrame = spark.readStream
            .format("socket")
            .option("host", "localhost")
            .option("port", 10000)
            .load

        // 输入的数据中包含时间戳, 而不是自动添加的时间戳
        val words: DataFrame = lines.as[String].flatMap(line => {
            val split = line.split(",")
            split(1).split(" ").map((_, Timestamp.valueOf(split(0))))
        }).toDF("word", "timestamp")

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


        val wordCounts: Dataset[Row] = words
            // 添加watermark, 参数 1: event-time 所在列的列名 参数 2: 延迟时间的上限.
            .withWatermark("timestamp", "2 minutes")
            .groupBy(window($"timestamp", "10 minutes", "2 minutes"), $"word")
            .count()
            // .sort("window")报错  update模式只输出更新和新增数据,对窗口排序没有意义

        val query: StreamingQuery = wordCounts.writeStream
            .outputMode("update")
            .trigger(Trigger.ProcessingTime(1000))
            .format("console")
            .option("truncate", "false")
            .start
        query.awaitTermination()
    }
}

注意: 初始化wartmark 是 0

有以下几条数据:

测试:

  1. 输入数据:2019-08-14 10:55:00,dog

    这个条数据作为第一批数据. 按照window($"timestamp", "10 minutes", "2 minutes")得到 5 个窗口. 由于是第一批, 所有的窗口的结束时间都大于 wartermark(0), 所以 5 个窗口都显示.

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |1    |
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |1    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    然后根据当前批次中最大的 event-time, 计算出来下次使用的 watermark. 本批次只有一个数据(10:55), 所有: watermark = 10:55 - 2min = 10:53

  2. 输入数据:2019-08-14 11:00:00,dog

    这条数据作为第二批数据, 计算得到 5 个窗口. 此时的watermark=10:53, 所有的窗口的结束时间均大于 watermark. 在 update 模式下, 只输出结果表中涉及更新或新增的数据.

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 11:00:00, 2019-08-14 11:10:00]|dog |1    |
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |2    |
    |[2019-08-14 10:58:00, 2019-08-14 11:08:00]|dog |1    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |2    |
    |[2019-08-14 10:56:00, 2019-08-14 11:06:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    其中: count 是 2 的表示更新, count 是 1 的表示新增. 没有变化的就没有显示.(但是内存中仍然保存着)

    // 第一批次中的数据仍然在内存保存着
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    

    此时的 watermark = 11:00 - 2min = 10:58

  3. 输入数据:2019-08-14 10:55:00,dog

    相当于一条延迟数据.

    这条数据作为第 3 批次, 计算得到 5 个窗口. 此时的 watermark = 10:58 当前内存中有两个窗口的结束时间已经低于 10: 58.

    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    

    则立即删除这两个窗口在内存中的维护状态. 同时, 当前批次中新加入的数据所划分出来的窗口, 如果窗口结束时间低于 11:58, 则窗口会被过滤掉.

    所以这次输出结果:

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:52:00, 2019-08-14 11:02:00]|dog |3    |
    |[2019-08-14 10:50:00, 2019-08-14 11:00:00]|dog |2    |
    |[2019-08-14 10:54:00, 2019-08-14 11:04:00]|dog |3    |
    +------------------------------------------+----+-----+
    

    第三个批次的数据处理完成后, 立即计算: watermark= 10:55 - 2min = 10:53, 这个值小于当前的 watermask(10:58), 所以保持不变.(因为 watermask 只能增加不能减少)

append 模式下使用 wartermark

把前一个案例中的update改成append即可.

val query: StreamingQuery = wordCounts.writeStream
    .outputMode("append")
    .trigger(Trigger.ProcessingTime(0))
    .format("console")
    .option("truncate", "false")
    .start

在 append 模式中, 仅输出新增的数据, 且输出后的数据无法变更.

测试:

  1. 输入数据:2019-08-14 10:55:00,dog

    这个条数据作为第一批数据. 按照window($"timestamp", "10 minutes", "2 minutes")得到 5 个窗口. 由于此时初始 watermask=0, 当前批次中所有窗口的结束时间均大于 watermask.

    但是 Structured Streaming 无法确定后续批次的数据中是否会更新当前批次的内容. 因此, 基于 Append 模式的特点, 这时并不会输出任何数据(因为输出后数据就无法更改了), 直到某个窗口的结束时间小于 watermask, 即可以确定后续数据不会再变更该窗口的聚合结果时才会将其输出, 并移除内存中对应窗口的聚合状态.

    +------+----+-----+
    |window|word|count|
    +------+----+-----+
    +------+----+-----+
    

    然后根据当前批次中最大的 event-time, 计算出来下次使用的 watermark. 本批次只有一个数据(10:55), 所有: watermark = 10:55 - 2min = 10:53

  2. 输入数据:2019-08-14 11:00:00,dog

    这条数据作为第二批数据, 计算得到 5 个窗口. 此时的watermark=10:53, 所有的窗口的结束时间均大于 watermark, 仍然不会输出.

    +------+----+-----+
    |window|word|count|
    +------+----+-----+
    +------+----+-----+
    

    然后计算 watermark = 11:00 - 2min = 10:58

  3. 输入数据:2019-08-14 10:55:00,dog

    相当于一条延迟数据.

    这条数据作为第 3 批次, 计算得到 5 个窗口. 此时的 watermark = 10:58 当前内存中有两个窗口的结束时间已经低于 10: 58.

    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    

    则意味着这两个窗口的数据不会再发生变化, 此时输出这个两个窗口的聚合结果, 并在内存中清除这两个窗口的状态.

    所以这次输出结果:

    +------------------------------------------+----+-----+
    |window                                    |word|count|
    +------------------------------------------+----+-----+
    |[2019-08-14 10:46:00, 2019-08-14 10:56:00]|dog |1    |
    |[2019-08-14 10:48:00, 2019-08-14 10:58:00]|dog |1    |
    +------------------------------------------+----+-----+
    

    第三个批次的数据处理完成后, 立即计算: watermark= 10:55 - 2min = 10:53, 这个值小于当前的 watermask(10:58), 所以保持不变.(因为 watermask 只能增加不能减少)

watermark 机制总结

加水印以清除聚合状态的条件
  1. watermark 在用于基于时间的状态聚合操作时, 该时间可以基于窗口, 也可以基于 event-time本身.

    The aggregation must have either the event-time column, or a window on the event-time column.

  2. 输出模式必须是appendupdate. 在输出模式是complete的时候(必须有聚合), 要求每次输出所有的聚合结果. 我们使用 watermark 的目的是丢弃一些过时聚合数据, 所以complete模式使用wartermark无效也无意义.

  3. 在输出模式是append时, 必须设置 watermask 才能使用聚合操作. 其实, watermask 定义了 append 模式中何时输出聚合聚合结果(状态), 并清理过期状态.

  4. 在输出模式是update时, watermask 主要用于过滤过期数据并及时清理过期状态.

  5. watermask 会在处理当前批次数据时更新, 并且会在处理下一个批次数据时生效使用. 但如果节点发送故障, 则可能延迟若干批次生效.

  6. withWatermark 必须使用与聚合操作中的时间戳列是同一列.df.withWatermark("time", "1 min").groupBy("time2").count() 无效

  7. withWatermark 必须在聚合之前调用. f.groupBy("time").count().withWatermark("time", "1 min") 无效

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

流数据去重

根据唯一的 id 实现数据去重.dropDuplicates

数据:

1,2019-09-14 11:50:00,dog
2,2019-09-14 11:51:00,dog
1,2019-09-14 11:50:00,dog
3,2019-09-14 11:53:00,dog
1,2019-09-14 11:50:00,dog
4,2019-09-14 11:45:00,dog

import java.sql.Timestamp

import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}

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

        val lines: DataFrame = spark.readStream
            .format("socket")
            .option("host", "localhost")
            .option("port", 10000)
            .load()

        val words: DataFrame = lines.as[String].map(line => {
            val arr: Array[String] = line.split(",")
            (arr(0), Timestamp.valueOf(arr(1)), arr(2))
        }).toDF("uid", "ts", "word")

        val wordCounts: Dataset[Row] = words
            .withWatermark("ts", "2 minutes")  // 
            .dropDuplicates("uid")  // 去重重复数据 uid 相同就是重复.  可以传递多个列

        wordCounts.writeStream
            .outputMode("append")
            .format("console")
            .start
            .awaitTermination()
    }
}

注意:

  1. dropDuplicates 不可用在聚合之后, 即通过聚合得到的 df/ds 不能调用dropDuplicates
  2. 使用watermask - 如果重复记录的到达时间有上限,则可以在事件时间列上定义水印,并使用guid和事件时间列进行重复数据删除。该查询将使用水印从过去的记录中删除旧的状态数据,这些记录不会再被重复。这限制了查询必须维护的状态量。
  3. 没有watermask - 由于重复记录可能到达时没有界限,查询将来自所有过去记录的数据存储为状态。

测试

  1. 第一批:

    1,2019-09-14 11:50:00,dog
    
    +---+-------------------+----+
    |uid|                 ts|word|
    +---+-------------------+----+
    |  1|2019-09-14 11:50:00| dog|
    +---+-------------------+----+
    
  2. 第 2 批:

    2,2019-09-14 11:51:00,dog
    
    +---+-------------------+----+
    |uid|                 ts|word|
    +---+-------------------+----+
    |  2|2019-09-14 11:51:00| dog|
    +---+-------------------+----+
    
  3. 第 3 批: 1,2019-09-14 11:50:00,dog
    id 重复无输出

  4. 第 4 批: 3,2019-09-14 11:53:00,dog

    +---+-------------------+----+
    |uid|                 ts|word|
    +---+-------------------+----+
    |  3|2019-09-14 11:53:00| dog|
    +---+-------------------+----+
    

    此时 watermask=11:51

  5. 第 5 批: 1,2019-09-14 11:50:00,dog 数据重复, 并且数据过期, 所以无输出

  6. 第 6 批 4,2019-09-14 11:45:00,dog 数据过时, 所以无输出

<think>嗯,用户问Flink的watermark是不是随着数据流动的。首先,我需要确认用户对Flink的基本概念是否了解,比如数据流、事件时间、处理时间,以及watermark的作用。用户可能是在学习Flink的时候遇到了watermark的概念,想更深入理解它的工作机制。 用户的问题看似简单,但需要明确watermark的生成和传播机制。首先,watermark是用来处理事件时间乱序问题的,它本身是一个特殊的时间戳,会被插入到数据流中。接下来要考虑的是,watermark是随着数据流一起流动的吗?或者说,它是如何在不同算子之间传递的? 根据已有的知识,Flink的数据流由数据元素和水位线组成,水位线会随着数据流在算子之间传递。每个算子会根据接收到的watermark来更新自己的事件时间时钟,从而触发计算,比如窗口的闭合。这里需要解释清楚watermark是嵌入在数据流中的,作为特殊记录流动,并且会影响下游算子的时间处理逻辑。 可能用户真正想知道的是watermark的传播机制,以及它对程序的影响。比如,如果某个算子的处理速度慢,是否会影响下游watermark的推进?或者,在并行任务中,watermark是如何协调的?这时候需要说明Flink的watermark对齐机制,即每个子任务会跟踪自己的watermark,而整个任务的watermark是所有子任务中最小的那个,这可能会引起处理延迟的问题。 另外,用户可能在实际应用中遇到了乱序数据处理的问题,想知道如何正确设置watermark的生成策略,比如周期性的生成还是根据特定事件生成。这时候需要提到不同的生成方式,比如AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks,以及它们适用的场景。 还需要注意用户可能的误区,比如认为watermark是全局唯一的,或者每个数据都带有自己的watermark。但实际上,watermark是周期性生成并传播的,每个算子维护自己的watermark状态,并根据上游传递的watermark来更新。 总结下来,回答需要涵盖以下几点: 1. Watermark的定义和作用:处理事件时间乱序,触发事件时间相关的计算。 2. Watermark的生成方式:周期性或基于事件生成。 3. Watermark的传播机制:随数据流流动,算子间传递并影响各自的事件时间时钟。 4. 并行环境下的处理:子任务的watermark对齐机制。 5. 实际应用中的配置建议:根据数据特性选择合适的生成策略,设置允许的延迟时间。 还需要给出一个例子,比如使用周期性生成watermark的代码片段,说明如何设置,并解释其效果。这样用户能更直观地理解watermark是如何在数据流中工作的。 最后,确保回答结构清晰,分点说明,语言简洁,避免过于技术化的术语,让初学者也能理解。同时,检查是否有错误,比如是否每个算子都有自己的watermark,或者watermark的传播是否受数据流顺序影响等,确保信息的准确性。</think>Flink 中的 **Watermark(水位线)** 确实会随着数据流在算子(Operator)之间流动,但其本质是一种**特殊的时间戳标记**,用于在事件时间(Event Time)语义下解决数据乱序问题。以下是详细解释: --- ### 一、Watermark 的核心作用 1. **事件时间处理** 当使用 `Event Time` 时,数据可能因网络延迟等原因乱序到达。Watermark 的作用是: - **标记事件时间进度**:表示“当前时间之前的数据理论上已到达”(允许一定的延迟)。 - **触发计算**:例如窗口(Window)的闭合、定时器(Timer)触发。 2. **乱序容忍机制** 通过设置 `allowedLateness`,Watermark 允许在窗口关闭后短暂保留数据,以处理延迟到达的数据。 --- ### 二、Watermark 的流动机制 1. **生成方式** - **周期性生成**:通过 `AssignerWithPeriodicWatermarks`,按固定时间间隔生成(如每 200ms)。 - **事件驱动生成**:通过 `AssignerWithPunctuatedWatermarks`,根据特定数据生成(如带标记的数据)。 2. **嵌入数据流** - Watermark 会被插入到数据流中,作为**特殊记录**(非业务数据)向下游传递。 - **示例代码**(周期性生成): ```java DataStream<Event> stream = ...; stream .assignTimestampsAndWatermarks( WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner((event, timestamp) -> event.getTimestamp()) ); ``` 3. **传递规则** - **广播性流动**:每个算子的输入分区(Input Partition)会收到来自上游的 Watermark。 - **对齐机制**:算子会跟踪所有输入分区的 Watermark,取 **最小值** 作为当前事件时间。 *(例如:上游有 3 个分区,Watermark 分别为 10:00、10:02、10:01,则当前算子的 Watermark 为 10:00)* --- ### 三、关键特性与影响 1. **全局进度推进** - Watermark 的流动推动整个作业的**事件时间时钟**向前推进。 - 若某个分区的数据长期停滞(无 Watermark 更新),全局事件时间会被阻塞(需注意数据倾斜问题)。 2. **窗口触发条件** 当 Watermark ≥ **窗口结束时间** 时,窗口会触发计算。例如: - 窗口范围:`[10:00, 10:15)` - Watermark 达到 `10:15` 时,窗口关闭并输出结果。 3. **与数据的关系** - Watermark **不依赖数据内容**,仅依赖时间戳。 - 若数据的时间戳 ≤ 当前 Watermark,该数据可能被丢弃(除非配置了延迟容忍)。 --- ### 四、典型问题与调优 1. **Watermark 生成过慢** - 现象:窗口延迟触发。 - 调优:缩短周期性生成间隔,或优化事件驱动逻辑。 2. **数据倾斜导致对齐阻塞** - 现象:某个分区的 Watermark 停滞,拖慢全局进度。 - 调优:检查数据分布,修复倾斜源(如 `keyBy` 后的热点 Key)。 3. **乱序时间过长** - 现象:Watermark 触发窗口后仍有大量迟到数据。 - 调优:增大 `allowedLateness` 或调整 Watermark 生成策略(如 `forBoundedOutOfOrderness` 的时间阈值)。 --- ### 五、总结 - **Watermark 是随着数据流动的**,但其流动逻辑独立于业务数据,用于协调全局事件时间进度。 - 正确设置 Watermark 策略(生成频率、乱序容忍度)是保证 Flink 事件时间处理可靠性的关键。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值