Flink中的水位线

文章目录

    • 水位线(Watermark)
        • 水位线的定义
        • 有序流中的水位线
        • 乱序流中的水位线
        • 水位线的特性
      • 水位线的生成
        • 水位线生成策略
        • Flink内置水位线生成器
        • 自定义水位线策略
          • (1)周期性水位线生成器
          • 断点式水位线生成器
        • 在自定义数据源中发送水位线
      • 水位线的传递
      • 水位线的总结

水位线(Watermark)

先引申出Flink中的时间语义:处理时间、事件时间

  • 处理时间:执行处理操作的机器的系统时间
  • 事件时间:数据生成的时间,自带一个时间戳(Timestamp)
水位线的定义

在事件时间的语义下,不依赖系统时间,而是基于数据自带的时间戳去定义一个时钟,用来表示当前时间的进展。

在数据流中加入一个时钟标记,记录当前的事件时间,这个标记可以直接广播到下游,当下游任务收到这个标记,就可以更新自己的时钟了,这种类似于水流中用来做标志的记号,在Flink中被称为水位线

Flink中的水位线_第1张图片

有序流中的水位线

Flink中的水位线_第2张图片

在理想状态下,数据应该按照它们生成的先后顺序、排好队进入流中。在实际应用中,如果当前数据量非常大,可能会有很多数据的时间戳是相同的,这时每来一条时间就提取时间戳、插入水位线就做了大量的无用功。而且即使时间戳不同,同时涌来的时间差会非常小(比如几毫秒),往往对处理计算没什么影响,故为了提高效率,一般会采取每隔一段时间生成一个水位线(对应于时间戳)。这个每隔的时间周期指的是处理时间(系统时间)

乱序流中的水位线

Flink中的水位线_第3张图片

在分布式系统中,数据在节点间的传输,会因为网络传输延迟的不确定性,导致顺序发生改变,这就是所谓的乱序。为了从乱序流中插入水位线,我们就需要定义一个规则:插入新的水位线时,先判断一下时间戳是否比之前的大,否则就不再生成新的水位线。

Flink中的水位线_第4张图片

如果考虑到大量数据同时到来的处理效率,我们同样可以周期性地生成水位线,这时只需要保存一下之前所有数据中最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新的水位线。

Flink中的水位线_第5张图片
但也有一个问题,我们无法正确处理“迟到”的数据。为了让窗口能正确收集到迟到的数据,我们可以等上几秒,也就是用当前已有数据的最大时间戳减去几秒,就是要插入的水位线的时间戳

Flink中的水位线_第6张图片

水位线的特性
  • 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
  • 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
  • 水位线是基于数据的时间戳生成
  • 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
  • 水位线可以通过设置延迟,来保证正确处理乱序数据
  • 一个水位线t,表示在当前流中事件时间已经达到了时间戳t,代表t之前的所有数据都到齐了,之后流中不会出现时间戳t’ <=t 的数据

水位线是Flink流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成堆乱序数据的正确处理

水位线的生成

计算处理更快、实时性更强、计算准确性尽可能得到保障,我们就需要设置合理的水位线。

水位线生成策略

在Flink的DataStream API中,有一个单独用于生成水位线的方法:assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间:

 public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks(
         WatermarkStrategy<T> watermarkStrategy)

上述方法需要传入一个watermarkStrategy参数,这就是所谓的水位线生成策略

public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>,
	WatermarkGeneratorSupplier<T>{
 		@Override
 		TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
 		@Override
 		WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
  • TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础
  • WatermarkGenerator:主要负责按照既定方式,基于时间戳生成水位线。在WatermarkGenerator接口中有两个方法:onEvent,onPeriodicEmit
    • onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个WatermarkOutput,可以基于事件做出各种操作
    • onPeriodicEmit:周期性调用的方法,可以由WatermarkOutput发出水位线。周期时间为处理时间,可以调用环境配置的…setAutoWatermarkInterval()方法来设置,默认为200ms
public interface WatermarkGenerator<T> {
    void onEvent(T event, long eventTimestamp, WatermarkOutput output);
    void onPeriodicEmit(WatermarkOutput output);
}
Flink内置水位线生成器

​ WatermarkStrategy这个接口是一个生成水位线策略的抽象,让我们可以灵活地实现自己的需求,如果想要自己实现还是比较麻烦的。Flink提供了内置的水位线生成器WatermarkGenerator,不仅开箱即用简化了编程,而且也为我们自定义水位线策略提供了模板。

​ 这两个生成器可以通过调用WatermarkStrategy 的静态辅助方法来创建。它们都是周期性 生成水位线的,分别对应着处理有序流和乱序流的场景

import com.yingzi.chapter05.Source.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.time.Duration;

public class WatermarkTest {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.getConfig().setAutoWatermarkInterval(100);

        //从元素中读取数据
        SingleOutputStreamOperator<Event> stream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Bob", "./prod?id=1", 3300L),
                new Event("Bob", "./home", 3500L),
                new Event("Alice", "./prod?id=200", 3200L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L))
                //有序流的watermark生成
//                .assignTimestampsAndWatermarks(WatermarkStrategy
//                        .forMonotonousTimestamps()
//                        .withTimestampAssigner(new SerializableTimestampAssigner() {
//                            @Override
//                            public long extractTimestamp(Event element, long recordTimestamp) {
//                                return element.timestamp;
//                            }
//                        }))
                //乱序流的watermark生成
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        }));

        env.execute();
    }
}

事实上,有序流的水位线生成器本质上和乱序流式一样的,相当于延迟设为0的乱序流水位线生成器,两者完全相同:

WatermarkStrategy.forMonotonousTimestamps()
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))

注意:乱序流中生成的水位线真正的时间戳,其实是当前最大时间戳 - 延迟时间 - 1,这里单位是毫秒。

public void onPeriodicEmit(WatermarkOutput output) {
 	output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
}
自定义水位线策略

一般来说,Flink内置的水位线生成器就可以满足应用需求了。不过有时我们得业务逻辑可能非常复杂,这就必须自定义实现水位线策略WatermarkStrategy。WatermarkGenerator接口中有两个方法:onEvent()、onPeriodicEmit(),前者是在每个时间到来时调用,后者由框架周期性调用。周期性调用的方法中发出水位线,自然就是周期性生成水位线;而在事件触发的方法中发出水位线,自然就是断点式生成了。两种方式的不同就集中体现在这两个方法的实现上

(1)周期性水位线生成器

周期性生成器一般是通过onEvent()观察判断输入的事件,而在onPeriodicEmit()里发出水位线

import com.yingzi.chapter05.Source.ClickSource;
import com.yingzi.chapter05.Source.Event;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class CustomWatermarkTest {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new CustomWatermarkStrategy()).print();

        env.execute();
    }

    public static class CustomWatermarkStrategy implements WatermarkStrategy<Event> {
        @Override
        public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.timestamp; // 告诉程序数据源里的时间戳是哪一个字段
                }
            };
        }

        @Override
        public WatermarkGenerator<Event>
        createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new CustomPeriodicGenerator();
        }
    }

    public static class CustomPeriodicGenerator implements WatermarkGenerator<Event> {
        private Long delayTime = 5000L; // 延迟时间
        private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

        @Override
        public void onEvent(Event event, long eventTimestamp, WatermarkOutput
                output) {
            // 每来一条数据就调用一次
            maxTs = Math.max(event.timestamp, maxTs); // 更新最大时间戳
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 发射水位线,默认 200ms 调用一次
            output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
        }
    }
}
断点式水位线生成器

断点式生成器会不停地检测onEvent()中的事件,当发现带有水位线信息的特殊事件时,立即发出水位线。一般来说,断点式生成器不会通过onPeriodicEmit()发出水位线

public class CustomPunctuatedGenerator implements WatermarkGenerator<Event> {
	@Override
 	public void onEvent(Event r, long eventTimestamp, WatermarkOutput output) {
	// 只有在遇到特定的 itemId 时,才发出水位线
 		if (r.user.equals("Mary")) {
 			output.emitWatermark(new Watermark(r.timestamp - 1));
 		}
 	}
     @Override
     public void onPeriodicEmit(WatermarkOutput output) {
     	// 不需要做任何事情,因为我们在 onEvent 方法中发射了水位线
     }
}

我们在 onEvent()中判断当前事件的 user 字段,只有遇到“Mary”这个特殊的值时,才调用 output.emitWatermark()发出水位线。这个过程是完全依靠事件来触发的,所以水位线的生成一 定在某个数据到来之后

在自定义数据源中发送水位线

我们也可以在自定义的数据源中抽取事件时间,然后发送水位线。这里要注意的是,在自 定义数据源中发送了水位线以后,就不能再在程序中使用 assignTimestampsAndWatermarks 方 法 来 生 成 水 位 线 了 。 在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用 assignTimestampsAndWatermarks 方法生成水位线二者只能取其一。

在自定义水位线中生成水位线相比 assignTimestampsAndWatermarks 方法更加灵活,可以 任意的产生周期性的、非周期性的水位线,以及水位线的大小也完全由我们自定义。所以非常 适合用来编写 Flink 的测试程序,测试 Flink 的各种各样的特性

import com.yingzi.chapter05.Source.Event;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.watermark.Watermark;

import java.util.Calendar;
import java.util.Random;

public class EmitWatermarkInSourceFunction {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        env.addSource(new ClickSourceWithWatermark()).print();
        env.execute();
    }

    // 泛型是数据源中的类型
    public static class ClickSourceWithWatermark implements SourceFunction<Event> {
        private boolean running = true;

        @Override
        public void run(SourceContext<Event> sourceContext) throws Exception {
            Random random = new Random();
            String[] userArr = {"Mary", "Bob", "Alice"};
            String[] urlArr = {"./home", "./cart", "./prod?id=1"};
            while (running) {
                long currTs = Calendar.getInstance().getTimeInMillis(); // 毫秒时间戳
                String username = userArr[random.nextInt(userArr.length)];
                String url = urlArr[random.nextInt(urlArr.length)];
                Event event = new Event(username, url, currTs);
                // 使用 collectWithTimestamp 方法将数据发送出去,并指明数据中的时间戳的字段
                sourceContext.collectWithTimestamp(event, event.timestamp);
                // 发送水位线
                sourceContext.emitWatermark(new Watermark(event.timestamp - 1L));
                Thread.sleep(1000L);
            }
        }

        @Override
        public void cancel() {
            running = false;
        }
    }
}

水位线的传递

Flink中的水位线_第7张图片
在实际应用中往往上下游都有多个并行子任务,为了统一推进事件时间的进展,我们要求上游任务处理完水位线、时钟改变之后,要把当前的水位线广播给所有的下游任务。这样,后续任务就不需要依赖原始数据中的时间戳,也可以知道当前事件时间了。

上游并行子任务发来不同的水位线,当前任务会为每一个分区设置一个“分区水位线” (Partition Watermark),这是一个分区时钟;而当前任务自己的时钟,就是所有分区时钟里最小的那个。

水位线的总结

水位线在事件时间的世界里面,承担了时钟的角色,是唯一的时间尺度。

水位线的默认计算公式:水位线 = 观察到的最大事件时间 – 最大延迟时间 – 1 毫秒

在数据流开始之前,Flink 会插入一个大小是负无穷大的水位线,而在数据流结束时,Flink 会插入一个正无穷大)的水位线,保 证所有的窗口闭合以及所有的定时器都被触发。

对于离线数据集,Flink 也会将其作为流读入,也就是一条数据一条数据的读取。在这种 情况下,Flink 对于离线数据集,只会插入两次水位线,也就是在最开始处插入负无穷大的水 位线,在结束位置插入一个正无穷大的水位线。因为只需要插入两次水位线,就可以保证计算的正确,无需在数据流的中间插入水位线了

你可能感兴趣的:(Flink,flink)