窗口分析
由于数据存在倾斜, 需要实现两阶段聚合, 这个时候萌生了连续使用eventtime window进行聚合的想法, 于是开始了以下的源码分析.
背景
flink的窗口函数, 是在流的基础上, 在一段时间内进行聚合, 对于eventtime而言, 你需要设置一个watermark进行提取, 最简单的形式如下:
datastream.assignTimestampsAndWatermarks(\your watermark\)
.map(\map function\)
.keyby(0)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(\process function\)
这个时候如果要二次聚合的话, 就需要再次keyby一下, 但process只能获取一条数据. 我想获得一批数据的话, 需要打开第二个窗口. 于是就有了以下的代码
datastream.assignTimestampsAndWatermarks(\your watermark\)
.map(\map function\)
.keyby(0)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(\process function\)
.keyby(1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
运行之后, 非常完美, 两个窗口一起触发. 数据没有任何问题. 可我还是有疑惑, window2的5秒是从window1处理完后, 还是从数据从watermark里出来来算?
经过一轮源码观察, 发现以下事实:
- 多个window公用一个水位线的时候, 多个window的水位上移同时进行, 但是在一个slot里有一个lock, 所以第二个窗口才会等第一个窗口的数据全部处理完毕, 才进行水位推进.
@Override
public void handleWatermark(Watermark watermark) {
try {
synchronized (lock) {
watermarkGauge.setCurrentWatermark(watermark.getTimestamp());
operator.processWatermark(watermark);
}
} catch (Exception e) {
throw new RuntimeException("Exception occurred while processing valve output watermark: ", e);
}
}
- 这个lock只是在单个slot中而言, 如果你重新keyby, 数据转移到其他的slot中, 这个lock数据可能会丢失.
- 如果两个window间添加其他高延时操作, 也可能导致数据丢失(比如加一个map thread sleep.)
结论就是, 请避免多窗口聚合, 或者打第二个水位.
window运算流程
流水账, 祟拜你看看
先来说说一下window窗口的运算流程
当你打了window函数后, 便有了一个windowOperator
对象, 它会持有一个internalTimerService
成员变量, 用来处理window相关的事情.
@Override
public void open() throws Exception {
super.open();
this.numLateRecordsDropped = metrics.counter(LATE_ELEMENTS_DROPPED_METRIC_NAME);
timestampedCollector = new TimestampedCollector<>(output);
internalTimerService =
getInternalTimerService("window-timers", windowSerializer, this);
...
一条数据过来的时候, 会走到WindowOperator的processElement中
@Override
public void processElement(StreamRecord element) throws Exception {
//这里会记录元素的时间戳
final Collection elementWindows = windowAssigner.assignWindows(
element.getValue(), element.getTimestamp(), windowAssignerContext);
...
//这里会去trigger里去判断元素是否触发trigger
TriggerResult triggerResult = triggerContext.onElement(element);
...
}
之后会走进EventTimeTrigger的onElement中
@Override
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;
}
}
在eventtime时间模式, 水位会以200毫秒的方式进行触发推动, 经过一些流程后, watermark会走到StausWatermarkValve中
private void findAndOutputNewMinWatermarkAcrossAlignedChannels() {
long newMinWatermark = Long.MAX_VALUE;
boolean hasAlignedChannels = false;
// determine new overall watermark by considering only watermark-aligned channels across all channels
for (InputChannelStatus channelStatus : channelStatuses) {
if (channelStatus.isWatermarkAligned) {
hasAlignedChannels = true;
newMinWatermark = Math.min(channelStatus.watermark, newMinWatermark);
}
}
// we acknowledge and output the new overall watermark if it really is aggregated
// from some remaining aligned channel, and is also larger than the last output watermark if (hasAlignedChannels && newMinWatermark > lastOutputWatermark) {
lastOutputWatermark = newMinWatermark;
//他会取所有channel(可以理解成并行)中的最低水位, 去进行水位推进的操作
outputHandler.handleWatermark(new Watermark(lastOutputWatermark));
}
}
之后进入到StreamInputProcessor, 这里加了把锁, 保证多个窗口顺序执行水位操作.
@Override
public void handleWatermark(Watermark watermark) {
try {
synchronized (lock) {
watermarkGauge.setCurrentWatermark(watermark.getTimestamp());
operator.processWatermark(watermark);
}
} catch (Exception e) {
throw new RuntimeException("Exception occurred while processing valve output watermark: ", e);
}
}
processWatermark之后会进入InternalTimerServiceImpl中, 执行advanceWatermark.
public void advanceWatermark(long time) throws Exception {
currentWatermark = time;
InternalTimer timer;
while ((timer = eventTimeTimersQueue.peek()) != null && timer.getTimestamp() <= time) {
eventTimeTimersQueue.poll();
keyContext.setCurrentKey(timer.getKey());
//这里 触发了水位推动
triggerTarget.onEventTime(timer);
}
}
在WindowOperator中, 水位终于推动了, 开始进行数据处理了.
@Override
public void onEventTime(InternalTimer timer) throws Exception {
...
if (triggerResult.isFire()) {
ACC contents = windowState.get();
if (contents != null) {
//推动了
emitWindowContents(triggerContext.window, contents);
}
}
...
}
之后就会进入你自己的后续task中了.