Flink(一)1.12.7或1.13.5详细介绍及本地安装部署、验证
Flink(二)1.13.5二种部署方式(Standalone、Standalone HA )、四种提交任务方式(前两种及session和per-job)验证详细步骤
Flink(三)flink重要概念(api分层、角色、执行流程、执行图和编程模型)及dataset、datastream详细示例入门和提交任务至on yarn运行
Flink(四)介绍Flink的流批一体、transformations的18种算子详细介绍、Flink与Kafka的source、sink介绍
Flink(五)source、transformations、sink的详细示例(一)
Flink(五)source、transformations、sink的详细示例(二)-source和transformation示例
Flink(五)source、transformations、sink的详细示例(三)-sink示例
Flink(六)Flink四大基石之Window详解与详细示例(一)
Flink(六)Flink四大基石之Window详解与详细示例(二)
Flink(七)Flink四大基石之Time和WaterMaker详解与详细示例(watermaker基本使用、kafka作为数据源的watermaker使用示例以及超出最大允许延迟数据的接收实现)
本文详细介绍了eventtime和watermaker,包括watermaker的Flink自带的api实现与自定义的实现,同时以三个示例来展示watermaker的实际可能的应用场景。
本文依赖kafka环境可用。
本文分为四个部分,即EventTime与watermaker介绍、watermaker基本使用、kafka作为数据源的watermaker使用示例以及超出最大允许延迟数据的接收实现。
支持事件时间的流处理器需要一种方法来测量事件时间的进度。例如,当事件时间超过一小时结束时,需要通知构建每小时窗口的窗口运算符,以便操作员可以关闭正在进行的窗口。
事件时间可以独立于处理时间进行。例如,在一个程序中,操作员的当前事件时间可能略微落后于处理时间(考虑接收事件的延迟),而两者以相同的速度进行。另一方面,另一个流程序可能会通过快进已经在 Kafka 主题(或其他消息队列)中缓冲的一些历史数据,在几秒钟的处理中完成数周的事件时间。
Flink 中衡量事件时间进度的机制是水印。水印作为数据流的一部分流动,并带有时间戳 t。水印(t) 声明事件时间已到达该流中的时间 t,这意味着流中不应再有时间戳为 t’ <= t 的元素(即时间戳早于或等于水印的事件)。
下图显示了具有(逻辑)时间戳和内联流动水印的事件流。在此示例中,事件是按顺序排列的(相对于其时间戳),这意味着水印只是流中的周期性标记。
水印对于无序流至关重要,如下图所示,事件不按其时间戳排序。通常,水印是一种声明,即到流中的该点,直到某个时间戳的所有事件都应该已到达。水印到达运算符后,运算符可以将其内部事件时钟提前到水印的值。
水印在source Function处或紧随source Function之后生成。source Function的每个并行子任务通常独立生成其水印。这些水印定义该特定并行源的事件时间。
当水印流经流媒体程序时,当水印到达的时候会触发事件时间的计算(they advance the event time at the operators where they arrive)。每当operator触发(advance)其事件时间时,它都会为其后续运算符在下游生成新的水印。
一些运算符使用多个输入流;例如,union,或 keyBy(…) 或 partition(…) 函数的运算符。此类运算符的当前事件时间是其输入流的事件时间的最小值。当其输入流更新其事件时间时,运算符也会更新。
this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
// start so that our lowest watermark would be Long.MIN_VALUE.
this.maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;
下图显示了流经并行流的事件和水印以及跟踪事件时间的运算符的示例。
某些元素可能会违反水印条件,这意味着即使在 Watermark(t) 发生之后,也会出现更多时间戳为 t’ <= t 的元素。事实上,在许多现实世界的设置中,某些元素可以任意延迟,因此无法指定某个事件时间戳的所有元素发生的时间。此外,即使延迟是有限制的,通常也不希望将水印延迟太多,因为这会导致事件时间窗口的评估延迟太多。
出于这个原因,流应用可能会明确地期望一些后期元素。延迟元素是在系统的事件时钟(由水印指示)已经过了延迟元素时间戳的时间之后到达的元素。有关如何在事件时间窗口中使用延迟元素的搜集(一般用OutputTag)与使用视情况而定。代码示例如下:
DataStream<T> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.<windowed transformation>(<window function>);
Watermaker就是给数据再额外的加的一个时间列,Watermaker是个时间戳。
Watermaker = 数据的事件时间 - 最大允许的延迟时间或乱序时间
Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间
这样可以保证Watermaker水位线会一直上升(变大),不会下降
https://nightlies.apache.org/flink/flink-docs-release-1.12/zh/dev/event_timestamps_watermarks.html
官网链接:https://nightlies.apache.org/flink/flink-docs-release-1.12/zh/dev/event_timestamps_watermarks.html
使用 Flink API 时需要设置一个同时包含 TimestampAssigner 和 WatermarkGenerator 的 WatermarkStrategy。WatermarkStrategy 工具类中也提供了许多常用的 watermark 策略,并且用户也可以在某些必要场景下构建自己的 watermark 策略。WatermarkStrategy 接口如下:
public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T>{
/**
* 根据策略实例化一个可分配时间戳的 {@link TimestampAssigner}。
*/
@Override
TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
/**
* 根据策略实例化一个 watermark 生成器。
*/
@Override
WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
通常情况下,不用实现此接口,而是可以使用 WatermarkStrategy 工具类中通用的 watermark 策略,或者可以使用这个工具类将自定义的 TimestampAssigner 与 WatermarkGenerator 进行绑定。例如,想要使用有界无序(bounded-out-of-orderness)watermark 生成器和一个 lambda 表达式作为时间戳分配器,那么可以按照如下方式实现:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp) -> event.f0);
//其中 TimestampAssigner 的设置与否是可选的,大多数情况下,可以不用去特别指定。例如,当使用 Kafka 或 Kinesis 数据源时,你可以直接从 Kafka/Kinesis 数据源记录中获取到时间戳。
WatermarkStrategy 可以在 Flink 应用程序中的两处使用
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<MyEvent> stream = env.readFile(
myFormat, myFilePath, FileProcessingMode.PROCESS_CONTINUOUSLY, 100,
FilePathFilter.createDefaultFilter(), typeInfo);
DataStream<MyEvent> withTimestampsAndWatermarks = stream
.filter( event -> event.severity() == WARNING )
.assignTimestampsAndWatermarks(<watermark strategy>);
withTimestampsAndWatermarks
.keyBy( (event) -> event.getGroup() )
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.reduce( (a, b) -> a.add(b) )
.addSink(...);
使用 WatermarkStrategy 去获取流并生成带有时间戳的元素和 watermark 的新流时,如果原始流已经具有时间戳或 watermark,则新指定的时间戳分配器将覆盖原有的时间戳和 watermark。
如果数据源中的某一个分区/分片在一段时间内未发送事件数据,则意味着 WatermarkGenerator 也不会获得任何新数据去生成 watermark。
称这类数据源为空闲输入或空闲源。在这种情况下,当某些其他分区仍然发送事件数据的时候就会出现问题。由于下游算子 watermark 的计算方式是取所有不同的上游并行数据源 watermark 的最小值,则其 watermark 将不会发生变化。
为了解决这个问题,可以使用 WatermarkStrategy 来检测空闲输入并将其标记为空闲状态。WatermarkStrategy 为此提供了一个工具接口:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withIdleness(Duration.ofMinutes(1));
可以针对每个事件去生成 watermark。但是由于每个 watermark 都会在下游做一些计算,因此过多的 watermark 会降低程序性能。
TimestampAssigner 是一个可以从事件数据中提取时间戳字段的简单函数,但是 WatermarkGenerator 的编写相对就要复杂一些了,将在接下来的两小节中介绍如何实现此接口。WatermarkGenerator 接口代码如下:
/**
* {@code WatermarkGenerator} 可以基于事件或者周期性的生成 watermark。
*
* 注意: WatermarkGenerator 将以前互相独立的 {@code AssignerWithPunctuatedWatermarks}
* 和 {@code AssignerWithPeriodicWatermarks} 一同包含了进来。
*/
@Public
public interface WatermarkGenerator<T> {
/**
* 每来一条事件数据调用一次,可以检查或者记录事件的时间戳,或者也可以基于事件数据本身去生成 watermark。
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* 周期性的调用,也许会生成新的 watermark,也许不会。
*
* 调用此方法生成 watermark 的间隔时间由 {@link ExecutionConfig#getAutoWatermarkInterval()} 决定。
*/
void onPeriodicEmit(WatermarkOutput output);
}
watermark 的生成方式本质上是有两种:周期性生成和标记生成。
周期性生成器会观察流事件数据并定期生成 watermark(其生成可能取决于流数据,或者完全基于处理时间)。
生成 watermark 的时间间隔(每 n 毫秒)可以通过 ExecutionConfig.setAutoWatermarkInterval(…) 指定。每次都会调用生成器的 onPeriodicEmit() 方法,如果返回的 watermark 非空且值大于前一个 watermark,则将发出新的 watermark。
如下是两个使用周期性 watermark 生成器的简单示例。
Flink 已经附带了 BoundedOutOfOrdernessWatermarks,它实现了 WatermarkGenerator,其工作原理与下面的 BoundedOutOfOrdernessGenerator 相似。可以在这里参阅如何使用它的内容。
/**
* 该 watermark 生成器可以覆盖的场景是:数据源在一定程度上乱序。
* 即某个最新到达的时间戳为 t 的元素将在最早到达的时间戳为 t 的元素之后最多 n 毫秒到达。
*/
public class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<MyEvent> {
private final long maxOutOfOrderness = 3500; // 3.5 秒
private long currentMaxTimestamp;
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 发出的 watermark = 当前最大时间戳 - 最大乱序时间
output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
}
}
/**
* 该生成器生成的 watermark 滞后于处理时间固定量。它假定元素会在有限延迟后到达 Flink。
*/
public class TimeLagWatermarkGenerator implements WatermarkGenerator<MyEvent> {
private final long maxTimeLag = 5000; // 5 秒
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// 处理时间场景下不需要实现
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
}
}
标记 watermark 生成器观察流事件数据并在获取到带有 watermark 信息的特殊事件元素时发出 watermark。
如下是实现标记生成器的方法,当事件带有某个指定标记时,该生成器就会发出 watermark:
public class PunctuatedAssigner implements WatermarkGenerator<MyEvent> {
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
if (event.hasWatermarkMarker()) {
output.emitWatermark(new Watermark(event.getWatermarkTimestamp()));
}
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// onEvent 中已经实现
}
}
当使用 Apache Kafka 连接器作为数据源时,每个 Kafka 分区可能有一个简单的事件时间模式(递增的时间戳或有界无序)。然而,当使用 Kafka 数据源时,多个分区常常并行使用,因此交错来自各个分区的事件数据就会破坏每个分区的事件时间模式(这是 Kafka 消费客户端所固有的)。
在这种情况下,你可以使用 Flink 中可识别 Kafka 分区的 watermark 生成机制。使用此特性,将在 Kafka 消费端内部针对每个 Kafka 分区生成 watermark,并且不同分区 watermark 的合并方式与在数据流 shuffle 时的合并方式相同。
例如,如果每个 Kafka 分区中的事件时间戳严格递增,则使用时间戳单调递增按分区生成的 watermark 将生成完美的全局 watermark。注意,在示例中未使用 TimestampAssigner,而是使用了 Kafka 记录自身的时间戳。
下图展示了如何使用单 kafka 分区 watermark 生成机制,以及在这种情况下 watermark 如何通过 dataflow 传播。
//kafka数据源示例,没有使用withTimestampAssigner
FlinkKafkaConsumer<MyType> kafkaSource = new FlinkKafkaConsumer<>("myTopic", schema, props);
kafkaSource.assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(20)));
DataStream<MyType> stream = env.addSource(kafkaSource);
//非kafka数据源示例,使用了withTimestampAssigner
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp) -> event.f0);
一般情况下,在将 watermark 转发到下游之前,需要算子对其进行触发的事件完全进行处理。例如,WindowOperator 将首先计算该 watermark 触发的所有窗口数据,当且仅当由此 watermark 触发计算进而生成的所有数据被转发到下游之后,其才会被发送到下游。换句话说,由于此 watermark 的出现而产生的所有数据元素都将在此 watermark 之前发出。
相同的规则也适用于 TwoInputStreamOperator。但是,在这种情况下,算子当前的 watermark 会取其两个输入的最小值。
每5s统计一次地铁进站每个入口人数,最多接受延迟3s的数据
数据结构:进站口、人数和进入时间
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author alanchan
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Subway {
private String sNo;
private Integer userCount;
private Long enterTime;
}
import java.time.Duration;
import java.util.Random;
import java.util.UUID;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
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;
/**
* @author alanchan
* 每10s统计一次地铁进站每个入口人数
*/
public class WatermakerDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
FastDateFormat df = FastDateFormat.getInstance("HH:mm:ss");
// env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
// source
DataStreamSource<Subway> subwayDS = env.addSource(new SourceFunction<Subway>() {
private boolean flag = true;
@Override
public void run(SourceContext<Subway> ctx) throws Exception {
Random random = new Random();
while (flag) {
String sNo = "No"+random.nextInt(3);
int userCount = random.nextInt(100);
// 模拟延迟数据
long eventTime = System.currentTimeMillis() - random.nextInt(10) * 1000;
Subway subway = new Subway(sNo, userCount, eventTime);
System.err.println(subway + " ,格式化后时间 " + df.format(subway.getEnterTime()));
ctx.collect(subway);
Thread.sleep(1000);
}
}
@Override
public void cancel() {
flag = false;
}
});
// transformation
// 设置Watermaker = 当前最大的事件时间 - 最大允许的延迟时间或乱序时间
SingleOutputStreamOperator<Subway> subwayWithWatermark = subwayDS
.assignTimestampsAndWatermarks(WatermarkStrategy.<Subway>forBoundedOutOfOrderness(Duration.ofSeconds(3))// 指定最大允许的延迟时间
.withTimestampAssigner((subway, timestamp) -> subway.getEnterTime()));// 指定eventtime事件时间列
// 计算窗口
SingleOutputStreamOperator<Subway> result = subwayWithWatermark
.keyBy(Subway::getSNo)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.sum("userCount");
// sink
result.print();
// execute
env.execute();
}
}
输出结果,按照预期计算出了结果
Subway(sNo=No0, userCount=7, enterTime=1689148937760) ,格式化后时间 16:02:17
Subway(sNo=No1, userCount=93, enterTime=1689148933782) ,格式化后时间 16:02:13
Subway(sNo=No2, userCount=21, enterTime=1689148940783) ,格式化后时间 16:02:20
10> Subway(sNo=No1, userCount=93, enterTime=1689148933782)
Subway(sNo=No0, userCount=7, enterTime=1689148936784) ,格式化后时间 16:02:16
Subway(sNo=No0, userCount=53, enterTime=1689148944788) ,格式化后时间 16:02:24
10> Subway(sNo=No0, userCount=14, enterTime=1689148937760)
Subway(sNo=No2, userCount=66, enterTime=1689148944790) ,格式化后时间 16:02:24
Subway(sNo=No2, userCount=97, enterTime=1689148944803) ,格式化后时间 16:02:24
Subway(sNo=No2, userCount=79, enterTime=1689148946807) ,格式化后时间 16:02:26
Subway(sNo=No2, userCount=83, enterTime=1689148945821) ,格式化后时间 16:02:25
Subway(sNo=No0, userCount=84, enterTime=1689148941836) ,格式化后时间 16:02:21
Subway(sNo=No2, userCount=50, enterTime=1689148947852) ,格式化后时间 16:02:27
Subway(sNo=No1, userCount=10, enterTime=1689148942864) ,格式化后时间 16:02:22
Subway(sNo=No0, userCount=20, enterTime=1689148944866) ,格式化后时间 16:02:24
Subway(sNo=No2, userCount=1, enterTime=1689148945877) ,格式化后时间 16:02:25
Subway(sNo=No0, userCount=62, enterTime=1689148953888) ,格式化后时间 16:02:33
3> Subway(sNo=No2, userCount=184, enterTime=1689148940783)
10> Subway(sNo=No0, userCount=157, enterTime=1689148944788)
3> Subway(sNo=No2, userCount=213, enterTime=1689148946807)
10> Subway(sNo=No1, userCount=10, enterTime=1689148942864)
。。。。。。
注意该示例中,发送数据时不需要eventtime,flink会以kafka发送数据的时间戳为eventtime,也不需要withTimestampAssigner指定eventtime,详见实现源码。
每10s统计一次地铁进站每个入口人数,最多接受延迟3s的数据
数据结构:进站口、人数
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka_2.12artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-sql-connector-kafka_2.12artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-jdbc_2.12artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-csvartifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-jsonartifactId>
<version>${flink.version}version>
dependency>
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author alanchan
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Subway {
private String sNo;
private Integer userCount;
}
import java.time.Duration;
import java.util.Properties;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.util.serialization.SimpleStringSchema;
/**
* @author alanchan
*
*/
public class KafkaWatermakerDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
// source
// 准备kafka连接参数
Properties props = new Properties();
props.setProperty("bootstrap.servers", "server1:9092");
props.setProperty("group.id", "flink");
props.setProperty("auto.offset.reset", "latest");
props.setProperty("flink.partition-discovery.interval-millis", "5000");
props.setProperty("enable.auto.commit", "true");
props.setProperty("auto.commit.interval.ms", "2000");
// 使用连接参数创建FlinkKafkaConsumer/kafkaSource
FlinkKafkaConsumer<String> kafkaSource = new FlinkKafkaConsumer<String>("t_kafkasource", new SimpleStringSchema(), props);
// 使用kafkaSource
DataStream<Subway> subwayDS = env.addSource(kafkaSource).map(new MapFunction<String, Subway>() {
@Override
public Subway map(String value) throws Exception {
String[] arr = value.split(",");
return new Subway(arr[0], Integer.parseInt(arr[1]));
}
});
// transformation
// 设置Watermaker = 当前最大的事件时间 - 最大允许的延迟时间或乱序时间
SingleOutputStreamOperator<Subway> subwayWithWatermark = subwayDS
.assignTimestampsAndWatermarks(WatermarkStrategy.<Subway>forBoundedOutOfOrderness(Duration.ofSeconds(3))// 指定最大允许的延迟时间
);
// 计算窗口
SingleOutputStreamOperator<Subway> result = subwayWithWatermark.keyBy(Subway::getSNo).window(TumblingEventTimeWindows.of(Time.seconds(10))).sum("userCount");
// sink
result.print();
// execute
env.execute();
}
}
1、启动kafka,创建topic
2、启动应用程序
3、在kafka命令行中输入符合格式要求的数据
4、观察应用程序的控制台输出
启动kafka、创建topic、启动应用程序不再赘述,如果不清楚的参考本人kafka专栏。
1、在kafka命令控制台输入数据
kafka-console-producer.sh --broker-list server1:9092 --topic t_kafkasource
[alanchan@server1 onekeystart]$ kafka-console-producer.sh --broker-list server1:9092 --topic t_kafkasource
>1,2
>1,3
>1,4
>1,5
>1,6
>1,7
>2,4
>2,6
>2,8
>3,6
2、观察应用程序中的控制台输出
7> Subway(sNo=1, userCount=20)
4> Subway(sNo=2, userCount=18)
7> Subway(sNo=1, userCount=7)
每10s统计一次地铁进站每个入口人数,最多接受延迟3s的数据,超过可接受范围则另外接收。
一般而言,针对延迟超过计算窗口的数据处理方式不同,视具体的情况而定。
本示例仅仅是打印出来。
数据结构:进站口、人数和进入时间
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author alanchan
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Subway {
private String sNo;
private Integer userCount;
private Long enterTime;
}
import java.time.Duration;
import java.util.Random;
import java.util.UUID;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext;
import org.apache.flink.streaming.api.scala.OutputTag;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
/**
* @author alanchan
*
*/
public class WatermakerLatenessDemo {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
FastDateFormat df = FastDateFormat.getInstance("HH:mm:ss");
// env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
// source
DataStreamSource<Subway> subwayDS = env.addSource(new SourceFunction<Subway>() {
private boolean flag = true;
@Override
public void run(SourceContext<Subway> ctx) throws Exception {
Random random = new Random();
while (flag) {
String sNo = "No" + random.nextInt(3);
int userCount = random.nextInt(100);
// 模拟延迟数据
long eventTime = System.currentTimeMillis() - random.nextInt(20) * 1000;
Subway subway = new Subway(sNo, userCount, eventTime);
System.err.println(subway + " ,格式化后时间 " + df.format(subway.getEnterTime()));
ctx.collect(subway);
Thread.sleep(1000);
}
}
@Override
public void cancel() {
flag = false;
}
});
// transformation
// 设置最大允许延迟时间3s
SingleOutputStreamOperator<Subway> orderDSWithWatermark = subwayDS.assignTimestampsAndWatermarks(
WatermarkStrategy.<Subway>forBoundedOutOfOrderness(Duration.ofSeconds(3)).withTimestampAssigner((subway, timestamp) -> subway.getEnterTime())// 指定事件时间列
);
// 接收延迟超过允许范围内的数据,供其他方式处理
OutputTag<Subway> latenessData = new OutputTag<Subway>("seriousLateData", TypeInformation.of(Subway.class));
SingleOutputStreamOperator<Subway> result1 = orderDSWithWatermark.keyBy(Subway::getSNo).window(TumblingEventTimeWindows.of(Time.seconds(10)))
.allowedLateness(Time.seconds(3)).sideOutputLateData(latenessData).sum("userCount");
DataStream<Subway> result2 = result1.getSideOutput(latenessData);
// sink
result1.print("延迟(含正常)在计算窗口内数据");
result2.print("延迟不在计算窗口内数据");
// execute
env.execute();
}
}
验证比较简单,就是由系统自己生成数据,然后观察应用程序的控制台的输出是否与预期一致,经验证,本示例与预期一致。
Subway(sNo=No1, userCount=44, enterTime=1689152768384) ,格式化后时间 17:06:08
Subway(sNo=No1, userCount=24, enterTime=1689152764407) ,格式化后时间 17:06:04
Subway(sNo=No1, userCount=29, enterTime=1689152774418) ,格式化后时间 17:06:14
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=68, enterTime=1689152768384)
Subway(sNo=No1, userCount=83, enterTime=1689152767430) ,格式化后时间 17:06:07
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=151, enterTime=1689152768384)
Subway(sNo=No0, userCount=19, enterTime=1689152759435) ,格式化后时间 17:05:59
延迟不在计算窗口内数据:10> Subway(sNo=No0, userCount=19, enterTime=1689152759435)
Subway(sNo=No1, userCount=32, enterTime=1689152760443) ,格式化后时间 17:06:00
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=183, enterTime=1689152768384)
Subway(sNo=No0, userCount=56, enterTime=1689152775459) ,格式化后时间 17:06:15
Subway(sNo=No1, userCount=12, enterTime=1689152778472) ,格式化后时间 17:06:18
Subway(sNo=No1, userCount=26, enterTime=1689152776475) ,格式化后时间 17:06:16
Subway(sNo=No0, userCount=49, enterTime=1689152772488) ,格式化后时间 17:06:12
Subway(sNo=No2, userCount=93, enterTime=1689152767500) ,格式化后时间 17:06:07
延迟不在计算窗口内数据:3> Subway(sNo=No2, userCount=93, enterTime=1689152767500)
Subway(sNo=No1, userCount=26, enterTime=1689152779502) ,格式化后时间 17:06:19
Subway(sNo=No2, userCount=63, enterTime=1689152782512) ,格式化后时间 17:06:22
Subway(sNo=No1, userCount=73, enterTime=1689152775526) ,格式化后时间 17:06:15
Subway(sNo=No0, userCount=46, enterTime=1689152783539) ,格式化后时间 17:06:23
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No0, userCount=105, enterTime=1689152775459)
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=166, enterTime=1689152774418)
Subway(sNo=No2, userCount=72, enterTime=1689152779552) ,格式化后时间 17:06:19
延迟(含正常)在计算窗口内数据:3> Subway(sNo=No2, userCount=72, enterTime=1689152779552)
Subway(sNo=No0, userCount=43, enterTime=1689152774553) ,格式化后时间 17:06:14
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No0, userCount=148, enterTime=1689152775459)
Subway(sNo=No2, userCount=1, enterTime=1689152781567) ,格式化后时间 17:06:21
Subway(sNo=No2, userCount=49, enterTime=1689152775582) ,格式化后时间 17:06:15
延迟(含正常)在计算窗口内数据:3> Subway(sNo=No2, userCount=121, enterTime=1689152779552)
Subway(sNo=No1, userCount=4, enterTime=1689152782591) ,格式化后时间 17:06:22
Subway(sNo=No2, userCount=79, enterTime=1689152778604) ,格式化后时间 17:06:18
延迟(含正常)在计算窗口内数据:3> Subway(sNo=No2, userCount=200, enterTime=1689152779552)
Subway(sNo=No2, userCount=11, enterTime=1689152794608) ,格式化后时间 17:06:34
延迟(含正常)在计算窗口内数据:3> Subway(sNo=No2, userCount=64, enterTime=1689152782512)
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=4, enterTime=1689152782591)
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No0, userCount=46, enterTime=1689152783539)
Subway(sNo=No1, userCount=47, enterTime=1689152784620) ,格式化后时间 17:06:24
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No1, userCount=51, enterTime=1689152782591)
Subway(sNo=No0, userCount=12, enterTime=1689152788634) ,格式化后时间 17:06:28
延迟(含正常)在计算窗口内数据:10> Subway(sNo=No0, userCount=58, enterTime=1689152783539)
Subway(sNo=No0, userCount=71, enterTime=1689152790634) ,格式化后时间 17:06:30
Subway(sNo=No0, userCount=83, enterTime=1689152799635) ,格式化后时间 17:06:39
Subway(sNo=No0, userCount=11, enterTime=1689152799649) ,格式化后时间 17:06:39
Subway(sNo=No1, userCount=77, enterTime=1689152786650) ,格式化后时间 17:06:26
延迟不在计算窗口内数据:10> Subway(sNo=No1, userCount=77, enterTime=1689152786650)
Subway(sNo=No0, userCount=6, enterTime=1689152802662) ,格式化后时间 17:06:42
Subway(sNo=No1, userCount=87, enterTime=1689152791668) ,格式化后时间 17:06:31
以上,详细介绍了eventtime和watermaker,包括watermaker的Flink自带的api实现与自定义的实现,同时以三个示例来展示watermaker的实际可能的应用场景。