一文搞懂 Flink 处理水印全过程

1.正文

前面,我们已经学过了 一文搞懂 Flink 处理 Barrier 全过程,今天我们一起来看一下 flink 是如何处理水印的,以 Flink 消费 kafka 为例

        FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<String>(topics, new SimpleStringSchema(), properties);
        consumer.setStartFromLatest();
        consumer.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<String>() {
            @Override
            public long extractAscendingTimestamp(String element) {
                String locTime = "";
                try {
                    Map<String, Object> map = Json2Others.json2map(element);
                    locTime = map.get("locTime").toString();
                } catch (IOException e) {
                }
                LocalDateTime startDateTime =
                    LocalDateTime.parse(locTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                long milli = startDateTime.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli();
                return milli;
            }
        });

通过 assignTimestampsAndWatermarks 来对 watermarksPeriodic 进行赋值,当 KafkaFetcher ( 关于 KafkaFetcher 可以参考 写给大忙人看的Flink 消费 Kafka) 在初始化的时候,会创建 PeriodicWatermarkEmitter

// if we have periodic watermarks, kick off the interval scheduler
        // 在构建 fetcher 的时候创建 PeriodicWatermarkEmitter 并启动,以周期性发送
        if (timestampWatermarkMode == PERIODIC_WATERMARKS) {
            @SuppressWarnings("unchecked")
            PeriodicWatermarkEmitter periodicEmitter = new PeriodicWatermarkEmitter(
                    subscribedPartitionStates,
                    sourceContext,
                    processingTimeProvider,
                    autoWatermarkInterval);

            periodicEmitter.start();
        }

PeriodicWatermarkEmitter 主要的作用就是周期性的发送 watermark,默认周期是 200 ms,通过 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); 指定。

@Override
        //每隔 interval 时间会调用此方法
        public void onProcessingTime(long timestamp) throws Exception {

            long minAcrossAll = Long.MAX_VALUE;
            boolean isEffectiveMinAggregation = false;
            for (KafkaTopicPartitionState<?> state : allPartitions) {

                // we access the current watermark for the periodic assigners under the state
                // lock, to prevent concurrent modification to any internal variables
                final long curr;
                //noinspection SynchronizationOnLocalVariableOrMethodParameter
                synchronized (state) {
                    curr = ((KafkaTopicPartitionStateWithPeriodicWatermarks<?, ?>) state).getCurrentWatermarkTimestamp();
                }

                minAcrossAll = Math.min(minAcrossAll, curr);
                isEffectiveMinAggregation = true;
            }

            // emit next watermark, if there is one
            // 每隔 interval 对 watermark 进行合并取其最小的 watermark 发送至下游算子,并且保持单调递增
            if (isEffectiveMinAggregation && minAcrossAll > lastWatermarkTimestamp) {
                lastWatermarkTimestamp = minAcrossAll;
                emitter.emitWatermark(new Watermark(minAcrossAll));// StreamSourceContexts.ManualWatermarkContext,watermark 与 record 的发送路径是分开的
            }

            // schedule the next watermark
            timerService.registerTimer(timerService.getCurrentProcessingTime() + interval, this);
        }

其中 PeriodicWatermarkEmitter 最关键性的方法就是 onProcessingTime。做了两件事

  1. 在保持水印单调性的同时合并各个 partition 的水印( 即取各个 partition 水印的最小值 )
  2. 注册 process timer 以便周期性的调用 onProcessingTime

接下来就是进行一系列的发送,与 StreamRecord 的发送过程类似,具体可以参考 一文搞定 Flink 消费消息的全流程

下游算子通过 StreamInputProcessor.processInput 方法接受到 watermark 并处理

......
                    // 如果元素是 watermark,就准备更新当前 channel 的 watermark 值(并不是简单赋值,因为有乱序存在)
                    if (recordOrMark.isWatermark()) {
                        // handle watermark
                        statusWatermarkValve.inputWatermark(recordOrMark.asWatermark(), currentChannel);
                        continue;
                        // 如果元素是 status,就进行相应处理。可以看作是一个 flag,标志着当前 stream 接下来即将没有元素输入(idle),
                        // 或者当前即将由空闲状态转为有元素状态(active)。同时,StreamStatus 还对如何处理 watermark 有影响。
                        // 通过发送 status,上游的 operator 可以很方便的通知下游当前的数据流的状态。
                    } else if (recordOrMark.isStreamStatus()) {
                        // handle stream status 把对应的 channelStatuse 改为 空闲,
                        // 然后如果所有的 channelStatuse 都是 idle 则找到最大的 watermark 并处理,否则找到最小的 watermark 并处理
                        statusWatermarkValve.inputStreamStatus(recordOrMark.asStreamStatus(), currentChannel);
                        continue;
                    } 
                    ......

进入 StatusWatermarkValve.inputWatermark watermark 真正被处理的地方

//当水印输入时的处理操作
    public void inputWatermark(Watermark watermark, int channelIndex) {
        // ignore the input watermark if its input channel, or all input channels are idle (i.e. overall the valve is idle).
        // streamStatus 和 channelStatus 都是 active 才继续往下计算
        if (lastOutputStreamStatus.isActive() && channelStatuses[channelIndex].streamStatus.isActive()) {
            long watermarkMillis = watermark.getTimestamp();

            // if the input watermark's value is less than the last received watermark for its input channel, ignore it also.
            // ignore 小于已经接收到的 watermark 的 watermark,保持其单调性
            if (watermarkMillis > channelStatuses[channelIndex].watermark) {
                channelStatuses[channelIndex].watermark = watermarkMillis;

                // previously unaligned input channels are now aligned if its watermark has caught up
                // 如果之前未对齐的,现在对齐。
                if (!channelStatuses[channelIndex].isWatermarkAligned && watermarkMillis >= lastOutputWatermark) {
                    channelStatuses[channelIndex].isWatermarkAligned = true;
                }

                // ow, attempt to find a new min watermark across all aligned channels
                findAndOutputNewMinWatermarkAcrossAlignedChannels();
            }
        }
    }

isWatermarkAligned 其实就是由于之前是 idle,无需关心 watermark, 现在有数据了,需要关心 watermark 了。
而 findAndOutputNewMinWatermarkAcrossAlignedChannels 其实就是取 所有 channel 中的最小值,并且在保证 watermark 单调递增的情况下处理 watermark, 调用了 StreamInputProcessor.handleWatermark

@Override
        public void handleWatermark(Watermark watermark) {
            try {
                synchronized (lock) {
                    watermarkGauge.setCurrentWatermark(watermark.getTimestamp());//gauge
                    //处理 watermark 的入口
                    operator.processWatermark(watermark);
                }
            } catch (Exception e) {
                throw new RuntimeException("Exception occurred while processing valve output watermark: ", e);
            }
        }

我们以 AbstractStreamOperator 为例看具体是如何处理 watermark 的

public void processWatermark(Watermark mark) throws Exception {//operator.processWatermark(mark)
        if (timeServiceManager != null) {//有 timeService 则不为 null 如 window   InternalTimeServiceManager
            //timeService
            timeServiceManager.advanceWatermark(mark);
        }
        //处理结束之后继续往下游发送依次循环。
        output.emitWatermark(mark);
    }

当 filter、flatMap 等算子 timeServiceManager 均等于 null,我们以 windowOperator 为例,看 timeServiceManager.advanceWatermark(mark); 如何操作的

    public void advanceWatermark(Watermark watermark) throws Exception {
        for (InternalTimerServiceImpl<?, ?> service : timerServices.values()) {
            service.advanceWatermark(watermark.getTimestamp());//处理 watermark  event time 对于 trigger 的调用逻辑
        }
    }

继续调用

    public void advanceWatermark(long time) throws Exception {//watermark timestamp
        currentWatermark = time;

        InternalTimer<K, N> timer;
        
        while ((timer = eventTimeTimersQueue.peek()) != null && timer.getTimestamp() <= time) {//自定义,触发一系列满足条件的 key
            eventTimeTimersQueue.poll();
            keyContext.setCurrentKey(timer.getKey());//
            // 触发 triggerTarget.onEventTime
            triggerTarget.onEventTime(timer);
        }
    }

当注册的 eventTimer 的 timestamp <= currentwatermark 时,触发 triggerTarget.onEventTime(timer); 即调用 WindowOperator.onEventTime 方法
关于 windowOperator 的具体细节可以参考 写给大忙人看的 Flink Window原理

// 这个是通过 timer 来调用的
    // window processElement 的时候 registerCleanupTimer(window)   window.maxTimestamp()+allowedLateness
    // 和 eventTrigger onElement  registerEventTimeTimer(window.maxTimestamp()) 会创建相应的 timer
    // 所以当 allowedLateness 不为 0 的时候,同一个 window.maxTimestamp() 对应的 eventWindow 会触发两次,
    // 而且默认 windowAssigner.isEventTime() && isCleanupTime(triggerContext.window, timer.getTimestamp()) 才会清除 window state
    public void onEventTime(InternalTimer<K, W> timer) throws Exception {
        triggerContext.key = timer.getKey();
        triggerContext.window = timer.getNamespace();

        MergingWindowSet<W> mergingWindows;

        if (windowAssigner instanceof MergingWindowAssigner) {
            mergingWindows = getMergingWindowSet();
            W stateWindow = mergingWindows.getStateWindow(triggerContext.window);
            if (stateWindow == null) {
                // Timer firing for non-existent window, this can only happen if a
                // trigger did not clean up timers. We have already cleared the merging
                // window and therefore the Trigger state, however, so nothing to do.
                return;
            } else {
                windowState.setCurrentNamespace(stateWindow);
            }
        } else {
            windowState.setCurrentNamespace(triggerContext.window);
            mergingWindows = null;
        }

        TriggerResult triggerResult = triggerContext.onEventTime(timer.getTimestamp());

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

        if (triggerResult.isPurge()) {
            windowState.clear();
        }

        if (windowAssigner.isEventTime() && isCleanupTime(triggerContext.window, timer.getTimestamp())) {
            clearAllState(triggerContext.window, windowState, mergingWindows);
        }

        if (mergingWindows != null) {
            // need to make sure to update the merging state in state
            mergingWindows.persist();
        }
    }

关于窗口的触发有三种情况( 对应的源码部分可以参考 写给大忙人看的 Flink Window原理 )

  1. 然后就是当 time == window.maxTimestamp() 立即触发窗口
  2. window.maxTimestamp() <= ctx.getCurrentWatermark() 立即触发,即允许迟到范围内的数据到来
  3. window.maxTimestamp() + allowedLateness<= ctx.getCurrentWatermark() ,主要是为了针对延迟数据,保证数据的准确性

2.总结

水印的处理其实还蛮简单的,分两部分

    1. 水印在满足单调递增的情况下,要么直接发往下游( OneInputStreamOperator,像 keyby、filter、flatMap ),
    要么取最小值然后发往下游( TwoInputStreamOperator,像 co系列 coFlatMap、IntervalJoinOperator、TemporalJoin)
    2. 设置水印时间为当前 StreamRecord 中的时间戳,此时间戳是<= watermark ,因为 watermark 是单调递增的,而 StreamRecord 的时间戳就是提取出来的时间戳
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容