注意:本篇博客中的所有解释都是在滚动窗口的前提下
在事件时间模式下,Flink流式应用处理的所有记录都必须要包含时间戳。
这个时间戳是记录所对应事件的发生时间,但实际上我们也可以自定义时间戳,但只要保证流记录的时间戳会随着数据流的前进大致递增即可。
Ingestion Time,是数据通过第三方进入到Flink,Flink接收数据的时间,因此如果按照事件的接入时间,来处理数据,是不能处理乱序情况下的数据(如果数据是乱序到达)。
即数据接入Flink后,通过算子处理数据的时间,使用的是当前主机的时间。
Flink流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,才会考虑其他的时间属性。
Flink默认采用的时Process Time时间概念
object addSink到kafka {
def main(args: Array[String]): Unit = {
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
val streamLocal = StreamExecutionEnvironment.createLocalEnvironment(3)
//这里配值我们在处理数据的时候,使用EventTime事件时间
streamLocal.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
streamLocal.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
streamLocal.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
import org.apache.flink.api.scala._ //如果数据是有限的(静态数据集)可以引入这个包
val dataStream = streamLocal.fromElements(("flink_er", 3), ("f", 1), ("c", 2), ("c", 1), ("d", 5))
.map(x => x._1)
//flink-connector-kafka的版本要保持一致,都是1.7.0
val kafkaProducer010 = new FlinkKafkaProducer[String]("master:9092", "ceshi01", new SimpleStringSchema())
dataStream.addSink(kafkaProducer010)
streamLocal.execute()
}
}
通常来讲,由于各种原因,包含但不限于网络、外部系统因素等,事件数据往往不能够及时传输到Flink系统中进行计算,因此,在开启EventTime的前提下,flink提供了一种依据watermark(水位线)机制结合window(窗口)来实现对乱序数据的处理的方式。
生成water_mark的方式主要是有两大类:
第一种可以定义一个最大允许乱序的时间,这种情况应用较多。
val watermark: DataStream[(String, Long, String, Int)] = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String, Long, String, Int)] {
val maxOutOfOrderness = 10000L //最大允许的乱序时间是10s
var currentMaxTimestamp = 0L
var a: Watermark = _
override def getCurrentWatermark: Watermark = {
a = new Watermark(currentMaxTimestamp - maxOutOfOrderness)
a
}
override def extractTimestamp(element: (String, Long, String, Int), previousElementTimestamp: Long): Long = {
val timestamp = element._2
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
val end = if (!a.toString.contains("-")) {
val regEx = "[^0-9]";
val p = Pattern.compile(regEx);
val m = p.matcher(a.toString);
val L_number = m.replaceAll("").trim()
format.format(L_number.toLong)
} else a.toString
println("timestamp:" + element._1 + "," + element._2 + "," + element._3 + "|" +
s"${a.toString}($end)" + "|" + format.format(currentMaxTimestamp - maxOutOfOrderness))
val lll: Long = System.currentTimeMillis()
timestamp
}
})
实际上可以理解为数据集中的元素没有按照顺序到达,而是其中某个元素延迟到达了,那么此时整个数据集,就是乱序的;因此才会有这个“允许最大乱序时间”的概念,即允许事件数据延迟多久到达。
(数据有可能会延迟到达,但我们又不能无限期的等待下去,必须有个机制来保证一个特定的事件后,必须触发windows去进行当前窗口的数据,这个特别的机制就是water_mark)
每接收一条数据就相当于往水池中添加水,因此水位线的高度只会升高不会降低,每当一个新的数据进来时,会重新计算水位线时间。每条数据中的water_marker记录的是截至到现在最高的水位线,每条数据计算水位线的时候如果小于当前水位线时间,则不会更新现有的水位线(如下方的timestamp:4)
当水位线到达窗口触发时间时才会触发窗口的计算,water_marker的意义在于数据无序传递的时候可以让其保持一定的容错率,如果晚来的数据在这个容错率之内,会当作正常传递来的数据进行处理。
输出数据为:
当前数据 | 当前数据的水位线 | 计算并更新水位线
timestamp:1,1586947680000
,18:48:00 | Watermark@-10000(Watermark@-10000) | 18:47:50
timestamp:2,1586947740000
,18:49:00 | Watermark@1586947670000(18:47:50) | 18:48:50
timestamp:3,1586947800000,18:50:00 | Watermark@1586947730000(18:48:50) | 18:49:50
timestamp:4,1586947620000,18:47:00 | Watermark@1586947790000(18:49:50) | 18:49:50
timestamp:5,1586947680000,18:48:00|Watermark@1586947790000(18:49:50) | 18:49:50
timestamp:6,1586947260000,18:41:00 | Watermark@1586947790000(18:49:50) | 18:49:50
timestamp:7,1586947980000,18:53:00 | Watermark@1586947790000(18:49:50) | 18:52:50
timestamp:8,1586947920000,18:52:00 | Watermark@1586947970000(18:52:50) | 18:52:50
以上输出数据的形成过程:
输入数据:
1,1586947680000
2,1586947740000
… …
water_mark生成方式
本篇博文中的示例 b
代码所示;
结论:
可以发现我的输入数据是乱序达到的,有的到达的时间早有的到达的时间晚,但就算是如此,我的水位线依然不受其影响,因此得证,Flink中的水位线只会增高不会降低。