前几天写了《实时大数据计算中,Spark的滑动窗口和允许消息迟到机制》,这几天一时兴起,对Flink的实时计算机制进行记录。
乱序和迟到的定义
Flink的实时计算控制好像比Spark多出了一个,就是关于乱序数据的处理,在Flink的定义中,乱序与迟到由相同点和不同点,首先相同点在于,它们都指代,处于时间窗口之外的数据,其中乱序范围称为乱序界,迟到范围称为迟到界,每个窗口都有自己的乱序界和迟到界,以窗口W1为例,假设窗口范围是 [00:00,00:05) ,允许乱序时间为10s,迟到时间为30s。
窗口 ── allowedLateness=30s ──
│ │ │
▼ ▼ ▼
00:00 00:05 ← 乱序界10s → 00:15 ← 迟到界30s → 00:35
▲ ▲ ▲
│ │ │
乱序阶段 允许迟到阶段 彻底关闭
(等10秒) (再等30秒) (丢弃)
乱序和迟到机制的设置一般要跟窗口绑定,才会生效。没有窗口了,数据一般就是来一条处理一条,输出一条,代码层面不影响编译。
窗口计算机制,乱序消息,迟到消息
在Spark Streaming中,窗口时间一到达,就会触发一次窗口内计算,后续针对迟到的数据还会再算一次,但是这哥操作算作更新。在Flink中,窗口的计算是由消息的事件时间来决定的,只是如果设置的策略是noWatermarks(),那么窗口计算的触发机制就是按照窗口事件来计算,这个比较简单,这里不作讨论。
如果一段连续的消息,它们的事件时间如下所示,从左到右,代表消息依次到达Flink程序。
00:01 00:02
这两条消息,对应的事件时间分别是00:01,00:02。最大事件时间是00:02,达不到触发窗口计算的要求,此时数据会积累起来,不会去计算。
【Note1:消息到达Flink的时间,即,程序处理时间,不参与窗口计算触发机制】
知识点1:消息到达Flink的时间,即,程序处理时间,不参与窗口计算触发机制。
知识点2:程序始终会去记录并更新最大事件时间。
进一步考虑,接下来又来了两条消息,它们的事件时间分别是00:07,00:04。如下所示。
00:01 00:02 00:07 00:04 00:09 00:08
此时最大事件时间来到了00:07,超出了窗口区间的右端点,此时如果出现窗口范围内的消息,就称为乱序消息。但依然不会触发窗口计算。事件时间为00:04的消息累积在W1范围内,00:07,00:09,00:08这三条消息累积在W2范围内。
进一步考虑,接下来出现了一条消息,如下所示。
00:01 00:02 00:03 00:07 00:04 00:09 00:08 00:15
这个消息超出了W1的乱序界,触发W1的窗口计算。数据 00:01,00:02,00:03,00:04,会按照预设的窗口进行计算。此时可以推断,触发窗口计算的代码逻辑是
int watermask = MaxEventTime - 10s;
if(watermask >= window_right){
triggerWindow();
}
其中,window_right是窗口区间的右端。一旦计算,窗口关闭。
再进一步,如果后续还继续出现需要在W1中计算的数据,如下所示
00:01 00:02 00:07 00:04 00:09 00:08 00:15 00:03
那么这个时间事件为00:03的消息被称为迟到消息,这个消息依然会纳入W1窗口内计算,只是这次计算算作是更新操作。迟到更新是针对每条迟到数据进行判定的,即,允许每条消息比自身事件时间迟到30s,具体判断逻辑如下
Watermark >= window_end 且 eventTime < Watermark 且 eventTime + allowedLateness >= Watermark
这三个判断条件分别代表窗口已关闭,该数据属于迟到数据,仍在宽限期内。
前两个比较好理解,但是第三个,仍在宽限期内可能不大好理解。但这样解释会清晰些,即,“当前的最大事件时间还没有到我不能迟到的程序,所以我还能进去参与计算”。
代码实现
核心创建数据的类
package com.edata.bigdata.flink;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.DeserializationSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.kafka.clients.consumer.OffsetResetStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Properties;
import java.io.Serializable;
public class StreamSource implements Serializable {
public Logger logger = LoggerFactory.getLogger(this.getClass());
public StreamExecutionEnvironment env;
//yyyy-MM-dd HH:mm:ss:SSS
public String dateFormat;
public <T> DataStream<T> createDataStream(Properties sourceProps,
Properties offsetProps,
long maxOutOfOrderSecond,
long idleness,
Class<T> clazz) {
DeserializationSchema<T> jsonSchema = new JsonDeserializationSchema<>(clazz);
KafkaSource<T> kafkaSource = KafkaSource
.<T>builder()
.setBootstrapServers(sourceProps.getProperty("kafka.bootstrap.servers"))
.setTopics(sourceProps.getProperty("subscribe"))
.setGroupId(sourceProps.getProperty("group.id"))
.setValueOnlyDeserializer(jsonSchema)
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))
.setProperties(offsetProps)
.build();
WatermarkStrategy<T> watermarkStrategy = WatermarkStrategy
.<T>forBoundedOutOfOrderness(Duration.ofSeconds(maxOutOfOrderSecond))
.withTimestampAssigner(new EventTimeExtractor<>(clazz, dateFormat))
.withIdleness(Duration.ofSeconds(idleness));
// WatermarkStrategy<T> watermarkStrategy = WatermarkStrategy.noWatermarks();
return env.fromSource(kafkaSource, watermarkStrategy, "Kafka Source");
}
public <T> WindowedStream<T, String, TimeWindow> applySlidingWindows(
DataStream<T> dataStream,
long windowSize,
long slidingSize,
long lateness,
Class<T> clazz) {
//先根据key进行分组,相同key的数据会分到一起,作为一组,每个组有各自的统计窗口。
return dataStream.keyBy(new EventKeyExtractor<>(clazz))
.window(SlidingEventTimeWindows.of(Duration.ofSeconds(windowSize), Duration.ofSeconds(slidingSize)))
.allowedLateness(Duration.ofSeconds(lateness));
}
public void start(String jobName) throws Exception {
if (env != null) {
env.execute(jobName);
}
}
public StreamSource(StreamExecutionEnvironment env, String dateFormat) {
this.env = env;
this.dateFormat = dateFormat;
}
}

被折叠的 条评论
为什么被折叠?



