概念

window 类型

Tumbling Window

按照固定的时间间隔或者Tuple数量划分窗口。

例子一,按照固定时间滚动,5秒滚一个窗口:

| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |...
0       5             10         15    -> time
|   w1  |    w2       |     w3   |...

例子二,按照固定Tuple数量滚动,5个Tuple滚一个窗口

| e1 e2 e3 e4 e5 | e6 e7 e8 e9 e10 |...
0              5                10    -> count
|       w1       |        w2       |...

Sliding Window

也可以根据时间间隔或者Tuple数量来划分窗口,由于窗口长度也可以是时间或者Tuple数量,所以Sliding Window的形式比Tumbling Window多

例子一,窗口长度为10s,滑动间隔为5s

| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |...
0       5             10         15    -> time
w1----->|
|----------w2---------|
        |-----------w3-----------|

例子二,窗口长度为10s,滑动间隔为5个Tuple

| e1 e2 e3 e4 e5 | e6 e7 e8 e9 e10 | e11 ...
0             5                10       -> count
|-------w1------|
         |----------w2------------|

例子三,窗口长度为10个Tuple,滑动间隔为5个Tuple

| e1 e2 e3 e4 e5 | e6 e7 e8 e9 e10 | e11 e12 e13 e14 e15 | ...
0              5                 10                    15  -> count
|-------w1-------|
|-----------------w2---------------|
                 |-------------------w2-----------------|

例子四,窗口长度为10个Tuplp,滑动间隔为5s

| e1 e2 e3 e4 e5 e6 e7 e8 e9 e10 e11 e12 e13 e14 e15 ...
0                      5             10           15   -> time
|------------w1---------|
        |--------------w2--------------|

当窗口长度和滑动距离相等时,便成了滚动窗口

TriggerPolicy

窗口的触发策略,用于确定窗口的计算点,以时间或者Tuplt数量为标准

EvictionPolicy

窗口的事件回收策略,用标记的方式确定事件是否属于本次窗口

Watermark和Lag

Watermark用于标记数据的处理进度,Lag主要是应对数据乱序的情况。

从当前数据中的最新一条数据的时间算起,往前减去Lag,得到一个时间,这个时间成为Watermark,认为Watermark之前的数据都已经到了。

06:00:00的数据有可能在06:00:06之后才到,若Lag=5s,不好意思,进不了窗口,会被 当成超时的数据。

代码分析

EvictionPolicy

事件回收策略接口,目前有4种实现,用于将event标记为以下4个状态:

EXPIRE:失效的事件,会从queue中移除
PROCESS:将在最近的一个window中处理事件
KEEP:将在以后的window中处理些事件
STOP:停止处理些事件之后的event,认为此event之后的event将不再满足这个策略

接口代码如下:

public interface EvictionPolicy<T> { 
    enum Action {
       EXPIRE,PROCESS,KEEP,STOP
    }
    Action evict(Event<T> event);  //对事件进行标记
    void track(Event<T> event);  //对事件进行跟踪
    void setContext(EvictionContext context);  //设置context
}

分别介绍4种EvictionPolicy的实现类。

CountEvictionPolicy

以event数量做为窗口长度,只会标记两种状态:EXPIRE和PROCESS,有track()和evict()两个主要方法。

track方法如下,用成员变量currentCount记录已经跟踪的event数据,但并不包括watermark event。

@Override
public void track(Event<T> event) {
    if (!event.isWatermark()) {
        currentCount.incrementAndGet();
    }
}

evict方法如下,返回一个标记后的Action,成员变量threshold即为窗口长度,当currentCount的值大于threshold时,则表示此事件不在本次窗口处理之内,需要标记为EXPIRE,否则标记为PROCESS。

@Override
public Action evict(Event<T> event) {
    while (true) {
        long curVal = currentCount.get();
        if (curVal > threshold) {
            if (currentCount.compareAndSet(curVal, curVal - 1)) {
                return Action.EXPIRE;
            }
        } else {
            break;
        }
    }
    return Action.PROCESS;
}

WatermarkCountEvictionPolicy

继承自CountEvictionPolicy,增加referenceTime和processed两个私有成员变量

referenceTime:即watermark(后面有解释)
processed:本次窗口中已经被标记为PROCESS状态的event数量

WatermarkCountEvictionPolicy比CountEvictionPolicy多了一个KEEP状态,被标记为KEEP状态的event将在下一个窗口中处理。

track()方法实现为空

evict的实现如下

@Override
public Action evict(Event<T> event) {
    Action action;
    if (event.getTimestamp() <= referenceTime && processed < currentCount.get()) {
        action = super.evict(event);
        if (action == Action.PROCESS) {
            ++processed;
        }
    } else {
        action = Action.KEEP;
    }
    return action;
}

TimeEvictionPolicy

以时间做为窗口的长度,只会标记两种状态:EXPIRE和PROCESS,用事件的时间和最新的时间做为标记的依据

track方法实现为空

evicty方法如下,成员变量referenceTime可能是在window中计算而来,或者是系统当前时间,

public Action evict(Event<T> event) {
    long now = referenceTime == null ? System.currentTimeMillis() : referenceTime;
    long diff = now - event.getTimestamp();
    if (diff >= windowLength) {
        return Action.EXPIRE;
    }
    return Action.PROCESS;
}

WatermarkTimeEvictionPolicy

WatermarkTimeEvictionPolicy继承自TimeEvictionPolicy,增加了成员变量lag,标记的状态也比TimeEvictionPolicy多了STOP和KEEP

重写了evict方法

1   public Action evict(Event<T> event) {
2       long diff = referenceTime - event.getTimestamp();
3       if (diff < -lag) {
4           return Action.STOP; 
5       } else if (diff < 0) {
6           return Action.KEEP;
7       } else {
8           return super.evict(event);
9       }
10  }

第3行的判断可以理解为event的时间比referenceTime大了一个lag以上,在此标记为STOP,后面的scan方法将在此处停止,认为后面的event都不可能在本次窗口事件中处理

第5行的判断为event的时间比referenceTime大了一个lag以内,标记为KEEP。

TriggerPolicy

window的触发策略接口,满足触发条件时,WindowManager的onTrigger方法得以执行。接口代码如下:

public interface TriggerPolicy<T> {
    void track(Event<T> event);  //跟踪每个event,看是否满足触发条件
    void reset();
    void start();
    void shutdown();
}

也有4个实现类

CountTriggerPolicy

track方法如下,很简单,当跟踪的event数据大于count时,触发onTrigger,count为构造方法传的触发上限。

public void track(Event<T> event) {
    if (started && !event.isWatermark()) {
        if (currentCount.incrementAndGet() >= count) {
            evictionPolicy.setContext(new DefaultEvictionContext(System.currentTimeMillis()));
            handler.onTrigger();
        }
    }
}

WatermarkCountTriggerPolicy

直接实现TriggerPolicy,而不是继承CountTriggerPolicy。由于watermark event来触发onTrigger

track方法如下:

public void track(Event<T> event) {
    if (started && event.isWatermark()) {
        handleWaterMarkEvent(event);
    }
}
private void handleWaterMarkEvent(Event<T> waterMarkEvent) {
    long watermarkTs = waterMarkEvent.getTimestamp();
    List<Long> eventTs = windowManager.getSlidingCountTimestamps(lastProcessedTs, watermarkTs, count);
    for (long ts : eventTs) {
        evictionPolicy.setContext(new DefaultEvictionContext(ts, null, Long.valueOf(count)));
        handler.onTrigger();
        lastProcessedTs = ts;
    }
}

TimeTriggerPolicy

定时来触发窗口的计算。这个实现中用会启动一个ScheduledExecutorService定时器,周期性执行内部线程newTriggerTask,由newTriggerTask来调用onTrigger方法。(代码很简单,就不贴啦)

WatermarkTimeTriggerPolicy

这个类也是直接实现了TriggerPolicy接口,在track方法中判断事件是否是watermark event,来决定是否触发窗口计算。
track方法如下:

public void track(Event<T> event) {
    if (started && event.isWatermark()) {
        handleWaterMarkEvent(event);
    }
}

handleWaterMarkEvent方法通过while偱环,windowEndTs是当然计算的窗口的终点,起点就是终点减去窗口长度,本次窗口计算结束,onTrigger如果返回为true,windowEndTs将加上一个slidingIntervalMs(滑动长度)做为下一个窗口的终点。onTrigger如果返回为false,则表示这次计算的窗口中没有event,将通过getNextAlignedWindowT方法来找到下一个窗口的终点。

private void handleWaterMarkEvent(Event<T> event) {
    long watermarkTs = event.getTimestamp();
    long windowEndTs = nextWindowEndTs;
    LOG.debug("Window end ts {} Watermark ts {}", windowEndTs, watermarkTs);
    while (windowEndTs <= watermarkTs) {
        long currentCount = windowManager.getEventCount(windowEndTs);
        evictionPolicy.setContext(new DefaultEvictionContext(windowEndTs, currentCount));
        if (handler.onTrigger()) {
            windowEndTs += slidingIntervalMs;
        } else {
            /*
             * 如果上次onTrigger没有event,将通过getNextAlignedWindowTs方法来找到下一个窗口
             */
            long ts = getNextAlignedWindowTs(windowEndTs, watermarkTs);
            LOG.debug("Next aligned window end ts {}", ts);
            if (ts == Long.MAX_VALUE) {
                LOG.debug("No events to process between {} and watermark ts {}", windowEndTs, watermarkTs);
                break;
            }
            windowEndTs = ts;
        }
    }
    nextWindowEndTs = windowEndTs;
}

getNextAlignedWindowT方法通过找到windowEndTs到watermark这段时间里最早的一个event的时间戳,以nextTs + (slidingIntervalMs - (nextTs % slidingIntervalMs))的方式做时间对齐,找到小于最早时间戳里,能被滑动间隔整除的最小的一个时间点,做为下次计算的窗口的终点。

private long getNextAlignedWindowTs(long startTs, long endTs) {
    long nextTs = windowManager.getEarliestEventTs(startTs, endTs);
    if (nextTs == Long.MAX_VALUE || (nextTs % slidingIntervalMs == 0)) {
        return nextTs;
    }
    return nextTs + (slidingIntervalMs - (nextTs % slidingIntervalMs));
}

通过EvictionPolicy和TriggerPolicy这两个接口的组合,形成了前面所讲的6个窗口类型。这两个接口的实现类中,也可以分为带watermark的类和不带watermark的类,如果用户设置了时间字段,就会以带watermark的类处理event.

WaterMark的计算

在api中使用以下方法来改变watermark的产生周期,默认值是1000ms

public BaseWindowedBolt withWatermarkInterval(Duration interval)

interval实际被设置到了WaterMarkEventGenerator中,WaterMarkEventGenerator是一个线程,每隔interval时间间隔被ScheduledExecutorService执行一次,看以下WaterMarkEventGenerator中的几个关键方法。

track

/**
* Tracks the timestamp of the event in the stream, returns
* true if the event can be considered for processing or
* false if its a late event.
* track在WindowedBoltExecutor的execute方法中被调用,以(ts >= lastWaterMarkTs)来判断事件是否应该被放到queue中
*/

public boolean track(GlobalStreamId stream, long ts) {
    Long currentVal = streamToTs.get(stream);
    if (currentVal == null || ts > currentVal) {
        streamToTs.put(stream, ts);             //更新streamid对应的时间戳
    }
    checkFailures();
    return ts >= lastWaterMarkTs;             
}

computeWaterMarkTs

/**
*计算新的watermark,watermark是所有输入流中最新的tuple时间戳的最小值(减去延时)
*/
private long computeWaterMarkTs() {
    long ts = 0;
    // only if some data has arrived on each input stream
    if (streamToTs.size() >= inputStreams.size()) {
        ts = Long.MAX_VALUE;
        for (Map.Entry<GlobalStreamId, Long> entry : streamToTs.entrySet()) {
            ts = Math.min(ts, entry.getValue());
        }
    }
    return ts - eventTsLag;
}

Example

(来自于官网)

若基于以下参数和数据:

Window length = 20s, sliding interval = 10s, watermark emit frequency = 1s, max lag = 5s

当前时间 = 09:00:00

Tuples e1(6:00:03), e2(6:00:05), e3(6:00:07), e4(6:00:18), e5(6:00:26), e6(6:00:36)9:00:00 and 9:00:01之前到达

09:00:01产生了新的 watermark, w1 = 6:00:31 ,此时,早于 6:00:31 的event到达时,将被当成超时数据。

通过事件中最早的一个 timestamp (06:00:03)和sliding interval来计算后,将产生三个window,第一个window的终点是06:00:10,如下所示:

  1. 5:59:50 - 06:00:10 : e1, e2, e3
  2. 6:00:00 - 06:00:20 : e1, e2, e3, e4
  3. 6:00:10 - 06:00:30 : e4, e5

由于e6(6:00:36) 比watermark(6:00:31)要晚,所以不在本次触发中处理。

Tuples e7(8:00:25), e8(8:00:26), e9(8:00:27), e10(8:00:39)9:00:01 and 9:00:02之间到达

09:00:02 产生下一个 watermark, w2 = 08:00:34 ,此时,早于 8:00:34 的event到达时,将被当成超时数据。

将产生以下window:

  1. 6:00:20 - 06:00:40 : e5, e6
  2. 6:00:30 - 06:00:50 : e6
  3. 8:00:10 - 08:00:30 : e7, e8, e9

e10 (8:00:39 )比 watermark 8:00:34 晚,所以不在本次处理

其它

第一个窗口怎么产生

进程启动初次触发窗口计算时,WindowManager的onTrigger()方法会返回false,WatermarkTimeTriggerPolicy中的getNextAlignedWindowTs()方法会被调用,从而产生第一个真正的Window。

之后每次触发窗口计算,会用上一次计算的最后一个窗口的结束时间加上sliding interval得到本次计算的下一个窗口的结束时间。

Guarantees

storm window提供了at-least once的保障,tuple在window中经过 windowLength + slidingInterval后被expire后,然后自动被ack,因此topology.message.timeout.secs 必须远大于 windowLength + slidingInterval 。如果是基于count触发的window,需要去结合实际的窗口长度和滑动时间才调整超时时间的大小。