使用window的大致骨架:
// Keyed Window
stream
.keyBy(...) <- 按照一个Key进行分组
.window(assigner) <- 将数据流中的元素分配到相应的窗口中
[.trigger(...)] <- 指定触发器Trigger(可选)
[.evictor(...)] <- 指定清除器Evictor(可选)
.reduce/aggregate/process()/apply() <- 窗口处理函数Window Function
// Non-Keyed Window
stream
.windowAll() <- 不分组,将数据流中的所有元素分配到相应的窗口中
[.trigger(...)] <- 指定触发器Trigger(可选)
[.evictor(...)] <- 指定清除器Evictor(可选)
.reduce/aggregate/process() /apply() <- 窗口处理函数Window Function
一,窗口划分
-
WindowAssinger:决定如何划分窗口
-
window()和timeWindow():timeWindow()是window()的简化版,timeWindow配合环境的TimeCharacteristic设置确定使用ProcessTime或者EventTime;window()需要显式指定:
.window(TumblingProcessingTimeWindows.of(Time.days(1l), Time.hours(-8)))
窗口分为两种:基于时间的窗口和基于数量的窗口。
基于时间的窗口将窗口时间内的记录缓存起来,在时间窗口结束时由窗口函数批量处理。
基于数量的窗口在积累到一定数量后由窗口函数批量处理,如一分钟之内三次输错密码,就冻结账号。
基于时间的窗口分为三种:滚动窗口、滑动窗口、会话窗口。
二,窗口处理函数
达到窗口触发条件后,数据会由窗口处理函数处理,窗口处理函数分为两类:
- 一种是增量计算,如reduce和aggregate
- 一种是全量计算,如process、apply。
增量计算指的是窗口保存一份中间数据,每流入一个新元素,新元素与中间数据两两合一,生成新的中间数据,再保存到窗口中。特点是一条一条单独处理。
- reduce和aggregate的区别在于,reduce的中间结果的类型必须和输入类型一致,aggregate可以产生不同与输入类型的中间类型。
private static class MyAggregate implements AggregateFunction<Tuple2<String,Double>, Double, Double> {
double accumulator;
@Override
public Double createAccumulator() {
return accumulator = 0;
}
@Override
public Double add(Tuple2<String,Double> tuple2, Double accumulator) {
this.accumulator = tuple2.f1 + accumulator;
return this.accumulator;
}
@Override
public Double getResult(Double aDouble) {
return accumulator;
}
@Override
public Double merge(Double aDouble, Double acc1) {
return aDouble + acc1;
}
}
全量计算指的是窗口先缓存该窗口所有元素,等到触发条件后对窗口内的全量元素执行计算。特点是一次处理一批数据。
-
process和apply的区别在于,process可以获得上下文对象context,apply可以获取timeWindow对象。
如需要使用侧道输出则需要process。 -
ProcessWindowFunction相比AggregateFunction和ReduceFunction的应用场景更广,能解决的问题也更复杂。但ProcessWindowFunction需要将窗口中所有元素作为状态存储起来,这将占用大量的存储资源,尤其是在数据量大窗口多的场景下,使用不慎可能导致整个程序宕机。比如,每天的数据在TB级,我们需要Slide为十分钟Size为一小时的滑动窗口,这种设置会导致窗口数量很多,而且一个元素会被复制好多份分给每个所属的窗口,这将带来巨大的内存压力。window可能很大,比如双十一计算从凌晨开始的各品种的交易额,这个窗口随着时间的增长将会越来越大,数据越来越多,这些数据全部缓存在窗口中,会导致内存不足。
-
ProcessWindowFunction与增量计算相结合
当我们既想访问窗口里的元数据,又不想缓存窗口里的所有数据时,可以将ProcessWindowFunction与增量计算函数相reduce和aggregate结合。对于一个窗口来说,Flink先增量计算,窗口关闭前,将增量计算结果发送给ProcessWindowFunction作为输入再进行处理。
SingleOutputStreamOperator<AggOrderPojo> aggregate =
streamSource.keyBy(tuple -> tuple.f0)
.window(TumblingProcessingTimeWindows.of(Time.days(1l), Time.hours(-8)))
.trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(1)))
.aggregate(new MyAggregate(), new MyWindow());
/**
* 累加各品类金额
*/
private static class MyAggregate implements AggregateFunction<Tuple2<String,Double>, Double, Double> {
double accumulator;
@Override
public Double createAccumulator() {
return accumulator = 0;
}
@Override
public Double add(Tuple2<String,Double> tuple2, Double accumulator) {
this.accumulator = tuple2.f1 + accumulator;
return this.accumulator;
}
@Override
public Double getResult(Double aDouble) {
return accumulator;
}
@Override
public Double merge(Double aDouble, Double acc1) {
return aDouble + acc1;
}
}
private static class MyWindow implements WindowFunction<Double, AggOrderPojo, String, TimeWindow> {
private FastDateFormat fastDateFormat = FastDateFormat.getInstance("yyyy-MM-ss HH:mm:ss");
@Override
public void apply(String s, TimeWindow timeWindow, Iterable<Double> iterable, Collector<AggOrderPojo> collector) throws Exception {
AggOrderPojo aggOrderPojo = new AggOrderPojo();
aggOrderPojo.setCategory(s);
double total = 0;
for(Double d : iterable) {
total += d;
}
aggOrderPojo.setTotalPrice(total);
aggOrderPojo.setOrderDate(fastDateFormat.format(System.currentTimeMillis()));
collector.collect(aggOrderPojo);
}
}
三,Trigger
触发器(Trigger)决定了何时启动Window Function来处理窗口中的数据以及何时将窗口内的数据清理。
每个窗口都有一个默认的Trigger,比如前文这些例子都是基于Processing Time的时间窗口,当到达窗口的结束时间时,Trigger以及对应的计算被触发。
如果我们有一些个性化的触发条件,比如窗口中遇到某些特定的元素、元素总数达到一定数量或窗口中的元素到达时满足某种特定的模式时,我们可以自定义一个Trigger。
我们甚至可以在Trigger中定义一些提前计算的逻辑,比如在Event Time语义中,虽然Watermark还未到达,但是我们可以定义提前计算输出的逻辑,以快速获取计算结果,获得更低的延迟。
当满足某个条件,Trigger会返回一个名为TriggerResult的结果:
- CONTINUE:什么都不做。
- FIRE:启动计算并将结果发送给下游,不清理窗口数据。
- PURGE:清理窗口数据但不执行计算。
- FIRE_AND_PURGE:启动计算,发送结果然后清理窗口数据。
DEMO: 某些业务场景中某些不符合常规的情况,如价格大幅涨跌、点击量瞬间大幅上升等情况,需要及时识别出来。如果窗口长度是60秒,如果价格跌幅超过5%,则立即执行Window Function,如果价格跌幅在1%到5%之内,那么10秒后触发Window Function。
class MyTrigger extends Trigger[StockPrice, TimeWindow] {
override def onElement(element: StockPrice,
time: Long,
window: TimeWindow,
triggerContext: Trigger.TriggerContext): TriggerResult = {
val lastPriceState: ValueState[Double] = triggerContext.getPartitionedState(new ValueStateDescriptor[Double]("lastPriceState", classOf[Double]))
// 设置返回默认值为CONTINUE
var triggerResult: TriggerResult = TriggerResult.CONTINUE
// 第一次使用lastPriceState时状态是空的,需要先进行判断
// 状态数据由Java端生成,如果是空,返回一个null
// 如果直接使用Scala的Double,需要使用下面的方法判断是否为空
if (Option(lastPriceState.value()).isDefined) {
if ((lastPriceState.value() - element.price) > lastPriceState.value() * 0.05) {
// 如果价格跌幅大于5%,直接FIRE_AND_PURGE
triggerResult = TriggerResult.FIRE_AND_PURGE
} else if ((lastPriceState.value() - element.price) > lastPriceState.value() * 0.01) {
val t = triggerContext.getCurrentProcessingTime + (10 * 1000 - (triggerContext.getCurrentProcessingTime % 10 * 1000))
// 给10秒后注册一个Timer
triggerContext.registerProcessingTimeTimer(t)
}
}
lastPriceState.update(element.price)
triggerResult
}
// 我们不用EventTime,直接返回一个CONTINUE
override def onEventTime(time: Long, window: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.CONTINUE
}
override def onProcessingTime(time: Long, window: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.FIRE_AND_PURGE
}
override def clear(window: TimeWindow, triggerContext: Trigger.TriggerContext): Unit = {
val lastPrice: ValueState[Double] = triggerContext.getPartitionedState(new ValueStateDescriptor[Double]("lastPrice", classOf[Double]))
lastPrice.clear()
}
}
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
val input: DataStream[StockPrice] = ...
val average = input
.keyBy(s => s.symbol)
.timeWindow(Time.seconds(60))
.trigger(new MyTrigger)
.aggregate(new AverageAggregate)
四,Evictor
清除器(Evictor)是在WindowAssigner和Trigger的基础上的一个可选选项,用来清除一些数据。我们可以在Window Function执行前或执行后调用Evictor。
/**
* T为元素类型
* W为窗口
*/
public interface Evictor<T, W extends Window> extends Serializable {
/**
* 在Window Function前调用
*/
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* 在Window Function后调用
*/
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* Evictor的上下文
*/
interface EvictorContext {
long getCurrentProcessingTime();
MetricGroup getMetricGroup();
long getCurrentWatermark();
}
}
evictBefore和evictAfter分别在Window Function之前和之后被调用,窗口的所有元素被放在了Iterable<TimestampedValue>,要实现自己的清除逻辑。当然,增量计算的ReduceFunction和AggregateFunction,没必要使用Evictor。