Flink对迟到数据的处理的三种方式

本文探讨Apache Flink处理事件时间窗口中迟到数据的方法,包括直接抛弃、重定向至侧输出流及允许迟到更新已计算窗口。介绍了如何通过设置allowedLateness实现窗口更新,并提供代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

**

Flink对迟到数据的处理

**

水位线可以用来平衡计算的完整性和延迟两方面。除非我们选择一种非常保守的水位线策略(最大延时设置的非常大,以至于包含了所有的元素,但结果是非常大的延迟),否则我们总需要处理迟到的元素。

迟到的元素是指当这个元素来到时,这个元素所对应的窗口已经计算完毕了(也就是说水位线已经没过窗口结束时间了)。这说明迟到这个特性只针对事件时间。

DataStream API提供了三种策略来处理迟到元素

直接抛弃迟到的元素

将迟到的元素发送到另一条流中去

可以更新窗口已经计算完的结果,并发出计算结果。

  1. 抛弃迟到元素
    抛弃迟到的元素是event time window operator的默认行为。也就是说一个迟到的元素不会创建一个新的窗口。
    process function可以通过比较迟到元素的时间戳和当前水位线的大小来很轻易的过滤掉迟到元素。

  2. 重定向迟到元素
    迟到的元素也可以使用侧输出(side output)特性被重定向到另外的一条流中去。迟到元素所组成的侧输出流可以继续处理或者sink到持久化设施中去。

例子 :直接使用算子指定迟到数据输出到测输出流

package com.late
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

//重定向的处理
object LateTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //设置并行度
    env.setParallelism(1)
    //设置时间为事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //
    val stream = env.socketTextStream("linux102", 9999, '\n')

    //数据来源 "a 100 12"   "a 99 14"
    val value = stream.map(x => {
      val str = x.split(" ")
      (str(0), str(1).toInt, str(2).toLong * 1000)
      //未设置延迟的水位线
    }).assignAscendingTimestamps(x => x._3).keyBy(_._1).timeWindow(Time.seconds(5))
      //直接将迟到数据重定向到”late_date"的数据流中
      .sideOutputLateData(new OutputTag[(String, Int, Long)]("late_date"))
      //.process(new MaxFunction)
      .aggregate(new MaxFunction)

    //获取“late_date"的测输出流
    value.getSideOutput(new OutputTag[(String,Int,Long)]("late_date")).print()
    env.execute()

    /* stream.map(x=>{
      val str = x.split(" ")
      (str(0),str(1).toLong*1000)*/

  }

//采用全窗口函数的形式
  /*class MaxFunction extends ProcessWindowFunction[(String,Int,Long),(String,Int),String,TimeWindow]{
    override def process(key: String, context: Context, elements: Iterable[(String, Int, Long)], out: Collector[(String, Int)]): Unit = {

      out.collect((key,elements.map(_._2).toIterator.max))
    }

  }*/

  // in out key windom
//采取质量函数的形式
  class MaxFunction extends AggregateFunction[(String,Int,Long),(String,Int),(String,Int)]{
    // 累加逻辑
    override def add(in: (String, Int, Long), acc: (String, Int)): (String, Int) = {
      (in._1,in._2.max(acc._2))}
    //初始化累加器
    override def createAccumulator(): (String, Int) =
      ("",0)
    //返回结果
    override def getResult(acc: (String, Int)): (String, Int) =
      acc
    //累加器聚合
    override def merge(acc: (String, Int), acc1: (String, Int)): (String, Int) =
      (acc._1,acc._2.max(acc1._2))
  }
}

通过自定义ProcessFunction来实现对迟到数据的处理 输出到侧输出流

object LateElement {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    val stream = env.socketTextStream("linux1", 9999, '\n')
    val s = stream
      .map(line => {
        val arr = line.split(" ")
        (arr(0), arr(1).toLong * 1000)
      })  //设值获取时间的方式 ,水位线的延迟为5秒钟
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, Long)](Time.seconds(5)) {
        override def extractTimestamp(element: (String, Long)): Long = element._2
      })
      .process(new MyLateProcess)
    s.getSideOutput(new OutputTag[String]("late")).print()
    env.execute()
  }
  class MyLateProcess extends ProcessFunction[(String, Long), (String, Long)] {
    val late = new OutputTag[String]("late")
    override def processElement(value: (String, Long),
                                ctx: ProcessFunction[(String, Long), (String, Long)]#Context,
                                out: Collector[(String, Long)]): Unit = {
      if (value._2 < ctx.timerService().currentWatermark()) {
      //将低于水位线的迟到数据输出到侧输出流
        ctx.output(late, "这个元素迟到了!")
      } else {
        out.collect(value)
      }
    }
  }
}
  1. 使用迟到元素更新窗口计算结果(Updating Results by Including Late Events)
    由于存在迟到的元素,所以已经计算出的窗口结果是不准确和不完全的。我们可以使用迟到元素更新已经计算完的窗口结果。

如果我们要求一个operator支持重新计算和更新已经发出的结果,就需要在第一次发出结果以后也要保存之前所有的状态。但显然我们不能一直保存所有的状态,肯定会在某一个时间点将状态清空,而一旦状态被清空,结果就再也不能重新计算或者更新了。而迟到的元素只能被抛弃或者发送到侧输出流。

window operator API提供了方法来明确声明我们要等待迟到元素。当使用event-time window,我们可以指定一个时间段叫做allowed lateness。window operator如果设置了allowed lateness,这个window operator在水位线没过窗口结束时间时也将不会删除窗口和窗口中的状态。窗口会在一段时间内(allowed lateness设置的)保留所有的元素。

当迟到元素在allowed lateness时间内到达时,这个迟到元素会被实时处理并发送到触发器(trigger)。当水位线没过了窗口结束时间+allowed lateness时间时,窗口会被删除,并且所有后来的迟到的元素都会被丢弃。

package com.atguigu

import org.apache.flink.api.common.state.ValueStateDescriptor
import org.apache.flink.api.scala.typeutils.Types
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

object AllowedLateTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    val stream = env.socketTextStream("Linux1", 9999, '\n')
    val s = stream
      .map(line => {
        val arr = line.split(" ")
        (arr(0), arr(1).toLong * 1000)
      })
//      .assignAscendingTimestamps(_._2)
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, Long)](Time.seconds(5)) {
        override def extractTimestamp(element: (String, Long)): Long = element._2
      })
      .keyBy(_._1)
      // [0,5),...
      .timeWindow(Time.seconds(5))
      // 水位线超过 窗口结束时间 窗口闭合计算,但不销毁
      // 水位线超过 窗口结束时间 + allowed lateness,窗口更新结果并销毁
      .allowedLateness(Time.seconds(5))
      .process(new MyAllowedLateProcess)
    s.print()
    env.execute()
  }
  class MyAllowedLateProcess extends ProcessWindowFunction[(String, Long),
    String, String,TimeWindow] {
    override def process(key: String,
                         context: Context,
                         elements: Iterable[(String, Long)],
                         out: Collector[String]): Unit = {
      lazy val isUpdate = getRuntimeContext.getState(
        new ValueStateDescriptor[Boolean]("update", Types.of[Boolean])
      )
      if (!isUpdate.value()) {
        out.collect("在水位线超过窗口结束时间的时候,窗口第一次闭合计算")
        isUpdate.update(true)
      } else {
        out.collect("迟到元素来了以后,更新窗口闭合计算的结果")
      }
    }
  }
}
<think>在Flink 1.16中,处理迟到数据主要依靠**事件时间语义**和**水印(Watermark)**机制,并结合窗口的**允许延迟(allowed lateness)**和**侧输出(side output)**功能。以下是具体实现方法: --- ### 1. **定义水印策略** 水印用于表示事件时间的进展,并容忍一定程度的乱序。在Flink SQL中,可以在创建表时定义水印策略: ```sql CREATE TABLE orders ( order_id STRING, amount DECIMAL(10, 2), order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND -- 允许5秒的乱序 ) WITH (...); ``` - `WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND` 表示系统允许事件时间比水印时间晚5秒。 --- ### 2. **使用窗口函数并设置允许延迟** 在窗口聚合(如`TUMBLE`、`HOP`)中,通过`GROUP BY`子句指定窗口,并设置`ALLOWED_LATENESS`来定义窗口关闭前的最大延迟时间: ```sql INSERT INTO result_table SELECT TUMBLE_END(order_time, INTERVAL '1' HOUR) AS window_end, SUM(amount) AS total_revenue FROM orders GROUP BY TUMBLE(order_time, INTERVAL '1' HOUR) -- 滚动窗口1小时 ALLOWED_LATENESS (INTERVAL '10' MINUTE); -- 允许迟到10分钟 ``` - **允许延迟机制**:窗口不会在水印到达窗口结束时间后立即关闭,而是会继续等待`10`分钟。在此期间到达的迟到数据,会触发窗口的**增量更新**(即重新计算并输出新结果)。 --- ### 3. **使用侧输出捕获迟到数据** 对于超过允许延迟的极端迟到数据,可通过`EMIT`语法(Flink 1.16新增)输出到独立的侧输出流: ```sql INSERT INTO result_table SELECT TUMBLE_END(order_time, INTERVAL '1' HOUR) AS window_end, SUM(amount) AS total_revenue FROM orders GROUP BY TUMBLE(order_time, INTERVAL '1' HOUR) WITH ( 'allow-lateness' = '10min', -- 允许迟到10分钟 'emit-mode' = 'allow-lateness-and-retract' -- 允许延迟并支持撤回 ); ``` - **侧输出流**:超过允许延迟的数据会被路由到侧输出表(需提前定义),避免污染主结果流。 --- ### 完整示例 #### Step 1: 创建源表(含水印) ```sql CREATE TABLE orders ( order_id STRING, amount DECIMAL(10, 2), order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'orders', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json' ); ``` #### Step 2: 创建结果表 ```sql CREATE TABLE revenue_report ( window_end TIMESTAMP(3), total_revenue DECIMAL(10, 2) ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://localhost:3306/flink', 'table-name' = 'revenue' ); ``` #### Step 3: 执行聚合(允许延迟10分钟) ```sql INSERT INTO revenue_report SELECT TUMBLE_END(order_time, INTERVAL '1' HOUR) AS window_end, SUM(amount) AS total_revenue FROM orders GROUP BY TUMBLE(order_time, INTERVAL '1' HOUR) ALLOWED_LATENESS (INTERVAL '10' MINUTE); ``` --- ### 关键点说明 1. **水印**:定义了事件时间的进度和乱序容忍度(如5秒)。 2. **允许延迟(`ALLOWED_LATENESS`)**:窗口会额外等待一段时间(如10分钟),期间迟到数据触发结果更新。 3. **侧输出**:超过允许延迟的数据单独处理,避免影响主流程[^1][^3]。 --- ### 相关问题 1. Flink SQL中水印延迟设置对窗口计算有何影响? 2. 如何配置Flink SQL作业以处理超过允许延迟的极端迟到数据? 3. 在Flink 1.16中,`EMIT`语法的具体使用场景有哪些? 4. 如何验证Flink SQL窗口聚合结果的准确性?</think>在 Flink 1.16 SQL 中处理迟到数据主要通过 **水印(Watermark)** 机制和 **允许延迟(Allowed Lateness)** 功能实现,以下是完整实现方案: ### 1. 定义水印策略 在创建表时使用 `WATERMARK` 子句定义水印策略,允许数据迟到达: ```sql CREATE TABLE orders ( order_id STRING, amount DECIMAL(10, 2), order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '2' MINUTE -- 允许2分钟延迟 ) WITH ( 'connector' = 'kafka', 'topic' = 'orders', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json' ); ``` - `WATERMARK FOR order_time AS order_time - INTERVAL '2' MINUTE` 表示系统允许事件时间比水印时间晚2分钟到达[^2] ### 2. 使用允许延迟功能 在窗口聚合时添加 `ALLOWED_LATENESS` 子句处理迟到数据: ```sql INSERT INTO revenue_report SELECT HOP_END(order_time, INTERVAL '5' MINUTE, INTERVAL '10' MINUTE) AS window_end, SUM(amount) AS total_revenue FROM orders GROUP BY HOP(order_time, INTERVAL '5' MINUTE, INTERVAL '10' MINUTE) ALLOWED_LATENESS (INTERVAL '5' MINUTE); -- 额外允许5分钟延迟 ``` - **窗口行为**: - 主窗口:每10分钟计算一次(窗口大小) - 滑动间隔:每5分钟触发一次计算 - 允许延迟:窗口关闭后额外等待5分钟处理迟到数据 ### 3. 完整示例(每5分钟计算,每10分钟输出) ```sql -- 创建结果表 CREATE TABLE revenue_report ( window_end TIMESTAMP(3), total_revenue DECIMAL(10, 2) ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://localhost:3306/flink', 'table-name' = 'revenue' ); -- 处理迟到数据的聚合查询 INSERT INTO revenue_report SELECT HOP_END(order_time, INTERVAL '5' MINUTE, INTERVAL '10' MINUTE) AS window_end, SUM(amount) AS total_revenue FROM orders GROUP BY HOP(order_time, INTERVAL '5' MINUTE, INTERVAL '10' MINUTE) ALLOWED_LATENESS (INTERVAL '5' MINUTE); ``` ### 迟到数据处理机制 1. **首次计算结果**:在窗口结束时间(如10:10)立即输出 2. **延迟数据到达**:窗口关闭后5分钟内到达的数据(允许延迟期内) 3. **更新结果**:Flink 会输出一条**撤回消息**(retract)和**更新后的结果** 4. **最终关闭**:水印超过 `窗口结束时间 + 允许延迟` 后窗口永久关闭 ### 验证结果准确性 可通过查询系统表验证迟到数据处理: ```sql SELECT * FROM TABLE( DESCRIBE_STATEMENT( 'INSERT INTO revenue_report...' -- 替换为实际INSERT语句 ) ); ``` ### 注意事项 1. **水印生成**:确保使用事件时间(`TIMESTAMP(3)` 类型) 2. **状态保留**:允许延迟会增加状态存储时间 3. **结果更新**:下游系统需支持撤回/更新(如JDBC、Upsert Kafka) 4. **极端延迟**:超过允许延迟的数据会被丢弃(可通过侧输出捕获)[^1][^3] > **提示**:在 Flink 1.16 中,Python UDF 支持得到增强,可通过 JNI 在 JVM 中执行自定义函数[^4],适用于复杂迟到数据处理逻辑。 --- ### 相关问题 1. Flink SQL 中如何配置水印延迟时间以优化迟到数据处理? 2. 在 Flink 1.16 中,如何实现窗口聚合结果的撤回和更新? 3. 如何处理超过允许延迟时间的极端迟到数据? 4. Flink SQL 的 `HOP` 窗口与 `CUMULATE` 窗口在处理迟到数据时有何区别? 5. 如何监控 Flink SQL 作业中迟到数据处理情况?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值