自定义Trigger解决Flink窗口延迟数据丢失问题

如题,今天来分享下之前解决过的一个问题,即Flink窗口延迟数据丢失。

流计算在支持Event Time的时候,引入了Watermark的概念(MillWheel提出这个概念的时候是叫Low Watermark,即低水位,鄙视翻译成水印的:),所有早于Watermark的数据都被认为是延迟数据。而在Flink的窗口计算当中,允许用户指定数据延迟的最大时间,即设置allowedLateness,超过这个时间的数据将会被丢弃,不参与窗口计算。Flink会将被丢弃的数据用SideOutput输出。

具体的实现在WindowOperator#processElement

			for (W window: elementWindows) {

				// drop if the window is already late
				if (isWindowLate(window)) {
					continue;
				}
				isSkippedElement = false;

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

				triggerContext.key = key;
				triggerContext.window = window;

				TriggerResult triggerResult = triggerContext.onElement(element);

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

				if (triggerResult.isPurge()) {
					windowState.clear();
				}
				registerCleanupTimer(window);
			}
	/**
	 * Returns {@code true} if the watermark is after the end timestamp plus the allowed lateness
	 * of the given window.
	 */
	protected boolean isWindowLate(W window) {
		return (windowAssigner.isEventTime() && (cleanupTime(window) <= internalTimerService.currentWatermark()));
	}
	/**
	 * Returns the cleanup time for a window, which is
	 * {@code window.maxTimestamp + allowedLateness}. In
	 * case this leads to a value greater than {@link Long#MAX_VALUE}
	 * then a cleanup time of {@link Long#MAX_VALUE} is
	 * returned.
	 *
	 * @param window the window whose cleanup time we are computing.
	 */
	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();
		}
	}

可以看到计算cleanupTime的时候是使用数据所属窗口的最大时间加上用户设置的allowedLateness,超过cleanupTime的窗口将不会参与计算。

在某些业务场景下,其实是不希望丢掉这部分数据的,尽管也可以利用SideOutput来处理,但还是比较麻烦,因此我们便需求其他解决方案。在仔细阅读过WindowOperator的代码后,我发现可以通过实现一个Trigger来解决这个问题。这里需要大家对Flink的窗口实现有一个基本了解,推荐阅读下官方这篇博客,Introducing Stream Windows in Apache Flink


博客中的这张图基本上把Flink的窗口实现讲明白了,每一条输入数据都会由WindowAssigner分配一个窗口,并且会带有一个默认的Trigger,由Trigger来决定什么时候输出窗口数据(emitWindowContents)。

回到我们的问题上来,上面在processElement的时候可以看到有一个registerCleanupTimer

	/**
	 * Registers a timer to cleanup the content of the window.
	 * @param window
	 * 					the window whose state to discard
	 */
	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);
		}
	}

该方法用来注册定时器,当超过cleanupTime的时候会清理掉窗口的state,代码在WindowOperator#onEventTime

要解决这个问题,首先第一步需要确保延迟窗口不会被丢弃,看了上面的代码之后,可以想到,把allowedLateness设置成Long.MAX_VALUE,这样的话isWindowLate就一直是返回false,但是这样做的话会导致在registerCleanupTimer的时候没办法注册到定时器来清理窗口的state,所有窗口的state会一直被保存,明显是不可行的。还好,Trigger可以解决这个问题。以下是自定义Trigger实现的主要部分,

	@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_AND_PURGE;
		} else {
			ctx.registerEventTimeTimer(window.maxTimestamp());
			return TriggerResult.CONTINUE;
		}
	}

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

onElement是在收到数据后判断是否需要输出窗口数据,上面的实现,在收到非延迟窗口时,注册窗口最大时间定时器,在Watermark超过该时间后,onEventTime方法会被调用,此时我们直接输出窗口数据(即FIRE)并清理窗口state(即PURGE);而在收到延迟窗口时,我们就直接就输出窗口数据并清理窗口state。

使用起来大概是像下面这样,

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
    ...
    dataStream.keyBy(...)
        .timeWindow(...)
        .trigger(new MyTrigger())
        .allowedLateness(Time.milliseconds(Long.MAX_VALUE))
        ...

这么做可以解决上面提到的丢失问题,但是从输出数据来看,同一个窗口,会存在多个输出数据,即非延迟的一个,延迟的一个或多个,因此在使用这些输出数据的时候需要再多做一步聚合,把同一个窗口的这些结果数据聚合起来才是正确的结果。

另外,上面的实现其实有个地方可以优化下,在收到延迟窗口的时候,可以不用马上FIRE_AND_PURGE,而是注册定时器,这样就可以在处理掉一批延迟数据后再进行FIRE_AND_PURGE了。

alright,今天就到这里,have fun la 😃

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值