Flink 对于迟到数据的处理

本文探讨了Apache Flink如何解决流式数据处理中的乱序问题,重点介绍了WaterMark和Window机制的应用。针对迟到数据,文章详细阐述了三种处理方式:丢弃、发送到侧输出流及更新计算结果,通过代码示例展示了如何使用allowedLateness实现更精确的计算。

WaterMark 和 Window 机制解决了流式数据的乱序问题,对于因为延迟而顺序有误的数据,可以根据eventTime进行业务处理。

Event Time语义下我们使用Watermark来判断数据是否迟到。一个迟到元素是指元素到达窗口算子时,该元素本该被分配到某个

窗口,但由于延迟,窗口已经触发计算。目前Flink有三种处理迟到数据的方式:

  • 直接将迟到数据丢弃
  • 将迟到数据发送到另一个流
  • 重新执行一次计算,将迟到数据考虑进来,更新计算结果

将迟到数据丢弃

如果不做其他操作,默认情况下迟到数据会被直接丢弃。

将迟到数据发送到另外一个流

如果想对这些迟到数据处理,我们可以使用Flink的侧输出(Side Output)功能,将迟到数据发到某个特定的流上。后续我们可以根

据业务逻辑的要求,对迟到的数据流进行处理。

假设输入的数据格式如下

String : timestamp

如 hello:1559207589000

代码示例如下

DataStream<Tuple2<String, Long>> dataStream = env.socketTextStream("10.0.2.11", 10000, "\n")
	.map(new MapFunction<String, Tuple2<String, Long>>() {
		@Override
		public Tuple2<String, Long> map(String s) throws Exception {
			String[] arr = s.split(":");
			return new Tuple2<String, Long>(arr[0], Long.valueOf(arr[1]));
		}
	}).filter(new FilterFunction<Tuple2<String, Long>>() {
		@Override
		public boolean filter(Tuple2<String, Long> tuple2) throws Exception {
			return !tuple2.f0.equals("0") && tuple2.f1 != 0L;
		}
	})
;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");

DataStream waterStream = dataStream.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
	Long currentMaxTimestamp = 0L;
	Long maxOutOfOrderness = 3_000L;
	Long lastEmittedWatermark = Long.MIN_VALUE;

	@Override
	public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
		// 将元素的时间字段值作为该数据的timestamp
		Long time = element.f1;
		if (time > currentMaxTimestamp) {
			currentMaxTimestamp = time;
		}
		String outData = String.format("key: %s    EventTime: %s    waterMark:  %s", element.f0, sdf.format(time),
				sdf.format(getCurrentWatermark().getTimestamp()));
		System.out.println(outData);
		return time;
	}

	@Nullable
	@Override
	public Watermark getCurrentWatermark() {
		// 允许延迟三秒
		Long potentialWM = currentMaxTimestamp - maxOutOfOrderness;
		// 保证水印能依次递增
		if (potentialWM >= lastEmittedWatermark) {
			lastEmittedWatermark = potentialWM;
		}
		return new Watermark(lastEmittedWatermark);
	}
});

OutputTag<Tuple2<String, Long>> lateData = new OutputTag<Tuple2<String, Long>>("late"){};
// 根据 name 进行分组
DataStream result = waterStream.keyBy(0)
		.window(TumblingEventTimeWindows.of(org.apache.flink.streaming.api.windowing.time.Time.seconds(5L)))
		.sideOutputLateData(lateData)
		.apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
			@Override
			public void apply(Tuple s, TimeWindow timeWindow, Iterable<Tuple2<String, Long>> iterable, Collector<String> collector) throws Exception {
				System.out.println("trigger window [" + sdf.format(new Date(timeWindow.getStart())) + "," + sdf.format(new Date(timeWindow.getEnd())) + "), " + s + ", " + JSON.toJSONString(iterable));
			}
		})
		;

((SingleOutputStreamOperator<String>) result).getSideOutput(lateData).print("late");

上面的代码将迟到的内容写进名为“late”的OutputTag下,之后使用getSideOutput获取这些迟到的数据。

更新计算结果

对于迟到数据,使用上面两种方法,都对计算结果的正确性有影响。如果将数据流发送到单独的侧输出,我们仍然需要完成单独

的处理逻辑,相对比较复杂。更理想的情况是,将迟到数据重新进行一次触发,得到一个更新的结果。 allowedLateness允许用户在

Event Time下对某个窗口先得到一个结果,如果在一定时间内有迟到数据,迟到数据会和之前的数据一起重新被计算,以得到一

个更准确的结果。使用这个功能时需要注意,原来窗口中的状态数据在窗口已经触发的情况下仍然会被保留,否则迟到数据到来

后也无法与之前数据融合。另一方面,更新的结果要以一种合适的形式输出到外部系统,或者将原来结果覆盖,或者同时保存且

有时间戳以表明来自更新后的计算。比如,我们的计算结果是一个键值对(Key-Value),我们可以把这个结果输出到Redis这样

的KV数据库中,使用某些Reids命令,对于同一个Key下,旧的结果被新的结果所覆盖。

如果不明确调用allowedLateness,默认的允许延迟的参数是0。如果对一个Processing Time下的程序使用allowedLateness,将

引发异常。

DataStream<Tuple2<String, Long>> dataStream = env.socketTextStream("10.0.2.11", 10000, "\n")
	.map(new MapFunction<String, Tuple2<String, Long>>() {
		@Override
		public Tuple2<String, Long> map(String s) throws Exception {
			String[] arr = s.split(":");
			return new Tuple2<String, Long>(arr[0], Long.valueOf(arr[1]));
		}
	}).filter(new FilterFunction<Tuple2<String, Long>>() {
		@Override
		public boolean filter(Tuple2<String, Long> tuple2) throws Exception {
			return !tuple2.f0.equals("0") && tuple2.f1 != 0L;
		}
	})
;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");

DataStream waterStream = dataStream.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
	Long currentMaxTimestamp = 0L;
	Long maxOutOfOrderness = 3_000L;
	Long lastEmittedWatermark = Long.MIN_VALUE;

	@Override
	public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
		// 将元素的时间字段值作为该数据的timestamp
		Long time = element.f1;
		if (time > currentMaxTimestamp) {
			currentMaxTimestamp = time;
		}
		String outData = String.format("key: %s    EventTime: %s    waterMark:  %s", element.f0, sdf.format(time),
				sdf.format(getCurrentWatermark().getTimestamp()));
		System.out.println(outData);
		return time;
	}

	@Nullable
	@Override
	public Watermark getCurrentWatermark() {
		// 允许延迟三秒
		Long potentialWM = currentMaxTimestamp - maxOutOfOrderness;
		// 保证水印能依次递增
		if (potentialWM >= lastEmittedWatermark) {
			lastEmittedWatermark = potentialWM;
		}
		return new Watermark(lastEmittedWatermark);
	}
});

OutputTag<Tuple2<String, Long>> lateData = new OutputTag<Tuple2<String, Long>>("late"){};
// 根据 name 进行分组
DataStream result = waterStream.keyBy(0)
		.window(TumblingEventTimeWindows.of(org.apache.flink.streaming.api.windowing.time.Time.seconds(5L)))
		.allowedLateness(org.apache.flink.streaming.api.windowing.time.Time.seconds(2L))
		.sideOutputLateData(lateData)
		.apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
			@Override
			public void apply(Tuple s, TimeWindow timeWindow, Iterable<Tuple2<String, Long>> iterable, Collector<String> collector) throws Exception {
				System.out.println("trigger window [" + sdf.format(new Date(timeWindow.getStart())) + "," + sdf.format(new Date(timeWindow.getEnd())) + "), " + s + ", " + JSON.toJSONString(iterable));
			}
		})
		;

((SingleOutputStreamOperator<String>) result).getSideOutput(lateData).print("late");

在上面的代码中,我们设置的窗口为5秒,5秒结束后,窗口计算会被触发,生成第一个计算结果。allowedLateness设置窗口结束后

还要等待长为lateness的时间。如果某个迟到元素归属窗口的结束时间 + lateness > watermark 时间,该元素仍然会被加入到该窗

口中。每新到一个迟到数据,迟到数据被加入WindowFunction的缓存中,窗口的Trigger会触发一次FIRE,窗口函数被重新调用一

次,计算结果得到一次更新。否则会被计入迟到元素。

需要注意的是,使用了allowedLateness可能会导致两个窗口合并成一个窗口。

<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
发出的红包

打赏作者

magic_kid_2010

你的支持将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值