Flink EventTime窗口不触发

6,840 阅读2分钟

前言

在本地调试一个 Flink 任务的时候,基于 EventTime 的窗口不触发的问题排查过程。

问题状况

Flink 任务接入本地的一个数据源,数据源只在接入时发送两条消息,之后不再发出数据,但 Flink 任务的 TumblingEventTimeWindows 始终不触发对这两条消息的计算。

排查

首先去追踪看下 TumblingEventTimeWindows 的 trigger :

@Override
public Trigger<Object, TimeWindow> getDefaultTrigger(StreamExecutionEnvironment env) {
	return EventTimeTrigger.create();
}

可以看到使用的是 EventTimeTrigger,继续追到里面看看触发逻辑:

@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;
	}
}

@Override
public TriggerResult onEventTime(long time, TimeWindow window, TriggerContext ctx) {
	return time == window.maxTimestamp() ?
		TriggerResult.FIRE :
		TriggerResult.CONTINUE;
}

从触发器里面知道,只有调用 onElement 和 onEventTime 时才有肯能会触发 FIRE。

onElement

先看 onElement 函数,这个函数是数据流中每来一条消息都会调用的,它的逻辑是:

  • 如果窗口最大时间小于等于当前的水印时间,则触发计算
  • 否则,注册一个定时器

一路追踪 ctx.getCurrentWatermark() 的调用逻辑,到 AbstractStreamOperator.processWatermark(),因为我的代码中使用的是:

assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<Tuple2<Long, JSONObject>>(Time.of(100, TimeUnit.MILLISECONDS)) {
    @Override
    public long extractTimestamp(Tuple2<Long, JSONObject> element) {
        return element.f1.getLongValue("timestamp");
    }
})

所以,找到 AbstractStreamOperator 的实现类 TimestampsAndPeriodicWatermarksOperator,看看它的 processWatermark 方法:

/**
 * Override the base implementation to completely ignore watermarks propagated from
 * upstream (we rely only on the {@link AssignerWithPeriodicWatermarks} to emit
 * watermarks from here).
 */
@Override
public void processWatermark(Watermark mark) throws Exception {
	// if we receive a Long.MAX_VALUE watermark we forward it since it is used
	// to signal the end of input and to not block watermark progress downstream
	if (mark.getTimestamp() == Long.MAX_VALUE && currentWatermark != Long.MAX_VALUE) {
		currentWatermark = Long.MAX_VALUE;
		output.emitWatermark(mark);
	}
}

从注释中可以知道,它已经完全忽略了从上游传播的水印,只用 AssignerWithPeriodicWatermarks 产生的水印,而我们用的 BoundedOutOfOrdernessTimestampExtractor 即是 AssignerWithPeriodicWatermarks 的一个实现,再看看 BoundedOutOfOrdernessTimestampExtractor. getCurrentWatermark 方法:

@Override
public final Watermark getCurrentWatermark() {
	// this guarantees that the watermark never goes backwards.
	long potentialWM = currentMaxTimestamp - maxOutOfOrderness;
	if (potentialWM >= lastEmittedWatermark) {
		lastEmittedWatermark = potentialWM;
	}
	return new Watermark(lastEmittedWatermark);
}

@Override
public final long extractTimestamp(T element, long previousElementTimestamp) {
	long timestamp = extractTimestamp(element);
	if (timestamp > currentMaxTimestamp) {
		currentMaxTimestamp = timestamp;
	}
	return timestamp;
}

getCurrentWatermark 的逻辑是根据数据源当前提取的最大时间戳减去允许的最大乱序时间的结果和上次触发水印的时间比较,然后产生新水印。

就是说不来新数据就不会产生新的水印,onElement 方法中就不会触发 FIRE。

onEventTime

与 onElement 方法一样,onEventTime 同样会追踪到TimestampsAndPeriodicWatermarksOperator.processWatermark 方法上,即该方法在BoundedOutOfOrdernessTimestampExtractor的实现中实际上是没有被调用的

问题原因

至此,问题原因已经定位了,因为 BoundedOutOfOrdernessTimestampExtractor 的实现中必须要根据新消息中的时间戳来产生水印然后触发窗口计算,而由于我代码中数据源发出两条消息后再不产生消息,所以窗口不触发。

解决方案

覆写BoundedOutOfOrdernessTimestampExtractor的getCurrentWatermark方法,让其在调用时即时没有新数据进来水印也能随着时间推进即可。