Watermark 是用于处理事件时间的一种机制,用于表示事件时间流的进展。在流处理中,由于事件到达的顺序和延迟,系统需要一种机制来衡量事件时间的进展,以便正确触发窗口操作等。Watermark 就是用来标记事件时间的进展情况的一种特殊数据元素。
Watermark 的生成方式通常是由系统根据数据流中的事件来自动推断生成的。一般来说,系统会根据事件时间戳和一定的策略来生成 Watermark,以此来表示事件时间的进展。在 Flink 中,通常会有内置的 Watermark 生成器或者用户自定义的生成器来实现这个功能。
当一个 Watermark 被生成后,它会被发送到流处理的所有并行任务中。任务会根据接收到的 Watermark,将小于或等于 Watermark 的事件时间的数据触发相关操作(如窗口计算),以此来确保计算的正确性。
优点:
缺点:
Apache Flink中的水印(Watermark)是事件时间处理的核心组件之一,它用于解决无序事件流中的事件时间问题。水印是一种元数据,用于告知系统事件时间流的进度,从而使系统能够在处理延迟的数据时做出正确的决策。
以下是Flink中水印的核心组件:
extractTimestamp()
用于提取事件时间戳,getCurrentWatermark()
用于生成当前水印。水印的核心作用在于解决事件时间处理中的乱序问题,通过适当的水印策略和生成机制,可以有效地处理延迟数据和乱序数据,保证数据处理的准确性和时效性。
在 Apache Flink 中,提供了一些内置的 Watermark 生成器,这些生成器可以用于简化在流处理中的 Watermark 管理。以下是一些常用的内置 Watermark 生成器:
BoundedOutOfOrdernessTimestampExtractor:
描述: 这是 Flink 内置的基于有界乱序时间的 Watermark 生成器。
用法: 用户可以通过指定最大允许的乱序时间来创建一个 BoundedOutOfOrdernessTimestampExtractor
实例。通常情况下,用户需要实现 extractTimestamp
方法,从事件中提取事件时间戳。
示例:
public class MyTimestampExtractor extends BoundedOutOfOrdernessTimestampExtractor {
public MyTimestampExtractor(Time maxOutOfOrderness) {
super(maxOutOfOrderness);
}
@Override
public long extractTimestamp(MyEvent event) {
return event.getTimestamp();
}
}
AscendingTimestampExtractor:
描述: 这是一个简单的 Watermark 生成器,适用于按照事件时间戳升序排列的数据流。
用法: 用户只需实现 extractAscendingTimestamp
方法,从事件中提取事件时间戳。
示例:
public class MyAscendingTimestampExtractor extends AscendingTimestampExtractor {
@Override
public long extractAscendingTimestamp(MyEvent event) {
return event.getTimestamp();
}
}
AssignerWithPunctuatedWatermarks:
描述: 这是一种特殊类型的 Watermark 生成器,它可以基于某些事件的属性产生 Watermark。
用法: 用户需要实现 checkAndGetNextWatermark
方法,根据事件的某些属性来判断是否生成 Watermark。
示例:
public class MyPunctuatedWatermarkAssigner implements AssignerWithPunctuatedWatermarks {
@Override
public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
return element.getTimestamp();
}
@Override
public Watermark checkAndGetNextWatermark(MyEvent lastElement, long extractedTimestamp) {
// 根据 lastElement 的某些属性判断是否生成 Watermark
if (lastElement.getProperty() > threshold) {
return new Watermark(extractedTimestamp);
}
return null; // 如果不生成 Watermark,则返回 null
}
}
这些内置的 Watermark 生成器提供了灵活性和方便性,使得在 Flink 中实现基于事件时间的处理变得更加容易。根据具体的业务需求和数据特征,可以选择合适的 Watermark 生成器来确保准确的事件时间处理。
在Apache Flink 1.18中,水印(Watermark)是事件时间处理的核心组件,用于解决事件时间流处理中的乱序和延迟数据的问题。下面是一些Flink 1.18中集成Watermark水印的应用场景:
总的来说,Flink 1.18中集成Watermark水印的应用场景涵盖了广泛的实时数据处理领域,包括流式窗口操作、处理乱序数据、事件时间窗口计算、处理迟到的数据以及实时数据监控和异常检测等方面。Watermark作为事件时间处理的核心组件,为Flink提供了处理实时数据流的强大功能,能够确保数据处理的准确性和时效性。
Apache Flink 中水印(Watermark)的使用是关键的,特别是在处理事件时间(Event Time)数据时。水印是一种机制,用于处理无序事件流,并确保在执行窗口操作时数据的完整性和正确性。以下是在使用 Flink 1.18 中水印的一些注意事项:
总的来说,水印在 Flink 中的使用是非常重要的,它能够确保在处理事件时间数据时保持数据的完整性和正确性。因此,在设计和部署 Flink 作业时,需要特别注意水印的生成和处理,以确保作业能够正确运行并获得良好的性能表现。
当涉及到事件时间处理时,延迟和乱序是非常常见的情况。下面是一个简单的案例,演示了在事件时间处理中可能遇到的延迟和乱序问题。
假设我们有一个用于监控网站用户访问的实时数据流。每个事件都包含用户ID、访问时间戳和访问的网页URL。我们想要计算每个用户在每小时内访问的不同网页数量。
考虑到网络传输和数据处理可能会引入延迟和乱序,我们的数据流可能如下所示:
Event 1: {UserID: 1, Timestamp: 12:00:05, URL: "example.com/page1"}
Event 2: {UserID: 2, Timestamp: 12:00:10, URL: "example.com/page2"}
Event 3: {UserID: 1, Timestamp: 12:00:15, URL: "example.com/page2"}
Event 4: {UserID: 1, Timestamp: 11:59:58, URL: "example.com/page3"} <-- 延迟
Event 5: {UserID: 2, Timestamp: 12:00:02, URL: "example.com/page4"} <-- 乱序
在这个示例中,Event 4由于延迟而晚于其他事件到达,而Event 5由于乱序而在其本应到达的时间之前到达。
如果没有使用水印机制,Flink 可能会错误地将 Event 4 的数据统计到 12:00:00 ~ 12:01:00 的窗口中,这是因为 Flink 默认情况下是根据接收到事件的时间来进行处理的,而不是根据事件实际发生的事件时间。
在上述案例中,Flink 的水印(Watermark)机制通过指示事件时间的上限,帮助系统确定事件时间窗口的边界。水印本质上是一种元数据,它告知 Flink 在某个时间点之前的数据已经全部到达。
下面简要说明水印如何在案例中发挥作用:
综合来说,水印帮助 Flink 在事件时间处理中正确处理延迟和乱序的数据,确保窗口操作的准确性和完整性。通过逐渐推进水印,系统能够在事件时间轴上有序地进行处理,而不会受到延迟和乱序数据的影响。
假设我们有以下十条乱序的事件数据,每条数据包含事件时间戳和相应的值:
事件时间戳(毫秒) 值
1000 10
2000 15
3000 12
1500 8
2500 18
1200 6
1800 14
4000 20
3500 16
3200 9
我们将使用Watermark来处理这些数据,并进行窗口统计。假设窗口大小为2秒,最大乱序时间为1秒。
使用Watermark前的统计:
使用Watermark后的统计:
Watermark的计算过程如下: Watermark = max(当前Watermark, 当前事件时间 - 最大乱序时间)
在这个例子中,我们设定最大乱序时间为1秒,即1000毫秒。
Watermark确定了什么时候触发窗口统计。在本例中,当Watermark超过窗口的结束时间时,窗口将被关闭,并进行统计。因此,Watermark确保了即使在乱序数据的情况下,窗口统计也能够按照正确的事件时间顺序进行。
为了更清晰地展示Watermark的影响,以下是每个事件被处理时的Watermark状态和窗口统计的结果:
事件时间戳(毫秒) 值 Watermark 窗口统计结果
1000 10 0 10
2000 15 1000 25
3000 12 2000 27
1500 8 2000 27
2500 18 2000 30
1200 6 2000 30
1800 14 2000 32
4000 20 3000 36
3500 16 3000 36
3200 9 3000 36
这里的窗口统计结果是在Watermark触发时计算的。在Watermark超过窗口结束时间时,窗口会被关闭,并进行统计。
4.0.0
com.xsy
aurora_flink_connector_file
1.0-SNAPSHOT
11
3.8.1
UTF-8
UTF-8
org.apache.logging.log4j
log4j-slf4j-impl
2.17.1
org.apache.logging.log4j
log4j-api
2.17.1
org.apache.logging.log4j
log4j-core
2.17.1
com.alibaba
fastjson
1.2.75
org.apache.flink
flink-connector-files
1.18.0
org.apache.flink
flink-java
1.18.0
org.apache.flink
flink-streaming-scala_2.12
1.18.0
org.apache.flink
flink-clients
1.18.0
${project.name}
src/main/resources
src/main/java
**/*.xml
org.apache.maven.plugins
maven-shade-plugin
3.1.1
package
shade
org.apache.flink:force-shading
org.google.code.flindbugs:jar305
org.slf4j:*
org.apache.logging.log4j:*
*:*
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
org.aurora.KafkaStreamingJob
org.springframework.boot
spring-boot-maven-plugin
${spring.boot.version}
true
${project.build.finalName}
repackage
maven-compiler-plugin
${maven.plugin.version}
${java.version}
UTF-8
-parameters
rootLogger.level=INFO
rootLogger.appenderRef.console.ref=ConsoleAppender
appender.console.name=ConsoleAppender
appender.console.type=CONSOLE
appender.console.layout.type=PatternLayout
appender.console.layout.pattern=%d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n
log.file=D:\\tmprootLogger.level=INFO
rootLogger.appenderRef.console.ref=ConsoleAppender
appender.console.name=ConsoleAppender
appender.console.type=CONSOLE
appender.console.layout.type=PatternLayout
appender.console.layout.pattern=%d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n
log.file=D:\\tmp
package com.aurora.demo;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.common.eventtime.TimestampAssigner;
import org.apache.flink.api.common.eventtime.TimestampAssignerSupplier;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Random;
/**
* 描述:Flink集成Watermark水印
*
* @author 浅夏的猫
* @version 1.0.0
* @date 2024-02-08 10:31:40
*/
public class WatermarkStreamingJob {
private static final Logger logger = LoggerFactory.getLogger(WatermarkStreamingJob.class);
public static void main(String[] args) throws Exception {
// 创建 执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 自定义数据源,每隔1000ms下发一条数据
SourceFunction dataSource = new SourceFunction<>() {
private volatile boolean running = true;
@Override
public void run(SourceContext sourceContext) throws Exception {
while (running) {
long timestamp = System.currentTimeMillis();
timestamp = timestamp - new Random().nextInt(11) + 10;
// 将时间戳转换为 LocalDateTime 对象
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
// 定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化日期时间对象为指定格式的字符串
String format = formatter.format(dateTime);
JSONObject dataObj = new JSONObject();
int transId = 8;
dataObj.put("userId", "user_" + transId);
dataObj.put("timestamp", timestamp);
dataObj.put("datetime", format);
dataObj.put("url", "example.com/page" + transId);
logger.info("数据源url={},用户={},交易时间={},系统时间={}", "example.com/page" + transId, "user_" + transId, format);
Thread.sleep(1000);
sourceContext.collect(dataObj);
}
}
@Override
public void cancel() {
running = false;
}
};
//创建水印策略处理事件发生时间
TimestampAssignerSupplier timestampAssignerSupplier = new TimestampAssignerSupplier() {
@Override
public TimestampAssigner createTimestampAssigner(Context context) {
return new TimestampAssigner() {
@Override
public long extractTimestamp(JSONObject element, long recordTimestamp) {
//使用自定义的事件发生时间来做水印,确保窗口统计的是按照我们的时间字段统计,提高准确度,否则默认使用消费时间
return element.getLong("timestamp");
}
};
}
};
//创建数据流
env.addSource(dataSource).assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(1))
.withTimestampAssigner(timestampAssignerSupplier))
//按照url分组
.keyBy(new KeySelector() {
@Override
public Object getKey(JSONObject jsonObject) throws Exception {
return jsonObject.getString("url");
}
})
.window(TumblingEventTimeWindows.of(Time.seconds(2)))
.reduce(new ReduceFunction() {
@Override
public JSONObject reduce(JSONObject reduceResult, JSONObject record) throws Exception {
logger.info("窗口统计url={},用户流水={},次数={}", reduceResult.getString("url"), reduceResult.getString("userId"), reduceResult.getInteger("urlNum") == null ? 1 : reduceResult.getInteger("urlNum"));
int urlNum = reduceResult.getInteger("urlNum") == null ? 1 : reduceResult.getInteger("urlNum");
reduceResult.put("urlNum", urlNum + 1);
return reduceResult;
}
})
.print();
// 执行任务
env.execute("WatermarkStreamingJob");
}
}