这是彭文华的第91篇原创
问:数据工程师最期望数据怎么来?
答:按顺序来。
MapReduce当初能用起来,就是因为Map阶段对所有数据都进行排序了,后面的Reduce阶段就可以直接用排序好的数据了。
批处理的时候因为数据已经落地了,咱可以慢慢排序。但是流式数据都是一条一条过来的,这个时候数据到达的时间和出发时的顺序不一致会导致非常多的问题,这该咋整呢?
Sparkstreaming对乱序支持很差,因为它其实是“微批”,不是真正的流。加州伯克利大学AMP实验室设计Spark的时候,想的就是弄一个更快的计算引擎,压根就没打算做成来一条处理一条的流式数据处理。所以对于一些乱序数据根本就不太关心,所以导致Sparkstreaming不能或者不太能支持乱序数据的处理。
但是Flink不行啊,数据一条一条的过来,然后进行窗口处理,乱序会导致各种统计问题,这就得必须解决了。
什么是乱序
一条数据在Flink里,有三个时间:
- Event Time:事件产生的时间;
- Ingestion Time:事件进入Flink的时间;
- Window Processing Time:事件被处理的时间。
当数据一条一条规规矩矩的按流程发送,MQ传输,Flink接受然后处理,这个时候,就是有序的数据。
当出现各种异常,有些数据延迟了,排在后面的数据跑前面去了,这就出现了乱序。
请思考一下,我们应该以哪个时间戳判定乱序呢?
Flink的WaterMark机制
乱序会导致各种统计上的问题。比如一个Time Window本应该计算1、2、3,结果3迟到了,那这个窗口统计就丢数据了。这可太坑了。
为了解决这个问题,Flink设置了一个三个机制来解决这个问题:
- WaterMark--水位线,;
- allowLateNess--数据迟到时间;
- sideOutPut--超长迟到数据收集;
水位线的设置很简单(系统时间为准):
override def getCurrentWatermark(): Watermark = { new Watermark(System.currentTimeMillis - 5000)
设置Watermark为-5秒。但是怎么理解这个-5秒的水位线呢?
经常户外徒步的同学应该知道一个徒步小队通常会有一正两副领队,队首队尾各一个副队,正队长在队伍中穿插协调。
队尾的领队叫后队领队,后队领队要保证所有队员都在前面,也就是说后队领队是整个队伍的队尾,当收队的时候,看见后队领队,那就说明整个队伍都已经完全到达了。
这个Watermark就相当于给整个数据流设置一个后队领队。但是窗口是不知道具体要来几个数的,所以只能设置一个时间上的限制,以此来推测当前窗口最后一条数据是否已经到达。假设窗口大小为10秒,Watermark为-5秒,那么他会做以下事情:
- 每来一条数据,取当前窗口内所有数据的最大时间戳;
- 用最大时间戳扣减Watermark后看看是不是符合窗口关闭条件;
- 如果不符合,则继续进数据;
- 如果符合,则关闭窗口开始计算。
你看,多像户外徒步?
- 每来一个人,就问问出发时是几号,然后确认所有已到队员最大的号码;
- 用最大的号码对比一下后队领队的号码;
- 如果比后队领队的号码小,就不收队;
- 如果号码大于等于后队领队号码,就收队。
迟到的数据
当然啊,即便是用了Watermark机制,依然还会存在迟到的数据。就像户外徒步一样,有人走错路然后又赶上来。后队领队分明没超过任何一个队员,但是还是有队员落在后面了。
所以Flink还增设了三种应对方式:
- allowLateNess--对于迟到一小会的数据,设置一个允许迟到时间;
- sideOutPut--对于超过允许迟到时间的数据,全部收集起来,后续再处理;
- 如果都不处理,Flink就默认自动丢弃。
也就是说,在watermark机制下,窗口虽然到了关闭时间,但是如果你设置了allowLateNess=10秒,那这个窗口还会再等10秒,看看是否还有他那个小队的数据,10秒后窗口关闭,开始计算。
如果等了10秒还没等到,11秒的时候,原本属于该窗口的数据才姗姗来迟,那么sideOutPut会把数据收集起来,放到侧输出流,等待后续处理。这个数据肯定就不会在当前窗口计算进去了。