基于flink 1.9.1版本和blink palner。
对流中所有事件聚合是不可能的,因为通常流是无效的。所以需要window来划定范围。window是一种可以把无限数据切割为有限数据块的手段
根据类型,window有2种:
根据是否重叠,窗口分如下2种:
使用例子:
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 text = env.socketTextStream("10.10.40.33", 8000);
DataStream> words = text.flatMap(new FlatMapFunction>() {
@Override
public void flatMap(String value, Collector> 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");
}
}
窗口中每进入一条数据,就进行一次计算。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 text = env.socketTextStream("10.10.40.33", 8000);
DataStream> words = text.flatMap(new FlatMapFunction>() {
@Override
public void flatMap(String value, Collector> 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, Object, Tuple, TimeWindow>() {
// 参数说明
// window 表示当前窗口
// input 是这一个窗口内全部数据的迭代器
// out 表示收集的数据
// tuple 表示聚合的key
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable> input, Collector
stream中有3中时间:
在flink中,时间默认是Processing Time,可以通过代码修改:
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
在使用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 text = env.socketTextStream("10.10.40.33", 8000);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStream> input = text.map(new MapFunction>() {
@Override
public Tuple2 map(String value) throws Exception {
String[] split = value.split(",");
return new Tuple2<>(split[0], Long.parseLong(split[1]));
}
});
// 生成水印
DataStream> waterMarkStream = input.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks>() {
// 保存当前最大的日志时间
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 element, long previousElementTimestamp) {
return element.f1;
}
});
waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3))) // TumblingEventTimeWindows表示使用event time
.apply(new WindowFunction, Long, Tuple, TimeWindow>() {
// 对window内的数据进行排序
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable> input, Collector out) throws Exception {
String key = tuple.toString();
ArrayList arrayList = new ArrayList<>();
for (Tuple2 item: input) {
arrayList.add(item.f1);
}
Collections.sort(arrayList);
// 返回最后一条记录的时间
out.collect(arrayList.get(arrayList.size() - 1));
}
}).print();
env.execute("window");
}
}
在watermark中,window的触发时间
上面的例子来说含义就是: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> outputTag = new OutputTag>("late-data"){};
waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用TimeWindow效果一样
.sideOutputLateData(outputTag) // 设置output
//丢弃数据处理
//把迟到的数据暂时打印到控制台,实际中可以保存到其他存储介质中
DataStream> sideOutput = window.getSideOutput(outputTag);
sideOutput.print();
多并行度下,每个线程都有一个watermark。flink对齐会取所有线程最小的watermark
以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 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吧。