• 按键分区处理函数(KeyedProcessFunction)


    在Flink程序中,为了实现数据的聚合统计,或者开窗计算之类的功能,一般都要先用keyBy算子对数据流进行“按键分区”,得到一个KeyedStream。也就是指定一个键(key),按照它的哈希值(hash code)将数据分成不同的“组”,然后分配到不同的并行子任务上执行计算;这相当于做了一个逻辑分流的操作,从而可以充分利用并行计算的优势实时处理海量数据。另外只有在KeyedStream中才支持使用TimerService设置定时器的操作。所以一般情况下,我们都是先做了keyBy分区之后,再去定义处理操作.

    1、定时器(Timer)和定时服务(TimerService)

    KeyedProcessFunction的一个特色,就是可以灵活地使用定时器。定时器(timers)是处理函数中进行时间相关操作的主要机制。在.onTimer()方法中可以实现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发时间。注册定时器的功能,是通过上下文中提供的“定时服务”(TimerService)来实现的。定时服务与当前运行的环境有关。前面已经介绍过,ProcessFunction的上下文(Context)中提供了.timerService()方法,可以直接返回一个TimerService对象:

    public abstract TimerService timerService();

    TimerService是Flink关于时间和定时器的基础服务接口,包含以下六个方法:

    /** Interface for working with time and timers. */
    @PublicEvolving
    public interface TimerService {
    
        /** Error string for {@link UnsupportedOperationException} on registering timers. */
        String UNSUPPORTED_REGISTER_TIMER_MSG = "Setting timers is only supported on a keyed streams.";
    
        /** Error string for {@link UnsupportedOperationException} on deleting timers. */
        String UNSUPPORTED_DELETE_TIMER_MSG = "Deleting timers is only supported on a keyed streams.";
    
        /** Returns the current processing time. */
        long currentProcessingTime();
    
        /** Returns the current event-time watermark. */
        long currentWatermark();
    
        /**
         * Registers a timer to be fired when processing time passes the given time.
         *
         * <p>Timers can internally be scoped to keys and/or windows. When you set a timer in a keyed
         * context, such as in an operation on {@link
         * org.apache.flink.streaming.api.datastream.KeyedStream} then that context will also be active
         * when you receive the timer notification.
         */
        void registerProcessingTimeTimer(long time);
    
        /**
         * Registers a timer to be fired when the event time watermark passes the given time.
         *
         * <p>Timers can internally be scoped to keys and/or windows. When you set a timer in a keyed
         * context, such as in an operation on {@link
         * org.apache.flink.streaming.api.datastream.KeyedStream} then that context will also be active
         * when you receive the timer notification.
         */
        void registerEventTimeTimer(long time);
    
        /**
         * Deletes the processing-time timer with the given trigger time. This method has only an effect
         * if such a timer was previously registered and did not already expire.
         *
         * <p>Timers can internally be scoped to keys and/or windows. When you delete a timer, it is
         * removed from the current keyed context.
         */
        void deleteProcessingTimeTimer(long time);
    
        /**
         * Deletes the event-time timer with the given trigger time. This method has only an effect if
         * such a timer was previously registered and did not already expire.
         *
         * <p>Timers can internally be scoped to keys and/or windows. When you delete a timer, it is
         * removed from the current keyed context.
         */
        void deleteEventTimeTimer(long time);
    }

    六个方法可以分成两大类:基于处理时间和基于事件时间。而对应的操作主要有三个:获取当前时间,注册定时器,以及删除定时器。需要注意,尽管处理函数中都可以直接访问TimerService,不过只有基于KeyedStream的处理函数,才能去调用注册和删除定时器的方法;未作按键分区的DataStream不支持定时器操作,只能获取当前时间。对于处理时间和事件时间这两种类型的定时器,TimerService内部会用一个优先队列将它们的时间戳(timestamp)保存起来,排队等待执行。可以认为,定时器其实是KeyedStream上处理算子的一个状态,它以时间戳作为区分。所以TimerService会以键(key)和时间戳为标准,对定时器进行去重;也就是说对于每个key和时间戳,最多只有一个定时器,如果注册了多次,onTimer()方法也将只被调用一次。这样一来,在代码中就方便了很多,可以肆无忌惮地对一个key注册定时器,而不用担心重复定义——因为一个时间戳上的定时器只会触发一次。基于KeyedStream注册定时器时,会传入一个定时器触发的时间戳,这个时间戳的定时器对于每个key都是有效的。这样,代码并不需要做额外的处理,底层就可以直接对不同key进行独立的处理操作了。利用这个特性,有时可以故意降低时间戳的精度,来减少定时器的数量,从而提高处理性能。比如可以在设置定时器时只保留整秒数,那么定时器的触发频率就是最多1秒一次。

    ong coalescedTime = time /1000 * 1000;
    tx.timerService().registerProcessingTimeTimer(coalescedTime);

    这里注意定时器的时间戳必须是毫秒数,所以得到整秒之后还要乘以1000。定时器默认的区分精度是毫秒。另外Flink对.onTimer()和.processElement()方法是同步调用的(synchronous),所以也不会出现状态的并发修改。Flink的定时器同样具有容错性,它和状态一起都会被保存到一致性检查点(checkpoint)中。当发生故障时,Flink会重启并读取检查点中的状态,恢复定时器。如果是处理时间的定时器,有可能会出现已经“过期”的情况,这时它们会在重启时被立刻触发。

    2、KeyedProcessFunction的使用

    KeyedProcessFunction可以说是处理函数中的“嫡系部队”,可以认为是ProcessFunction的一个扩展。只要基于keyBy之后的KeyedStream,直接调用.process()方法,这时需要传入的参数就是KeyedProcessFunction的实现类。

    stream.keyBy( t -> t.f0 ).process(new MyKeyedProcessFunction())

    类似地,KeyedProcessFunction也是继承自AbstractRichFunction的一个抽象类,源码中定义如下:

       /**
         * Process one element from the input stream.
         *
         * <p>This function can output zero or more elements using the {@link Collector} parameter and
         * also update internal state or set timers using the {@link Context} parameter.
         *
         * @param value The input value.
         * @param ctx A {@link Context} that allows querying the timestamp of the element and getting a
         *     {@link TimerService} for registering timers and querying the time. The context is only
         *     valid during the invocation of this method, do not store it.
         * @param out The collector for returning result values.
         * @throws Exception This method may throw exceptions. Throwing an exception will cause the
         *     operation to fail and may trigger recovery.
         */
        public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;
    
        /**
         * Called when a timer set using {@link TimerService} fires.
         *
         * @param timestamp The timestamp of the firing timer.
         * @param ctx An {@link OnTimerContext} that allows querying the timestamp, the {@link
         *     TimeDomain}, and the key of the firing timer and getting a {@link TimerService} for
         *     registering timers and querying the time. The context is only valid during the invocation
         *     of this method, do not store it.
         * @param out The collector for returning result values.
         * @throws Exception This method may throw exceptions. Throwing an exception will cause the
         *     operation to fail and may trigger recovery.
         */
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}

    可以看到与ProcessFunction的定义几乎完全一样,区别只是在于类型参数多了一个K,这是当前按键分区的key的类型。同样地,必须实现一个.processElement()抽象方法,用来处理流中的每一个数据;另外还有一个非抽象方法.onTimer(),用来定义定时器触发时的回调操作。由于定时器只能在KeyedStream上使用,所以到了KeyedProcessFunction这里,才真正对时间有了精细的控制,定时方法.onTimer()才真正派上了用场。下面是一个使用处理时间定时器的具体示例:

        public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
            DataStreamSource<Event> eventDS = env.addSource(new ClickSource());
    
            eventDS.keyBy(data -> data.user)
                    //KeyedProcessFunction<String, Event, String>( K I O )
                    .process(new KeyedProcessFunction<String, Event, String>() {
                        @Override
                        public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
                            //获取处理时间
                            long currTs = ctx.timerService().currentProcessingTime(); // 处理时间
                            out.collect(ctx.getCurrentKey() + " 数据到达时间 -> " + new Timestamp(currTs));
                            //注册 10s 的定时器:处理时间定时器
                            ctx.timerService().registerProcessingTimeTimer(currTs + 10 * 1000);
    
                        }
                         @Override
                        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                            out.collect(ctx.getCurrentKey() + " 定时器触发时间 -> " + new Timestamp(timestamp));
                        }
                    }).print();
            env.execute();
        }

    在上面的代码中,由于定时器只能在KeyedStream上使用,所以先要进行keyBy;这里的.keyBy(data-> true)是将所有数据的key都指定为了true,其实就是所有数据拥有相同的key,会分配到同一个分区。之后自定义了一个KeyedProcessFunction,其中.processElement()方法是每来一个数据都会调用一次,主要是定义了一个10秒之后的定时器;而.onTimer()方法则会在定时器触发时调用。所以会看到,程序运行后先在控制台输出“数据到达”的信息,等待10秒之后,又会输出“定时器触发”的信息,打印出的时间间隔正是10秒。当然,上面的例子是处理时间的定时器,所以是真的需要等待10秒才会看到结果。事件时间语义下,又会有什么不同呢?可以对上面的代码略作修改,做一个测试:

        public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
            SingleOutputStreamOperator<Event> eventDS = env.addSource(new ClickSource())
                    .assignTimestampsAndWatermarks(
                            WatermarkStrategy
                                    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                                    .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                                        @Override
                                        public long extractTimestamp(Event element, long recordTimestamp) {
                                            return element.timestamp;
                                        }
                                    }));
    
    
            eventDS.keyBy(data -> data.user)
                    .process(new KeyedProcessFunction<String, Event, String>() {
                        @Override
                        public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
                            //获取处理时间
                            long currTs = ctx.timerService().currentWatermark();
                            out.collect(ctx.getCurrentKey() + " 数据到时间戳 -> " + new Timestamp(currTs) + " watermaker " + ctx.timerService().currentWatermark());
                            //注册 10s 的定时器
                            ctx.timerService().registerEventTimeTimer(currTs + 10 * 1000);
    
                        }
    
                        //触发
                        @Override
                        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                            out.collect(ctx.getCurrentKey() + " 定时器触发时间 -> " + new Timestamp(timestamp));
                        }
                    }).print();
    
            env.execute();
        }
    由于是事件时间语义,所以必须从数据中提取出数据产生的时间戳。这里为了更清楚地看到程序行为,自定义了一个数据源,发出三条测试数据,时间戳分别为1000、11000和11001,并且发出数据后都会停顿5秒。在代码中,依然将所有数据分到同一分区,然后在自定义的KeyedProcessFunction中使用定时器。同样地,每来一条数据,就将当前的数据时间戳和水位线信息输出,并注册一个10秒后(以当前数据时间戳为基准)的事件时间定时器。执行程序结果如下:
    令狐冲 数据到时间戳 -> 292278994-08-17 15:12:55.192 watermaker -9223372036854775808
    令狐冲 定时器触发时间 -> 292269055-12-03 00:47:14.192
    任盈盈 数据到时间戳 -> 2022-07-06 12:31:55.509 watermaker 1657081915509
    莫大 数据到时间戳 -> 2022-07-06 12:31:56.521 watermaker 1657081916521
    依琳 数据到时间戳 -> 2022-07-06 12:31:57.529 watermaker 1657081917529
    任盈盈 数据到时间戳 -> 2022-07-06 12:31:58.53 watermaker 1657081918530
    令狐冲 数据到时间戳 -> 2022-07-06 12:31:59.545 watermaker 1657081919545
    风清扬 数据到时间戳 -> 2022-07-06 12:32:00.55 watermaker 1657081920550
    风清扬 数据到时间戳 -> 2022-07-06 12:32:01.551 watermaker 1657081921551
    依琳 数据到时间戳 -> 2022-07-06 12:32:02.554 watermaker 1657081922554
    风清扬 数据到时间戳 -> 2022-07-06 12:32:03.567 watermaker 1657081923567
    任盈盈 数据到时间戳 -> 2022-07-06 12:32:04.577 watermaker 1657081924577
    任盈盈 定时器触发时间 -> 2022-07-06 12:32:05.509
    任盈盈 数据到时间戳 -> 2022-07-06 12:32:05.587 watermaker 1657081925587
    莫大 定时器触发时间 -> 2022-07-06 12:32:06.521
    风清扬 数据到时间戳 -> 2022-07-06 12:32:06.599 watermaker 1657081926599
    依琳 定时器触发时间 -> 2022-07-06 12:32:07.529
    莫大 数据到时间戳 -> 2022-07-06 12:32:07.61 watermaker 1657081927610
    任盈盈 定时器触发时间 -> 2022-07-06 12:32:08.53
    莫大 数据到时间戳 -> 2022-07-06 12:32:08.623 watermaker 1657081928623
    令狐冲 定时器触发时间 -> 2022-07-06 12:32:09.545
    依琳 数据到时间戳 -> 2022-07-06 12:32:09.633 watermaker 1657081929633
    风清扬 定时器触发时间 -> 2022-07-06 12:32:10.55
    令狐冲 数据到时间戳 -> 2022-07-06 12:32:10.643 watermaker 1657081930643
    风清扬 定时器触发时间 -> 2022-07-06 12:32:11.551
    风清扬 数据到时间戳 -> 2022-07-06 12:32:11.653 watermaker 1657081931653
    依琳 定时器触发时间 -> 2022-07-06 12:32:12.554

    每来一条数据,都会输出两行“数据到达”的信息,并以分割线隔开;两条数据到达的时间间隔为5秒。当第三条数据到达后,随后立即输出一条定时器触发的信息;再过5秒之后,剩余两条定时器信息输出,程序运行结束。可以发现,数据到来之后,当前的水位线与时间戳并不是一致的。当第一条数据到来,时间戳为1000,可水位线的生成是周期性的(默认200ms一次),不会立即发生改变,所以依然是最小值Long.MIN_VALUE;随后只要到了水位线生成的时间点(200ms到了),就会依据当前的最大时间戳1000来生成水位线了。这里没有设置水位线延迟,默认需要减去1毫秒,所以水位线推进到了999。而当时间戳为11000的第二条数据到来之后,水位线同样没有立即改变,仍然是999,就好像总是“滞后”数据一样。这样程序的行为就可以得到合理解释了。事件时间语义下,定时器触发的条件就是水位线推进到设定的时间。第一条数据到来后,设定的定时器时间为1000 + 10 * 1000 = 11000;而当时间戳为11000的第二条数据到来,水位线还处在999的位置,当然不会立即触发定时器;而之后水位线会推进到10999,同样是无法触发定时器的。必须等到第三条数据到来,将水位线真正推进到11000,就可以触发第一个定时器了。第三条数据发出后再过5秒,没有更多的数据生成了,整个程序运行结束将要退出,此时Flink会自动将水位线推进到长整型的最大值(Long.MAX_VALUE)。于是所有尚未触发的定时器这时就统一触发了,就在控制台看到了后两个定时器的触发信息。

  • 相关阅读:
    Civil 3D .NET二次开发第11章代码升级至2018版注意事项
    创建道路曲面
    ObjectARX® for Beginners: An Introduction
    mshcMigrate制作的mshc文件中有链接打不开
    Word 2013无法发布文章到博客园
    ionic 安装插件报错:源文本中存在无法识别的标记
    typescript文件中 使用回调函数无法调用函数外的变量和方法的办法
    ionic2---自定义插件
    angular2----使用swiper做轮播图
    angular2----router
  • 原文地址:https://www.cnblogs.com/wdh01/p/16450337.html
Copyright © 2020-2023  润新知