StreamSource可以直接将时间戳分配给它们生成的元素,还可以发出水印。完成此操作后,不需要时间戳分配程序。请注意,如果使用时间戳赋值器,则源提供的任何时间戳和水印都将被覆盖。
要将时间戳直接分配给源中的元素,源必须在SourceContext上使用collectWithTimestamp(…)方法。要生成水印,源必须调用emitWatermark(Watermark)函数。
https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/connectors/kafka.html
一种是像下面这一种1.8版本的,在创建kafkatable时使用,同时还可以指定最大延迟时间。
水印(Watermarks)的生成方式有两种,分别是Periodic(周期性的)和Punctuated(可以在每条数据上都生成水印)。这两种方式还有不同的使用方法,下面分别展示和官网上类似的用法。
AssignerWithPeriodicWatermarks 定期分配时间戳并生成水印(可能取决于流元素,或完全基于处理时间)。
通过定义生成水印的间隔(n默认是200毫秒) ExecutionConfig.setAutoWatermarkInterval(n)。分配器的getCurrentWatermark()方法每次都会被调用,如果返回的水印非空且大于前一个水印,则将发出新的水印。
new AssignerWithPeriodicWatermarks[Tuple2[String, Long]] {
var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
val maxOutOfOrderness = 20000L // 最大延迟时间,单位是毫秒
/**
* 返回当前水印。系统定期调用该方法来检索当前水印(通过StreamExecutionEnvironment
* .getConfig.setAutoWatermarkInterval()来设定周期间隔)。
* 该方法可能会返回{@code null}来表示没有可用的新水印,不过貌似是是针对于Punctuated模式的。
* @return
*/
override def getCurrentWatermark: Watermark = {
new Watermark(currentMaxTimestamp - maxOutOfOrderness)
}
//
/**
* 为每条进入数据流的数据分配时间戳。
* 例如kafka : 顺序反着看
* extractTimestamp
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPeriodicWatermarks#getTimestampForRecord
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher#emitRecordWithTimestampAndPeriodicWatermark
* @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
* @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
* @return 当前element的时间戳
*/
override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
val timestamp = element._2
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
timestamp
}
}
若要在特定事件下才可能生成新水印时,可以使用带标点水印。Flink将首先调用extractTimestamp(…)方法来为元素分配一个时间戳,然后立即对该元素调用checkAndGetNextWatermark(…)方法。
checkAndGetNextWatermark(…)方法将传递extractTimestamp(…)方法中返回的时间戳,并可以决定是否要生成水印。每当checkAndGetNextWatermark(…)方法的返回非空且大于上一个水印时,则将发出该新水印。
new AssignerWithPunctuatedWatermarks[Tuple2[String, Long]]{
/**
* 为每条进入数据流的数据分配时间戳。
* 例如kafka : 顺序反着看
* extractTimestamp
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getTimestampForRecord
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
* 下面是去获取所有partition中最小的水印并发射的,在这之前还会调用一次 checkAndGetNextWatermark 方法
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getCurrentPartitionWatermark
* @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
* @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
* @return 当前element的时间戳
*/
override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
element._2
}
/**
* 询问此实现是否要发出水印。这个方法在{@link #extractTimestamp(Object, long)}方法之后被调用。
* 这一层的返回会在调用它的方法中再次判断,判断你返回的时候为 null 或者小于之前的水印,
* 这两种情况皆沿用之前在 checkAndGetNewWatermark 方法中成功赋值的水印。
* 虽然保留了原来的水印,但并不代表发射的一定就是当前分区的水印,因为每次都是对比后才去发射的watermark。
* 例如kafka:书序倒着看
* checkAndGetNextWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# checkAndGetNewWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark (这里发射用的水印是在上面那个方法里赋的值)
* 在updateMinPunctuatedWatermark方法里轮询找出所有partion中最小的那个水印并发射
* @param lastElement 刚刚进入过extractTimestamp方法的数据
* @param extractedTimestamp {@link #extractTimestamp(Object, long)}方法提取的时间戳
* @return
*/
override def checkAndGetNextWatermark(lastElement: (String, Long), extractedTimestamp: Long): Watermark = {
// 简单判断一下再返回水印
if (lastElement._2 != 0) new Watermark(extractedTimestamp) else null
}
}
1.10.0
flink-planner
周期性水印生成的最简单的特例是给定源任务看到的时间戳按升序出现的情况。在这种情况下,当前时间戳始终可以充当水印,因为不会有更早的时间戳到达。
注意,对于每个并行数据源任务,时间戳只需要升序。例如,在特定的设置中,如果一个Kafka分区被一个并行数据源实例读取,那么只需要在每个Kafka分区内时间戳是递增的。Flink的水印合并机制将在并行流被洗牌、联合、连接或合并时生成正确的水印。
就是不用担心单个多并行度下kafka每个分区的时间戳问题。
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#cleanupTime
cleanupTime(清除时间)=当前窗口的结束时间+allowedLateness
如果不是EventTime模式,则没有allowedLateness就算设置了也不起作用。
窗口(例如长度2s)的结束开始结束界限只精确到秒(1596695796199是窗口的第一条数据,那开始时间就是1596695796000,结束时间就是1596695798000,能够被算进这个窗口的最大时间戳是结束时间就是1596695797999)
窗口:start_time >= 数据时间 < end_time
如果数据在窗口触发之前到了,就算数据的时间戳小于当前水位线,也依然会被计算在窗口内。
a,0,1596695796199,EventTime:2020-08-06 14:36:36.199,watermark:2020-08-06 14:36:35.199
a,1,1596695797213,EventTime:2020-08-06 14:36:37.213,watermark:2020-08-06 14:36:36.213
a,2,1596695798999,EventTime:2020-08-06 14:36:38.999,watermark:2020-08-06 14:36:37.999
a,3,1596695796000,EventTime:2020-08-06 14:36:36.000,watermark:2020-08-06 14:36:37.999
a,4,1596695899215,EventTime:2020-08-06 14:38:19.215,watermark:2020-08-06 14:38:18.215
(a,3,1596695796000)
并不会去拿到来的数据的时间戳和水印对比此条数据是否应该抛弃,水位线和窗口配合是为了让这个窗口晚些被触发,尽量多等一些属于这个窗口的数据的到来(防止有延迟数据)。
org.apache.flink.streaming.api.operators.InternalTimerServiceImpl#advanceWatermark
在这里去判断的是否触发窗口(在这之前数据应该已经计算好了)
还会去判断 timer 是否小于水印,这个方法就是判断水印是否>= timer ,是否触发窗口。
那么 timer 是怎么得来的呢?
timer就是用这个这个窗口中能出现的最大的一个时间戳注册的。
org.apache.flink.streaming.runtime.io.StreamTaskNetworkInput#processElement
在这里会去判断过来的是水印、latencymark、record、……中的哪一种。
每过来一个水印或者record就会去调用一次,分别去判断是否触发窗口(根据窗口的 timer(窗口的endTime-1+allowedLateness)去判断是否触发)和更新窗口状态。
我得看它是不是触发和平常的收集单条记录走的是不同的路径,是不是在触发之前就已经计算了,在哪里计算的。
是的,在触发之前窗口中的数据就已经计算好了。
这是以socket->flatMap->keyBy->window->maxBy-print为例:
数据:
org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.StreamTaskNetworkOutput#emitRecord
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#processElement
windowAssigner.assignWindows()为当前数据指定窗口。
如果有迟到数据,在if (isWindowLate(window))这里会根据设置的allowedLateness判断是否丢弃。
triggerContext.onElement(element);为当前窗口设置 timer。
emitWindowContents(window, contents);这里也能触发窗口,但默认是不触发的(交给水印那个路径(inputWatermark)去触发),执行这个方法前会有判断,如果有迟到数据未被丢弃,就会执行这个方法。
org.apache.flink.runtime.state.heap.HeapReducingState# add
窗口聚合操作在被触发之前就会根据新来的数据去计算并更新结果(状态),在timer与水印对比触发窗口之前,数据就已经计算好了。
org.apache.flink.runtime.state.heap.HeapReducingState.ReduceTransformorg.apache.flink.runtime.state.heap.StateTable# transform
org.apache.flink.runtime.state.heap.CopyOnWriteStateMap# transform
ation# apply
registerCleanupTimer(window);在这里设置清理窗口的 timer(windoMaxTimestamp + allowedLateness(windoMaxTimestamp = endtime - 1))。
org.apache.flink.runtime.state.heap.HeapReducingState#add 在这个方法后面去计算并更新状态
org.apache.flink.runtime.state.heap.StateTable#transform
org.apache.flink.runtime.state.heap.CopyOnWriteStateMap#transform
将apply计算后的数据放入对应的key+namespace(starttime-endtime)的state中
org.apache.flink.runtime.state.heap.HeapReducingState.ReduceTransformation#apply
会先去判断这个窗口内是否已经有计算过的数据存在,如果没有的话直接把当前这条数据存入state
org.apache.flink.streaming.api.functions.aggregation.ComparableAggregator#reduce
计算state中的这个窗口内上一条计算结果与新来的数据。
水印:
org.apache.flink.streaming.runtime.streamstatus.StatusWatermarkValve#inputWatermark
org.apache.flink.streaming.runtime.streamstatus.StatusWatermarkValve#findAndOutputNewMinWatermarkAcrossAlignedChannels
org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.StreamTaskNetworkOutput#emitWatermark
org.apache.flink.streaming.api.operators.AbstractStreamOperator#processWatermark
org.apache.flink.streaming.api.operators.InternalTimeServiceManager#advanceWatermark
org.apache.flink.streaming.api.operators.InternalTimerServiceImpl#advanceWatermark
这个方法里的while循环就对应着那句话,触发窗口的条件就是:窗口中有数据,并且水印大于等于窗口的结束时间。
eventTimeTimersQueue.poll();把eventTimeTimersQueue中 <= wartermark的 timer 都poll出来。
就是在这里去触发那些被水印>=的 timer
eventTimeTimersQueue.peek()把eventTimeTimersQueue里面的 timer 都拿出来,然后再看这个 timer 是不是 <= 水印,是的话才会接着向下走去触发窗口。
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#onEventTime
这里调用的是WindowOperator的方法。
triggerContext.onEventTime(timer.getTimestamp());用窗口的 timer 去判断是否=window的maxtimestamp(就是endtime-1)
true的话接下来才会调用windowState.get()返回状态中保存的此窗口的计算结果,只要返回的contents不为null接着往下走。
org.apache.flink.runtime.state.heap.AbstractHeapAppendingState#getInternal -> return stateTable.get(currentNamespace);
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#emitWindowContents
org.apache.flink.streaming.runtime.operators.windowing.functions.InternalSingleValueWindowFunction#process
org.apache.flink.streaming.api.functions.windowing.PassThroughWindowFunction#apply
org.apache.flink.streaming.api.operators.TimestampedCollector#collect(T)
org.apache.flink.streaming.api.operators.AbstractStreamOperator.CountingOutput#collect(org.apache.flink.streaming.runtime.streamrecord.StreamRecord<OUT>)
org.apache.flink.streaming.runtime.tasks.OperatorChain.CopyingChainingOutput#collect(org.apache.flink.streaming.runtime.streamrecord.StreamRecord<T>)
org.apache.flink.streaming.runtime.tasks.OperatorChain.CopyingChainingOutput#pushToOperator
org.apache.flink.streaming.api.operators.StreamSink#processElement
org.apache.flink.streaming.api.functions.sink.SinkFunction#invoke(IN, org.apache.flink.streaming.api.functions.sink.SinkFunction.Context)
org.apache.flink.streaming.api.functions.sink.PrintSinkFunction#invoke
org.apache.flink.api.common.functions.util.PrintSinkOutputWriter#write 到这一步结果就打印出来了
下面是附加的部分代码运行顺序:
StreamSource run()
FlinkKafkaConsumer run()
----------在这里会去调用FlinkKafkaConsumer010的createFetcher()->Kafka010Fetcher()->Kafka09Fetcher()->AbstractFetcher()
----------AbstractFetcher里会去判断使用哪种水印策略
----------在这里会去判断是否使用轮循发现分区后,往下走
Kafka09Fetcher runfetchLoop()
Kafka010Fetcher emitRecord() 在这里consumerRecord携带的时间戳,应该就是数据进入kafka的时间(有待验证,因为那个时间戳既不是当前系统时间,也不是数据本身携带的时间,而是一个小时左右前的,我好像就是在哪个时候塞数据进入kafka的)
AbstractFetcher emitRecordWithTimestamp()
----------如果是NO_TIMESTAMPS_WATERMARKS模式,直接调用 collectWithTimestamp()发出数据。
----------否则就会去判断使用 emitRecoudWithTimestampAndPariodicWatermark()或者 emitRecoudWithTimestampAndPunctuatedWatermark()
----------调用collectWithTimestamp:
--------------------StreamSourceContext collectWithTimestamp()
--------------------AbstractStreamOperator collect()
--------------------OperatorChain collect() -> pushToOperator()
--------------------StreamSink processElement() 因为我是Source直接就到sink,所以才是这个流程(并且使用的是NO_TIMESTAMPS_WATERMARKS)。
----------调用emitRecoudWithTimestampAndPariodicWatermark():
--------------------AbstractFetcher emitRecoudWithTimestampAndPariodicWatermark()
------------------------------KafkaTopicPartitionStateWithPeriodicWatermarks getTimestampForRecord()
----------------------------------------AccendingTimestampExtractor extractTimestamp() 这里会去调用我重写的extractAscendingTimestamp()(获取每条数据本身携带的时间戳,紧跟着FlinkKafkaConsumer写的)
a,0,1596695796199
a,1,1596695797999
a,2,1596695798000 # 这个不被计算在第一个窗口里(窗口:start_time >= 数据时间 < end_time )
a,3,1596695798999 # 这里应该触发了,水印 >= endtime-1
a,4,1596695796000 # allowedLateness = 2000 所以它和下面这两条还能计入第一个窗口,并再次输出第一个窗口重计算后的结果。
a,5,1596695796234
a,6,1596695800999
# 一旦水印超过或赶上(> or =)一个窗口的 windoMaxTimestamp + allowedLateness(windoMaxTimestamp = endtime - 1),那么属于这个窗口的数据再到来时将会被抛弃。
# 第一个窗口 796000 <-> 798000,cleanupTime = 797999 + 2000 = 799999。这条数据的wartermark = 800999 - 1000 = 799999。
# 所以在这条数据发射水印之后,属于第一个窗口的数据在这之后到来将会被丢弃
# 这时水印正好等于第二个窗口的 windoMaxTimestamp,触发了第二个窗口。
a,7,1596695796567
import java.text.SimpleDateFormat
import java.util.Date
import org.apache.flink.api.common.functions.{FlatMapFunction, ReduceFunction}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks, TimestampAssigner}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
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
import org.apache.flink.util.Collector
import org.apache.flink.api.scala._
object WarterMarkTest{
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.enableCheckpointing(60000)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(200) // 默认就是200ms
val ds = env.socketTextStream("127.0.0.1", 5688, '\n')
def main(args: Array[String]): Unit = {
// 周期
val dss = ds.assignTimestampsAndWatermarks(new MyPeriodicWatermarks(1000))
// 标点
// val dss = ds.assignTimestampsAndWatermarks(new MyPunctuatedWatermarks(1000))
publicFunction(dss,10)
env.execute("test")
}
def publicFunction(ds:DataStream[String],tetw:Int): Unit = {
ds.flatMap(new FlatMapFunction[String, Tuple3[String, Long, String]] {
override def flatMap(value: String, out: Collector[(String, Long, String)]): Unit = {
val sp = value.split(",")
out.collect((sp(0), sp(1).toLong, sp(2)))
}
})
.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(tetw)))
.allowedLateness(Time.milliseconds(2000))// 仅对EventTime窗口有效
.maxBy(1)
// .reduce(new ReduceFunction[(String, Long, String)] {
// override def reduce(value1: (String, Long, String), value2: (String, Long, String)): (String, Long, String) = {
// (value1._1 + "," + value2._1, value1._2 + value2._2, value1._3 + "," + value2._3)
// }
// })
.print()
}
}
/**
* AssignerWithPeriodicWatermarks 定期分配时间戳并生成水印(可能取决于流元素,或完全基于处理时间(这句话是官网翻译,不用在意))。
*/
class MyPeriodicWatermarks(outTime:Long) extends AssignerWithPeriodicWatermarks[String] {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
val maxOutOfOrderness = outTime // 最大延迟时间,单位是毫秒
/**
* 返回当前水印。系统定期调用该方法来检索当前水印(通过StreamExecutionEnvironment
* .getConfig.setAutoWatermarkInterval()来设定周期间隔,默认是200毫秒)。
* 该方法可能会返回{@code null}来表示没有可用的新水印,不过貌似是是针对于Punctuated模式的。
*
* @return
*/
override def getCurrentWatermark: Watermark = {
// println("Send WarterMark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))) + " Current Date:" + sdf.format(new Date()))
new Watermark(currentMaxTimestamp - maxOutOfOrderness)
}
/**
* 为每条进入数据流的数据分配时间戳。
* 例如kafka : 调用顺序反着看
* extractTimestamp
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPeriodicWatermarks#getTimestampForRecord
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher#emitRecordWithTimestampAndPeriodicWatermark
* @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
* @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
* @return 当前element的时间戳
*/
override def extractTimestamp(element: String, previousElementTimestamp: Long): Long = {
val timestamp = element.split(",")(2).toLong
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
println(element + ",EventTime:" + sdf.format(new Date(timestamp)) + ",watermark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))))
timestamp
}
}
/**
* 若要在特定事件下才可能生成新水印时,可以使用带PunctuatedWatermarks。
* Flink将首先调用extractTimestamp(…)方法来为元素分配一个时间戳,
* 然后立即对该元素调用checkAndGetNextWatermark(…)方法。
*
* checkAndGetNextWatermark(…)方法将传递extractTimestamp(…)方法中返回的时间戳,并可以决定是否要生成水印。
* 每当checkAndGetNextWatermark(…)方法的返回非空且大于上一个水印时,则将发出该新水印。
*/
class MyPunctuatedWatermarks(outTime:Long) extends AssignerWithPunctuatedWatermarks[String] {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
val maxOutOfOrderness = outTime // 允许的延迟时间,单位是毫秒
/**
* 询问此实现是否要发出水印。这个方法在{@link #extractTimestamp(Object, long)}方法之后被调用。
* 这一层的返回会在调用它的方法中再次判断,判断你返回的时候为 null 或者小于之前的水印,
* 这两种情况皆沿用之前在 checkAndGetNewWatermark 方法中成功赋值的水印。
* 虽然保留了原来的水印,但并不代表发射的一定就是当前分区的水印,因为每次都是对比后才去发射的watermark。
* 例如kafka:书序倒着看
* checkAndGetNextWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# checkAndGetNewWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark (这里发射用的水印是在上面那个方法里赋的值)
* 在updateMinPunctuatedWatermark方法里轮询找出所有partion中最小的那个水印并发射
* @param lastElement 刚刚进入过extractTimestamp方法的数据
* @param extractedTimestamp {@link #extractTimestamp(Object, long)}方法提取的时间戳
* @return
*/
override def checkAndGetNextWatermark(lastElement: String, extractedTimestamp: Long): Watermark = {
// 该样去返回水印,可以在这个方法里定义自己的逻辑
val timestamp = lastElement.split(",")(2).toLong
new Watermark(timestamp - maxOutOfOrderness)
}
/**
* 为每条进入数据流的数据分配新的时间戳(会覆盖掉原来的时间戳,比如从kafka过来的数据会带有它进入kafka时的时间)。
* 例如kafka : 调用顺序反着看
* extractTimestamp
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getTimestampForRecord
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
* 下面是去获取所有partition中最小的水印并发射的,在这之前还会调用一次 checkAndGetNextWatermark 方法
* -> org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark
* -> org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getCurrentPartitionWatermark
* @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
* @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
* @return 当前element的时间戳
*/
override def extractTimestamp(element: String, previousElementTimestamp: Long): Long = {
val timestamp = element.split(",")(2).toLong
println(element + ",EventTime:" + sdf.format(new Date(timestamp)) + ",watermark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))))
timestamp
}
}