Flink窗口


1.简介

  Flink的窗口主要为4中类型:滚动、滑动、session和global。其中滚动和滑动是同一类,处理流程基本相同,session有特殊流程,global是全局只有一个窗口,不划分窗口。
  在另一个层面上分,可以分为是否进行keyBy,windowAll是不进行keyby的全域窗口。
  注意global和windowAll的差别。global是进行keyBy操作的,只是没有按时间再划分窗口;windowAll则可以按时间划分窗口,但是不做分区(keyBy)。这是两个层面上的东西,windowAll可以跟滚动、滑动、global任意结合(session好像没有结合)

1.1.基本结构

  window接口会返回WindowedStream,可以设置一些基本属性,然后进行后续的聚合操作。
  基本属性包括:触发器(trigger)、清除器(evictor)、允许延迟(allowedLateness)、侧边输出(sideOutput)
  延迟设置有三项,注意差别。1、触发器设置的延迟,这个是跟Watermarks相关的,设置什么时候触发窗口;2、allowedLateness,这个是在窗口触发后,继续保留一定时间,有新延迟数据会再次触发;3、sideOutput,这个是在窗口数据已经清理之后,单独做处理的流程。

1.2.对象关系

1.2.1.WindowedStream

  WindowedStream是window对应的stream类型,在调用window相关的算子时会返回WindowedStream。

1.2.2.WindowAssigner

  WindowAssigner是核心,实际的窗口类都是实现的WindowAssigner接口。
在这里插入图片描述

1.2.3.WindowOperator

  WindowOperator是对应的算子,在调用reduce等聚合算子后,会构建到WindowOperator。Operator是实际运行的算子,processElement接口对应算子处理过程,其中会调用到WindowAssigner的assignWindows确定窗口划分

@Override
public void processElement(StreamRecord<IN> element) throws Exception {
    final Collection<W> elementWindows =
            windowAssigner.assignWindows(
                    element.getValue(), element.getTimestamp(), windowAssignerContext);

1.3.调用链

  数据流转流程调用链,待补充

2.assignWindows

  确认数据落入的窗口,会返回一系列的窗口列表(只有滑动窗口才会落入多个窗口,所以对于步长和窗口长度差距很大的滑动窗口,可能存在隐患,一个数据在多个窗口,多个窗口状态都得缓存)
  以SlidingEventTimeWindows为例,过程如下,可以看出,一个数据会落入size / slide个窗口当中。timestamp获取在watermark流程当中,另介绍

@Override
public Collection<TimeWindow> assignWindows(
        Object element, long timestamp, WindowAssignerContext context) {
    if (timestamp > Long.MIN_VALUE) {
        List<TimeWindow> windows = new ArrayList<>((int) (size / slide));
        long lastStart = TimeWindow.getWindowStartWithOffset(timestamp, offset, slide);
        for (long start = lastStart; start > timestamp - size; start -= slide) {
            windows.add(new TimeWindow(start, start + size));
        }
        return windows;
    } else 

3.数据处理

  assignWindows是processElement处理的第一步,后续会真正进行数据处理。根据窗口类型,分为不同的处理:1、MergingWindowAssigner,即Session窗口;2、其他窗口
  session窗口有窗口合并机制,处理比较复杂,先看其他窗口,都是单纯的窗口计算
  整体就是对assignWindows获得的窗口遍历进行操作

for (W window : elementWindows) {

3.1.超时

  首先判断是否超时,就是已经过了窗口的结束时间,这里的超时是算上了allowedLateness的

protected boolean isWindowLate(W window) {
    return (windowAssigner.isEventTime()
            && (cleanupTime(window) <= internalTimerService.currentWatermark()));
}

  cleanupTime的计算如下,是包含allowedLateness的,计算的是窗口清理时间

private long cleanupTime(W window) {
    if (windowAssigner.isEventTime()) {
        long cleanupTime = window.maxTimestamp() + allowedLateness;
        return cleanupTime >= window.maxTimestamp() ? cleanupTime : Long.MAX_VALUE;
    } else {
        return window.maxTimestamp();
    }
}

3.2.窗口状态更新

  窗口是一个聚合型的,新数据到达需要更新窗口的内容,可以认为是累加结果
  State分很多类型,具体的后续再研究

windowState.setCurrentNamespace(window);
windowState.add(element.getValue());

3.3.窗口触发

  就是根据窗口触发器,确认是否触发窗口计算。触发器可以自定义设置,SlidingEventTimeWindows默认的触发器是EventTimeTrigger,触发逻辑如下
  触发逻辑简单来说就是数据Watermarks超过了窗口的最晚边界

public TriggerResult onElement(
        Object element, long timestamp, TimeWindow window, TriggerContext ctx)
        throws Exception {
    if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
        // if the watermark is already past the window fire immediately
        return TriggerResult.FIRE;
    } else {
        ctx.registerEventTimeTimer(window.maxTimestamp());
        return TriggerResult.CONTINUE;
    }
}

  如果触发了,计算进行窗口计算

if (triggerResult.isFire()) {
    ACC contents = windowState.get();
    if (contents != null) {
        emitWindowContents(triggerContext.window, contents);
    }
}

  窗口计算就是调用业务设置的function进行操作

private void emitWindowContents(W window, ACC contents) throws Exception {
    timestampedCollector.setAbsoluteTimestamp(window.maxTimestamp());
    processContext.window = window;
    userFunction.process(
            triggerContext.key, window, processContext, contents, timestampedCollector);
}

  这个userFunction会根据算子设置不同初始化时设置,比如跟reduce,就会创建InternalSingleValueAllWindowFunction

3.4.清理窗口

  完成窗口清理。Trigger执行后窗口有四种执行类型:CONTINUE、FIRE_AND_PURGE、FIRE、PURGE。其中CONTINUE不做操作,FIRE触发窗口,PURGE清理窗口

if (triggerResult.isPurge()) {
    windowState.clear();
}

3.5.注册清理时间器

  就是前面计算时间里的internalTimerService相关的内容,此处是将新的时间信息记录下来,以备下次使用
  应该没有定时器的功能,需要继研究下

protected void registerCleanupTimer(W window) {
    long cleanupTime = cleanupTime(window);
    if (cleanupTime == Long.MAX_VALUE) {
        // don't set a GC timer for "end of time"
        return;
    }

    if (windowAssigner.isEventTime()) {
        triggerContext.registerEventTimeTimer(cleanupTime);
    } else {
        triggerContext.registerProcessingTimeTimer(cleanupTime);
    }
}

4.侧边输出

  这一步是processElement在窗口处理之后的步骤,负责侧边输出
  侧边输出是窗口已经清理后对数据的处理,如果没有设置侧边输出,则数据将被丢弃

if (isSkippedElement && isElementLate(element)) {
    if (lateDataOutputTag != null) {
        sideOutput(element);
    } else {
        this.numLateRecordsDropped.inc();
    }
}

  是在allowedLateness之后的,allowedLateness过了会清理窗口

protected boolean isElementLate(StreamRecord<IN> element) {
    return (windowAssigner.isEventTime())
            && (element.getTimestamp() + allowedLateness
                    <= internalTimerService.currentWatermark());
}

5.onEventTime/onProcessingTime

  以上2-4步骤是processElement的处理步骤,是有输出触发调用的,WindowOperator中还有onEventTime/onProcessingTime方法,是由Watermarks触发的
  Watermarks在flink中也是像数据一样传输的,在接口AbstractStreamTaskNetworkInput(WindowOperator的上层调用链)的processElement方法中,根据传输的数据类型不同,有不同的处理

private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {
    if (recordOrMark.isRecord()) {
        output.emitRecord(recordOrMark.asRecord());
    } else if (recordOrMark.isWatermark()) {
        statusWatermarkValve.inputWatermark(
                recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);
    } else if (recordOrMark.isLatencyMarker()) {
        output.emitLatencyMarker(recordOrMark.asLatencyMarker());
    } else if (recordOrMark.isWatermarkStatus()) {
        statusWatermarkValve.inputWatermarkStatus(
                recordOrMark.asWatermarkStatus(),
                flattenedChannelIndices.get(lastChannel),
                output);
    } else {
        throw new UnsupportedOperationException("Unknown type of StreamElement");
    }
}

  Watermarks的后续处理流程会触发onEventTime/onProcessingTime方法,由于是Watermarks触发的,所有没有针对数据的处理,只是针对窗口状态和触发的处理,整体流程基本就是processElement去除了数据处理的部分

6.session窗口

6.1.assignWindows

  session窗口下,数据只会落入一个窗口。EventTimeSessionWindows的处理逻辑如下

@Override
public Collection<TimeWindow> assignWindows(
        Object element, long timestamp, WindowAssignerContext context) {
    return Collections.singletonList(new TimeWindow(timestamp, timestamp + sessionTimeout));
}

  这个窗口并不是最终数据落入的窗口,因为session窗口要根据间隔时间合并,所以这个窗口是以数据时间为起点,超时时间为长度的窗口,这样,后续就可以根据两个边界进行窗口合并。

6.2.窗口合并

  窗口合并操作在MergingWindowSet的addWindow的内,MergingWindowSet本身缓存了一系列的窗口,每次操作之后会更新这个列表

6.2.1.重叠窗口合并

  首先是获取当前所有窗口(包括此次新增的窗口),进行合并操作

windows.addAll(this.mapping.keySet());
windows.add(newWindow);

final Map<W, Collection<W>> mergeResults = new HashMap<>();
windowAssigner.mergeWindows(
        windows,
        new MergingWindowAssigner.MergeCallback<W>() {
            @Override
            public void merge(Collection<W> toBeMerged, W mergeResult) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Merging {} into {}", toBeMerged, mergeResult);
                }
                mergeResults.put(mergeResult, toBeMerged);
            }
        });

  最后逻辑在TimeWindow的mergeWindows。其处理逻辑核心为两步:1、排序,按窗口的起始时间做排序;2、归并,遍历窗口列表,将有重叠的窗口进行合并

Collections.sort(
        sortedWindows,
        new Comparator<TimeWindow>() {
            @Override
            public int compare(TimeWindow o1, TimeWindow o2) {
                return Long.compare(o1.getStart(), o2.getStart());
            }
        });

  归并逻辑如下,分支一是空值状态的添加;分支二归并;分支三无归并项直接进入列表

for (TimeWindow candidate : sortedWindows) {
    if (currentMerge == null) {
        currentMerge = new Tuple2<>();
        currentMerge.f0 = candidate;
        currentMerge.f1 = new HashSet<>();
        currentMerge.f1.add(candidate);
    } else if (currentMerge.f0.intersects(candidate)) {
        currentMerge.f0 = currentMerge.f0.cover(candidate);
        currentMerge.f1.add(candidate);
    } else {
        merged.add(currentMerge);
        currentMerge = new Tuple2<>();
        currentMerge.f0 = candidate;
        currentMerge.f1 = new HashSet<>();
        currentMerge.f1.add(candidate);
    }
}

  归并判断和具体操作如下,就是判断区域是否有重叠,有重叠的取最大上下限。由于是排序过的窗口,所以与下一个窗口没有重叠的话,本窗口肯定是独立的。需要确认的是窗口列表的来源,这个后文说明

public boolean intersects(TimeWindow other) {
    return this.start <= other.end && this.end >= other.start;
}

/** Returns the minimal window covers both this window and the given window. */
public TimeWindow cover(TimeWindow other) {
    return new TimeWindow(Math.min(start, other.start), Math.max(end, other.end));
}

6.2.2.窗口集合的增加

  第一个窗口时,不会调用到windowAssigner.mergeWindows里的自定义merge函数,所以mergeResults为空,这种情况下,在返回前的最后一个操作中,会将窗口加入集合
  实际这个方法起作用不止第一个窗口,产生一个独立窗口后都会加入

// the new window created a new, self-contained window without merging
if (mergeResults.isEmpty() || (resultWindow.equals(newWindow) && !mergedNewWindow)) {
    this.mapping.put(resultWindow, resultWindow);
}

6.2.3.state合并

  第一节中的合并是针对窗口范围进行的一个窗口合并,并没有实际对窗口数据进行合并。
  注意第一节中的内容,列表中的currentMerge.f1的窗口跟原来列表里的窗口是一致不变的,而且在最后做了一个翻转。也就是说,实际结果就是一个老窗口-新窗口的一个对应关系,这样就可以基于此进行state合并了

for (Tuple2<TimeWindow, Set<TimeWindow>> m : merged) {
    if (m.f1.size() > 1) {
        c.merge(m.f1, m.f0);
    }
}
  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值