概念
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,如下所示:
-
5:59:50 - 06:00:10
: e1, e2, e3 -
6:00:00 - 06:00:20
: e1, e2, e3, e4 -
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:
-
6:00:20 - 06:00:40
: e5, e6 -
6:00:30 - 06:00:50
: e6 -
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,需要去结合实际的窗口长度和滑动时间才调整超时时间的大小。