概要
基于flink 1.9.1版本和blink palner。
窗口分类
window (窗口)
对流中所有事件聚合是不可能的,因为通常流是无效的。所以需要window来划定范围。window是一种可以把无限数据切割为有限数据块的手段
根据类型,window有2种:
- 基于时间驱动的 time window,比如最近30s内。
- 基于数据驱动的 count window,比如最近100个元素。
根据是否重叠,窗口分如下2种:
- tumbling windows。滚动窗口,窗口之间不重叠,比如统计5分钟内的数据,窗口大小为5分钟。
- sliding windows。滑动窗口,窗口之间会重叠。比如每10秒统计最近20秒内的数据,窗口大小为20秒,每次窗口开始时间滑动10s。
使用例子:
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
public class WordCountStreamJava {
public static void main(String args[]) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> text = env.socketTextStream("10.10.40.33", 8000);
DataStream<Tuple2<String, Integer>> words = text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
String[] split = value.split("\\s+");
for (String word : split) {
out.collect(new Tuple2<>(word, 1));
}
}
});
// 时间窗口,timeWindow没有第2个参数,表示滚动窗口。
words.keyBy(0).timeWindow(Time.seconds(5)).sum(1).print();
// 数据窗口
words.keyBy(0).countWindow(10).sum(1).print();
// 如果没有执行keyBy,需要使用timeWindowAll。
words.timeWindowAll(Time.seconds(5)).sum(1).print();
env.execute("window");
}
}
window 聚合方式
增量聚合
窗口中每进入一条数据,就进行一次计算。reduce, aggregate、sum、min、max等函数都是增量聚合。
全量聚合
等属于窗口的数据到齐,才开始进行聚合计算。如何窗口计算涉及到数据排序,必须使用全量聚合。主要的函数有,apply(windowFunction)和processWindowFunction
使用实例:
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class WordCountStreamJava {
public static void main(String args[]) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> text = env.socketTextStream("10.10.40.33", 8000);
DataStream<Tuple2<String, Integer>> words = text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
String[] split = value.split("\\s+");
for (String word : split) {
out.collect(new Tuple2<>(word, 1));
}
}
});
// 全量聚合。
words.keyBy(0).timeWindow(Time.seconds(5))
.apply(new WindowFunction<Tuple2<String,Integer>, Object, Tuple, TimeWindow>() {
// 参数说明
// window 表示当前窗口
// input 是这一个窗口内全部数据的迭代器
// out 表示收集的数据
// tuple 表示聚合的key
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Integer>> input, Collector<Object> out) throws Exception {
int count = 0;
for (Tuple2<String, Integer> item : input) {
count++;
}
out.collect("key:" + tuple + ", window:" + window + ", count:" + count);
}
})
.print();
env.execute("window");
}
}
时间
stream中有3中时间:
- event time:时间产生的时间
- Ingestion time:事件进入Flink的时间
- Processing Time:事件被处理时当前系统的时间
在flink中,时间默认是Processing Time,可以通过代码修改:
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
Watermarks
在使用eventTime的时候,可能会存在数据乱序问题。比如在使用kafka的时候,数据发送到了多个分区中,这个时候flink获取的数据就可能是乱序的。watermark就是为了解决数据乱序问题。
watermark可以翻译为水位线,通过定义一个容许的最大乱序时间,flink收集这个时间内的数据,并可以将这些数据进行重新排序。
代码实例:
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
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.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.api.windowing.windows.Window;
import org.apache.flink.util.Collector;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
public class WordCountStreamJava {
public static void main(String args[]) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> text = env.socketTextStream("10.10.40.33", 8000);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStream<Tuple2<String, Long>> input = text.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] split = value.split(",");
return new Tuple2<>(split[0], Long.parseLong(split[1]));
}
});
// 生成水印
DataStream<Tuple2<String, Long>> waterMarkStream = input.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
// 保存当前最大的日志时间
Integer maxEventTime = 0;
// 允许的最大乱序时间
Long maxOutOfoOrderness = 1000L;
/**
* 定义生成water mark的逻辑
* 默认情况下100ms 生成1次。
*
* water mark的时间,应该是最大的event time - 允许的最大乱序时间
*/
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(maxEventTime - maxOutOfoOrderness);
}
// 获取时间的时间
@Override
public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
return element.f1;
}
});
waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3))) // TumblingEventTimeWindows表示使用event time
.apply(new WindowFunction<Tuple2<String,Long>, Long, Tuple, TimeWindow>() {
// 对window内的数据进行排序
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<Long> out) throws Exception {
String key = tuple.toString();
ArrayList<Long> arrayList = new ArrayList<>();
for (Tuple2<String, Long> item: input) {
arrayList.add(item.f1);
}
Collections.sort(arrayList);
// 返回最后一条记录的时间
out.collect(arrayList.get(arrayList.size() - 1));
}
}).print();
env.execute("window");
}
}
在watermark中,window的触发时间
- 收集,根据window大小,见数据发分为多个时间段,区间是“[start, end)”。
- 生成的watermar看时间是,最大的eventtime - 允许的最大乱序时间。
- 当watermark时间大于等于窗口区间的end时间时候,窗口才会进行计算。反过来说,窗口要执行计算,当前的窗口的end时间需要小于watermark时间。
上面的例子来说含义就是:3s内的数据,有可能延迟10s到达,于是flink等待10s中。
延迟数据的处理
默认情况下,如何收到数据的event time 小于 wartermark的时间,这些数据会被直接丢弃。
可以通过allowedLateness设置数据最大延迟时间:
window(TumblingEventTimeWindows.of(Time.seconds(3))) // TumblingEventTimeWindows表示使用event time
allowedLateness(Time.seconds(2))
设置这个后,watermark时间2秒内的数据,任然会触发计算;watermark时间2秒外的数据,任然会被丢弃。简单的说,window触发时间是:watermark < window_end_time + allowedLateness 时间内
还可以通过sideOutputLateData,收集延迟的数据保存起来,方便定位问题。
//保存被丢弃的数据
OutputTag<Tuple2<String, Long>> outputTag = new OutputTag<Tuple2<String, Long>>("late-data"){};
waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用TimeWindow效果一样
.sideOutputLateData(outputTag) // 设置output
//丢弃数据处理
//把迟到的数据暂时打印到控制台,实际中可以保存到其他存储介质中
DataStream<Tuple2<String, Long>> sideOutput = window.getSideOutput(outputTag);
sideOutput.print();
多并行度下的watermark
多并行度下,每个线程都有一个watermark。flink对齐会取所有线程最小的watermark
flink sql 下的时间
以event-time 来说明。
窗口触发时间
假定窗口: [window_start, window_end)
watermark延迟时间: window_delay
数据最大延迟时间:
window 10s, 延迟2s
{“word”: “x”, “ts”: “2019-12-12 18:00:01”}
{“word”: “x”, “ts”: “2019-12-12 18:00:11”}
窗口结束后,处于历史窗口的数据不会聚合。
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
{“word”: “x”, “ts”: “2019-12-12 18:00:33”}
{“word”: “x”, “ts”: “2019-12-12 19:00:16”}
watermark 并非立即更新。有间隔时间,并且有
数据有之后才会更新。
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
输入{“word”: “x”, “ts”: “2019-12-12 18:00:01”}
2019-12-12 18:00:00的窗口还是会进行计算。
需要输入多条{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
watermark才更新。
watermark更新,需要设置env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(entry.getPeriodicWatermarksInterval)
不设置这个watermark是不会更新。
就是窗口结束很久后,数据任然一直保留
若配置了AFTER WATERMARK 策略,需要显式地在TableConfig中配置minIdleStateRetentionTime标识能忍受的最大迟到时间。
minIdleStateRetentionTime在window中只影响窗口何时清除,不直接影响窗口何时触发, 例如配置为3600000,最多容忍1小时的迟到数据,超过这个时间的数据会直接丢弃
tableConfig.getMinIdleStateRetentionTime ,这个作为了allowLateness
watermark 的 delay ,定义的是数据最大乱序时间。也就是event_time 到达窗口结束时间,等待一定的时间才计算。
watermark >= 窗口结束时间,才会触发计算。
窗口结束后,allowLateness 定义数据最大的延迟时间。allowLateness 这段时间内到达的数据,
任然触发计算。
{“word”: “x”, “ts”: “2019-12-12 18:00:01”}
// watermark 更新到了09,任然小于 window_time
{“word”: “x”, “ts”: “2019-12-12 18:00:11”}
{“word”: “x”, “ts”: “2019-12-12 18:00:11”}
{“word”: “x”, “ts”: “2019-12-12 18:00:11”}
{“word”: “x”, “ts”: “2019-12-12 18:00:11”}
// 00窗口,任然完成了聚合
{“word”: “x”, “ts”: “2019-12-12 18:00:05”}
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
{“word”: “x”, “ts”: “2019-12-12 18:00:13”}
{“word”: “x”, “ts”: “2019-12-12 18:00:05”}
窗口的声明周期:
属于窗口的第一个元素到达,窗口就会创建
当时间(event-time/process-time), 超过窗口结束时间加允许的延迟时间
该窗口将被完全删除。
假设定义了,5分钟的滚动窗口,最大延迟时间为1分钟。
flink 创建了一个 12:00 到 12:05 的窗口,当watermark 经过 12:06的实时,窗口会被删除。
flink 只能删除基于时间的窗口,不能删除全局窗口。
{“word”: “x”, “ts”: “2019-12-12 18:00:59”}
public WindowOperatorBuilder withAllowedLateness(Duration allowedLateness) {
checkArgument(!allowedLateness.isNegative());
if (allowedLateness.toMillis() > 0) {
this.allowedLateness = allowedLateness.toMillis();
// allow late element, which means this window will send retractions
this.sendRetraction = true;
}
return this;
}
public KeyedProcessFunctionWithCleanupState(long minRetentionTime, long maxRetentionTime) {
this.minRetentionTime = minRetentionTime;
this.maxRetentionTime = maxRetentionTime;
this.stateCleaningEnabled = minRetentionTime > 1; // minRetentionTime 小于1不会清理状态。
}
protected void initCleanupTimeState(String stateName) {
if (stateCleaningEnabled) {
ValueStateDescriptor<Long> inputCntDescriptor = new ValueStateDescriptor<>(stateName, Types.LONG);
cleanupTimeState = getRuntimeContext().getState(inputCntDescriptor);
}
}
protected void registerProcessingCleanupTimer(Context ctx, long currentTime) throws Exception {
if (stateCleaningEnabled) {
registerProcessingCleanupTimer(
cleanupTimeState,
currentTime,
minRetentionTime,
maxRetentionTime,
ctx.timerService(
KeyedProcessFunctionWithCleanupState 管理是否清理状态。
minIdleStateRetentionTime 为0,窗口超过了watermark,窗口不会再触发,但是状态也不会清理。。。
这TMD是一个bug吧。
本文详细介绍了Flink的时间窗口概念,包括时间驱动和数据驱动的窗口类型,如tumbling和sliding窗口。重点讲解了水印(watermark)在处理乱序事件中的作用,以及如何设置允许的最大乱序时间。此外,还讨论了Flink SQL中基于event-time的时间处理,特别是窗口触发时间和延迟数据的处理策略。

202

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



