最近在慢慢看flink的知识,我们都知道,flink和sparkstreaming的一大区别就是flink支持多种时间类型以及增加了watermark水位的概念,那么flink增加的这些功能有什么好处呢?
对于流式数据,最大的特点就是数据上带有时间属性特征,flink根据时间产生的位置不同,将时间分为三种概念。
1.Event Time 事件生成时间 (事件产生的时间)
2.Ingestion Time 事件接入时间 (事件刚接入到flink系统的事件)
3.Processing Time 事件处理时间 (事件经过flink处理,处理算子的实例所在的系统时间)
那么flink为什么要分这三种时间呢?我们直接拿实际生产中的数据来说一下
Event1:
{“logId”:“c171bc53-75f4-41b2-9a8f-065813cd4960”,“responseId”:“c171bc53-75f4-41b2-9a8f-065813cd4960”,“logTime”:1567493296000,“environment”:“prod”,“code”:200,“createTime”:1567493430000}
LogTime:1567493296000 (2019-09-03 14:48:16) 事件产生时间
CreatTime: 1567493430000 (2019-09-03 14:50:30) 事件达到服务端的时间
Event2:
{“logId”:“c171bc53-75f4-41b2-9a8f-065813cd4960”,“responseId”:“c171bc53-75f4-41b2-9a8f-065813cd4960”,“logTime”:1567493361715,“environment”:“prod”,“code”:200,“createTime”:1567493392000}
LogTime:1567493361715 (2019-09-03 14:49:21) 事件产生时间
CreatTime: 1567493392000 (2019-09-03 14:49:52) 事件达到服务端的时间
我们可以看到实际生产中,Event1的产生的时间是要比Event2早的,可是因为种种原因,Event1到达服务端的时间却要比Event2要晚,这样就会产生一个事件乱序的问题,如果对一个时间精度很高的需求,就要避免这种乱序事件发生。
所以我们在实际开发中就要根据不同需求,选择时间类型:
1.Event Time 可以处理数据乱序以及分布式机器时钟不统一的情况
2.Ingestion Time 可以处理分布式机器时钟不统一的情况但处理不了数据乱序的问题
3.Processing Time 都处理不了但在性能和易用性的角度有优势
这里会有个小疑问,刚接触的人可能会想,那我有了这个事件时间类型就可以避免了数据乱序以及分布式机器时钟不统一的情况,那这个watermark是不是多余呢?显然是不是的,如下图就说明了没有水位,但只有事件时间的弊处。
上图表示一个滑动窗口,红色的a是第13s产生的消息,但却因为网络延时,在第19秒到达flink的系统,我们这里采用了eventTime但没有用到watermark,理应第13秒的消息应该在window1和window2中,但是因为没有水位,在时间到达15s钟的时候,window1已经处理了,所以当延迟到19s的消息到达时,虽然这条消息是13s产生的,但是window1已经关闭,所以只能存在于window2里了。
所以我们这里必须要引入watermark,watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。
我们用四个问题简单明了的解释watermark(水位)
Q1.什么是watermark?
A1.watermark可以理解为就是一个时间戳,他是每条数据都会带有的一个隐藏属性。
Q2.watermark的含义?
A2.表示在时间上,小于这个watermark时间戳的数据已经全部达到
Q3.watermark有什么用?
A3.正如上面举的例子,对于有延迟的数据,watermark可以保证该数据在正确的时间处理,
并避免了处理数据时无限期的等下去。
Q4.如何使用watermark?
目前flink支持两种方式指定Timestamps和watermark,第一种是在DataStream Source算子接口的Source Function中定义。因为我们在使用中,我们读数据都是对接外部数据源的,所以此种方法就不多说了。(补充:读kafak数据源支持在source中定义)
另一种就是自定义Timestamps Assigner和Watermark Generator。我们有两种自定义方法,每种自定义方法又根据watermarks生成形式不同分为两种类型,如下所示:
我们平时最常用的就是打星号的方法,这两种方法的区别就是第一种的水位是传入的最大时间戳减去固定时长,第二种则是可以自定义水位时间戳
代码一:flink自带的以固定时长生成watermark的方法
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object watermarkTest {
def main(args: Array[String]): Unit = {
val parameter = ParameterTool.fromArgs(args)
val host = parameter.get("host")
val port = parameter.getInt("port")
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val sockStream = env.socketTextStream(host,port)
val resultDS = sockStream.map(x=>(x.split(",")(0),x.split(",")(1),1))
val watermark = resultDS.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[(String, String,Int)](Time.seconds(2)) {
override def extractTimestamp(element: (String, String,Int)): Long = element._2.toLong
})
val result = watermark.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(5))).sum(2)
result.print()
env.execute("test")
}
}
代码二:实现接口自定义生成watermark的方法
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object watermarkTest1 {
def main(args: Array[String]): Unit = {
val parameter = ParameterTool.fromArgs(args)
val host = parameter.get("host")
val port = parameter.getInt("port")
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val sockStream = env.socketTextStream(host,port)
val resultDS = sockStream.map(x=>(x.split(",")(0),x.split(",")(1).toLong,1))
val watermark = resultDS.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String,Long,Int)] {
var currentMaxTimestamp = 0L
val maxOutOfOrderness = 2000L//最大允许的乱序时间是10s
var a : Watermark = _
override def getCurrentWatermark: Watermark = {
a = new Watermark(currentMaxTimestamp - maxOutOfOrderness)
a
}
override def extractTimestamp(t: (String, Long, Int), previousElementTimestamp: Long): Long = {
val timestamp = t._2
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
timestamp
}
})
val result = watermark.keyBy(_._1).window(TumblingEventTimeWindows.of(Time.seconds(5))).sum(2)
result.print()
env.execute("test")
}
}
这两种方法都是采用了Periodic Watermarks的方式,即是根据设定时间间隔周期性的生成watermarks,
这个周期具体由ExecutionConfig.setAutoWatermarkInterval设置,如果没有设置会一直调用getCurrentWatermark方法。