1、案例(为什么要有watermark)
在理解Watermark之前我们先看个案例
我们有一个带有时间戳的事件流,这些事件并不是按顺序到达的。图中的数字表示事件发生的时间。第一个事件在时间4到达,后面跟着一个发生时间更早的事件(时间2),以此类推
注意这是一个基于事件时间处理的例子,这意味着时间戳反映的是事件发生的时间,而不是事件处理的时间。基于事件时间处理的强大之处在于创建流处理程序无论是处理实时的数据还是重新处理历史的数据,都能保证结果的一致。
需求:将收集到的数据流按照事件时间排序?
说明一
数据流中的第一个元素是时间4,但是我们不能直接将它作为排序后数据流的第一个元素输出。因为数据可能是乱序到达的,可能还有一个更早发生的数据还没有到达。事实上我们可以预见这个流的的一些未来,我们知道新数据流至少要等到时间2到达后才输出结果。
说明二
如果我们做错了(没有更早的数据了),我们可能会永远等待下去。首先,我们应用程序看到的第一个事件是时间 4,然后是时间 2 。是否会有一个比时间 2 更早的数据到达呢?也许会,也许不会。我们可以一直等下去,但可能会永远等不到时间 1
我们不能无限制的等待下去,我们必须果敢地输出时间 2 作为排序后新数据流的第一个结果。
说明三
我们需要定义某种策略来决定什么时候不再去停止等待更早数据的到来。(这就是我们接下来要说的watermark)
2、Watermark的理解
2.1 本质
Watermark是基于已经收集的消息来估算是否还有消息未到达,本质上是一个时间戳。时间戳反映的是事件发生的事件,而不是事件处理的时间。
2.2 作用
用于处理乱序事件(乱序:Flink接收到的事件的顺序和Event time顺序不一致)
2.3 使用场景
主要是在使用window计算的时候
使用Flink消费kafa多个分区的数据的时候,无法保证有序,在window计算的时候,我们又不能无限期的等下去,必须有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制就是watermark 。
上图中,我们设置的允许最大延迟到达时间为2s,所以时间戳为7s的事件对于的watermar是5s,时间戳为12s的事件的Watermark是10s,如果我们的窗口1是1s5s,窗口2是610s,那么时间戳为7s的事件达到时间的Watermark恰好触发窗口1,时间戳为12s事件到达时间的Watermark恰好触发窗口2。
Watermark就是触发前一窗口的‘关窗时间’,一旦触发关门那么当前时刻为准在窗口范围的所有数据都会收入窗中。
2.4 特点
- Watermark是一个时间戳,它表示小于该时间戳的事件都已经到达了
- Watermark一般情况在源位置产生(也可以在流图中的其他节点产生),通过流节点
- Watermark也是StreamElement,和普通数据一起在算子之间传递
- Watermark可以触发窗口计算,时间戳为Long.MAX_VALUE表示算子后续没有任何数据
2.5 分类
1、定期水位线( Assigner with periodic watermarks)
按照固定时间间隔生成新的水位线,不管是否有新的消息抵达,水位线提升的时间间隔是由用户设置的,在两次水位线提升间隔内会有一部分消息流入**,用户可以根据这部分数据来计算出新的水位线**。举个例子,最简单的水位线算法就是取目前为止最大的事件时间,然而这种方式比较暴力,对乱序事件的容忍程度比较低,容易出现大量迟到事件。
应用定期水位线需要实现AssignerWithPeriodicWatermarks API ,以下是Flink官网提供的定期水位线的实现例子。
class BoundedOutOfOrdernessGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
val maxOutOfOrderness = 3500L; // 3.5 seconds
var currentMaxTimestamp: Long;
override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
val timestamp = element.getCreationTime()
currentMaxTimestamp = max(timestamp, currentMaxTimestamp)
timestamp;
}
override def getCurrentWatermark(): Watermark = {
// return the watermark as current highest timestamp minus the out-of-orderness bound
new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
}
其中extractTimestamp用于从消息中提取事件时间,而getCurrentWatermark用于生成新的水位线,新的水位线只有大于当前水位线才是有效的。
注:周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。
2、标点水位线(Assigner with punctuated watermarks)
标点水位线(Punctuated Watermark)通过数据流中某些特殊标记事件来触发新水位线的生成。这种方式下窗口的触发与时间无关,而是决定于何时收到标记事件。
应用标点水位线需要实现AssignerWithPunctuatedWatermarks API
,以下是 Flink 官网提供的标点水位线的实现例子。
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[MyEvent] {
override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
element.getCreationTime
}
override def checkAndGetNextWatermark(lastElement: MyEvent, extractedTimestamp: Long): Watermark = {
if (element.hasWatermarkMarker()) new Watermark(extractedTimestamp) else null
}
}
**其中extractTimestamp用于从消息中提取事件时间,checkAndGetNextWatermark用于检查事件是否标点事件,若是则生成新的水位线。**不同于定期水位线定时调用getCurrentWatermark,标点水位线是每接受一个事件就需要调用checkAndGetNextWatermark,若返回值非 null 且新水位线大于当前水位线,则触发窗口计算。
数据流中每一个递增的EventTime都会产生一个Watermark。在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成
3、迟到事件
虽说水位线表明着早于它的事件不应该再出现,但是如上文所讲,接收到水位线以前的消息是不可避免的,这就是所谓的迟到事件。实际上迟到事件是乱序事件的特例,和一般乱序事件不同的是他们的乱序程序超出了水位线的预计,导致窗口在它们到达之间已经关闭。
迟到事件出现窗口已经关闭并产生出了计算结果,因此处理的方法有3种:
- 重新激活已经关闭的窗口并重新计算以修正结果
- 将迟到事件手机起来另外处理
- 将迟到事件视为错误消息并丢弃
Flink 默认的处理方式是第3种直接丢弃,其他两种方式分别使用Side Output和Allowed Lateness。
- Side Output机制可以将迟到事件单独放入一个数据流分支,这会作为 window 计算结果的副产品,以便用户获取并对其进行特殊处理。
- Allowed Lateness机制允许用户设置一个允许的最大迟到时长。Flink 会再窗口关闭后一直保存窗口的状态直至超过允许迟到时长,这期间的迟到事件不会被丢弃,而是默认会触发窗口重新计算。因为保存窗口状态需要额外内存**,并且如果窗口计算使用了 ProcessWindowFunction API 还可能使得每个迟到事件触发一次窗口的全量计算,代价比较大,所以允许迟到时长不宜设得太长,迟到事件也不宜过多,否则应该考虑降低水位线提高的速度或者调整算法**。
3、实例
需求: 统计每30内单词出行的次数(滑动窗口、消息的最大延迟时间是5秒)
1、准备环境:
IDEA、maven 、Flink 1.9.0 、scala 2.11.8 、windows nc
dependency
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
2、实现代码
scala版
package com.szsk.yulq.test
import org.apache.flink.api.common.functions.{FilterFunction, MapFunction}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
object TumblingWindowWatermarkWordCount {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
//设置EventTime作为Flink的时间处理标准,不指定默认为ProcessTime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//设置并行度
env.setParallelism(1)
//指定Source nc-lp 9000
val sourceDS: DataStream[String] = env.socketTextStream("localhost",9000)
//过滤非法数据 (23432-aa)类等等
val filterDS: DataStream[String] = sourceDS.filter(new FilterFunction[String] {
override def filter(t: String): Boolean = {
if (null == t || "".equals(t)) {
return false
}
val lines = t.split(",")
if (lines.length != 2) {
return false
}
return true
}
})
import org.apache.flink.streaming.api.scala._
/*做了一个简单的map转换,将数据转换成Tuple2<long,String,Integer>格式,
第一个字段代表是时间 第二个字段代表的是单词,第三个字段固定值出现了1次*/
val mapDS: DataStream[(Long, String, Integer)] = filterDS.map(new MapFunction[String, Tuple3[Long, String, Integer]] {
override def map(t: String): (Long, String, Integer) = {
val lines = t.split(",")
return new Tuple3[Long, String, Integer](lines(0).toLong, lines(1), 1)
}
})
/*设置Watermark的生成方式为Periodic Watermark,并实现他的两个函数getCurrentWatermark和extractTimestamp*/
val wordcountDS: DataStream[(Long, String, Integer)] = mapDS.assignTimestampsAndWatermarks(
new AssignerWithPeriodicWatermarks[(Long, String, Integer)] {
var currentMaxTimestamp = 0L
//最大允许的消息延迟是5000ms
val maxoutOfOrderness = 5000L
override def getCurrentWatermark: Watermark = {
return new Watermark(currentMaxTimestamp - maxoutOfOrderness)
}
override def extractTimestamp(t: (Long, String, Integer), l: Long): Long = {
val timestamp = t._1
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
return timestamp
}
}
/*这里根据第二个元素 单词进行统计 时间窗口是30秒 最大延时是5秒,统计每个窗口单词出现的次数*/
//注意: Flink的时间窗口是左闭右开的[0,30000)
).keyBy(1)
// 时间窗口是30s
.timeWindow(Time.seconds(30))
.sum(2)
wordcountDS.print("\n 单词统计:")
env.execute("Window WordCount")
}
}
JAVA版
package com.hadoop.ljs.flink110.window;
import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.time.Time;
import javax.annotation.Nullable;
public class TumblingWindowWatermarkWordCount {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment senv = StreamExecutionEnvironment.getExecutionEnvironment();
/*设置使用EventTime作为Flink的时间处理标准,不指定默认是ProcessTime*/
senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//这里为了便于理解,设置并行度为1,默认并行度是当前机器的cpu数量
senv.setParallelism(1);
/*指定数据源 从socket的9000端口接收数据,先进行了不合法数据的过滤*/
DataStream<String> sourceDS = senv.socketTextStream("localhost", 9000)
.filter(new FilterFunction<String>() {
@Override
public boolean filter(String line) throws Exception {
if(null==line||"".equals(line)) {
return false;
}
String[] lines = line.split(",");
if(lines.length!=2){
return false;
}
return true;
}
});
/*做了一个简单的map转换,将数据转换成Tuple2<long,String,Integer>格式,第一个字段代表是时间 第二个字段代表的是单词,第三个字段固定值出现了1次*/
DataStream<Tuple3<Long, String,Integer>> wordDS = sourceDS.map(new MapFunction<String, Tuple3<Long, String,Integer>>() {
@Override
public Tuple3<Long, String,Integer> map(String line) throws Exception {
String[] lines = line.split(",");
return new Tuple3<Long, String,Integer>(Long.valueOf(lines[0]), lines[1],1);
}
});
/*设置Watermark的生成方式为Periodic Watermark,并实现他的两个函数getCurrentWatermark和extractTimestamp*/
DataStream<Tuple3<Long, String, Integer>> wordCount = wordDS.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple3<Long, String, Integer>>() {
private Long currentMaxTimestamp = 0L;
/*最大允许的消息延迟是5秒*/
private final Long maxOutOfOrderness = 5000L;
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
@Override
public long extractTimestamp(Tuple3<Long, String, Integer> element, long previousElementTimestamp) {
long timestamp = element.f0;
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
return timestamp;
}
/*这里根据第二个元素 单词进行统计 时间窗口是30秒 最大延时是5秒,统计每个窗口单词出现的次数*/
}).keyBy(1)
/*时间窗口是30秒*/
.timeWindow(Time.seconds(30))
.sum(2);
wordCount.print("\n单词统计:");
senv.execute("Window WordCount");
}
}