目录
package window
import java.text.SimpleDateFormat
import java.time.Duration
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, WatermarkStrategy}
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/**
*
* @PROJECT_NAME: flink1.13
* @PACKAGE_NAME: window
* @author: 赵嘉盟-HONOR
* @data: 2023-05-23 19:51
* @DESCRIPTION
*
*/
object LateDataExample {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
//TODO 处理迟到数据的三种方式
//定义一个测输出流标签
val outputTag=OutputTag[Event]("late-data")
val data1 = env.socketTextStream("localhost", 7777)
.map(data => {
val datas = data.split(",")
Event(datas(0).trim, datas(1).trim, datas(2).trim.toLong)
})
//TODO 一重保证:Watermark
val result = data1.assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness[Event](Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner[Event] {
override def extractTimestamp(t: Event, l: Long): Long = t.timestamp
})).keyBy(_.user).window(TumblingEventTimeWindows.of(Time.seconds(10)))
//TODO 二重保证:指定窗口允许等待时间
.allowedLateness(Time.minutes(1))
//TODO 三重保证:将迟到数据输出到侧输出流
.sideOutputLateData(outputTag)
.aggregate(new AggregateFunction[Event, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(in: Event, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long = ???
}, new ProcessWindowFunction[Long, UrlViewCount, String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Long], out: Collector[UrlViewCount]): Unit = {
out.collect(UrlViewCount(key,
elements.iterator.next(),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getStart),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getEnd)
))
}
})
result.print("result")
data1.print("data ")
result.getSideOutput(outputTag).print("late-data")
env.execute("watermark")
}
}
这段代码是一个使用 Apache Flink 处理流数据的示例,主要演示了如何处理迟到数据。下面我将总结代码功能并拓展相关理论。
代码功能总结
这段代码实现了一个基于事件时间的滚动窗口统计系统,主要功能包括:
-
数据源:从本地 socket 端口 7777 接收输入数据,数据格式为
user,url,timestamp
。 -
数据处理:
- 将输入的字符串解析为
Event
对象 - 为数据流分配时间戳并生成 Watermark(允许 5 秒的乱序)
- 按照用户进行分组,使用 10 秒的滚动事件时间窗口
- 将输入的字符串解析为
-
迟到数据处理:
- 第一重保障:使用 Watermark 机制处理一定程度的乱序数据(5 秒)
- 第二重保障:设置窗口允许等待时间为 1 分钟,即窗口关闭后仍会继续收集迟到数据并更新结果
- 第三重保障:将超过允许等待时间的迟到数据输出到侧输出流
-
结果输出:
- 正常数据的统计结果输出到 "result"
- 原始输入数据输出到 "data"
- 迟到数据输出到 "late-data"
拓展理论
1. 事件时间与 Watermark
在流处理中,时间语义分为:
- 处理时间:数据到达处理系统的时间
- 摄入时间:数据进入 Flink 的时间
- 事件时间:数据实际发生的时间
事件时间处理需要解决乱序数据问题,Watermark 是 Flink 中处理乱序数据的核心机制。它表示一个时间戳,即后续不会再出现早于该时间戳的数据。
2. 迟到数据处理策略
在实际生产环境中,迟到数据是不可避免的,Flink 提供了多种处理策略:
-
Watermark 机制:设置合理的乱序容忍度,确保大部分迟到数据能被正确处理
-
窗口允许等待时间:
.allowedLateness(Time.minutes(1))
窗口关闭后继续接收迟到数据并更新结果,适用于对结果准确性要求较高的场景
-
侧输出流:
.sideOutputLateData(outputTag)
将无法处理的迟到数据单独输出,适用于需要保留所有数据记录的场景
-
状态清理策略:
.withEventTimeOutputWatermarks( WatermarkStrategy .<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(20)) .withIdleness(Duration.ofMinutes(1)) )
对于长时间空闲的流,可以配置 Watermark 生成器进入空闲状态,避免状态无限增长
3. 不同窗口类型对迟到数据的处理差异
不同窗口类型对迟到数据的处理方式有所不同:
- 滚动窗口:迟到数据可能会触发窗口重新计算
- 滑动窗口:迟到数据可能影响多个窗口的结果
- 会话窗口:迟到数据可能会合并两个已关闭的会话窗口
4. 迟到数据处理的权衡
在实际应用中,需要根据业务需求权衡以下因素:
- 结果准确性 vs 处理延迟:允许等待时间越长,结果越准确,但处理延迟也越大
- 状态管理开销:窗口允许等待时间越长,Flink 需要维护的状态就越多
- 下游系统影响:窗口结果的更新可能会对下游系统造成影响,需要考虑幂等性设计
5. 其他相关概念
- 水印生成策略:除了固定延迟策略,Flink 还提供了升序时间戳、自定义水印生成器等策略
- 触发器:控制窗口何时触发计算,可以自定义触发器实现更灵活的触发逻辑
- 增量聚合:使用 AggregateFunction 可以在窗口内进行增量聚合,减少内存占用
以上是对这段代码的功能总结和相关理论的拓展。在实际生产环境中,需要根据业务场景选择合适的时间语义和迟到数据处理策略。