一般来说,Flink内置的水位线生成器就可以满足应用需求了。不过有时由于业务逻辑可能非常复杂,这时对水位线生成的逻辑也有更高的要求,开发人员就必须自定义实现水位线策略WatermarkStrategy了。在WatermarkStrategy中,时间戳分配器TimestampAssigner都是大同小异的,指定字段提取时间戳就可以了;而不同策略的关键就在于WatermarkGenerator的实现。整体说来,Flink有两种不同的生成水位线的方式:一种是周期性的(Periodic),另一种是断点式的(Punctuated)。就是WatermarkGenerator接口中的两个方法——onEvent()和onPeriodicEmit(),前者是在每个事件到来时调用,而后者由框架周期性调用。周期性调用的方法中发出水位线,自然就是周期性生成水位线;而在事件触发的方法中发出水位线,自然就是断点式生成了。两种方式的不同就集中体现在这两个方法的实现上;
1、周期性水位线生成器(Periodic Generator)
周期性生成器一般是通过onEvent()观察判断输入的事件,而在onPeriodicEmit()里发出水位线
public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); //生成周期性水位线 env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(new MyWatermarkStrategy()) .print(); env.execute(); } //自定义周期性生成水位线 public static class MyWatermarkStrategy 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 * 1000L; } }; } @Override public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) { //周期生成水位线 return new MyPeriodicGenerator(); } } //周期性生成水位线 public static class MyPeriodicGenerator implements WatermarkGenerator<Event> { //延迟时间 private long delayTime = 5000L; //观察到最大时间cuo private long maxTs = Long.MIN_VALUE + delayTime + 1L; //每来一条数据调研一次 @Override public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) { //更新最大时间cuo maxTs = Math.max(event.timestamp, maxTs); } @Override public void onPeriodicEmit(WatermarkOutput output) { //发射水位线,默认200 ms 调用一次 output.emitWatermark(new Watermark(maxTs - delayTime - 1L)); } }
在onPeriodicEmit()里调用output.emitWatermark(),就可以发出水位线了;这个方法由系统框架周期性地调用,默认200ms一次。所以水位线的时间戳是依赖当前已有数据的最大时间戳的(这里的实现与内置生成器类似,也是减去延迟时间再减1),但具体什么时候生成与数据无关。
2、断点式水位线生成器(PunctuatedGenerator)
public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); //生成周期性水位线 env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(new MyWatermarkStrategy()) .print(); env.execute(); } //自定义周期性生成水位线 public static class MyWatermarkStrategy 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 * 1000L; } }; } @Override public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) { //周期生成水位线 // return new MyPeriodicGenerator(); //断点生成水位线 return new MyPunctuatedGenerator(); } } //断点生成水位线 public static class MyPunctuatedGenerator implements WatermarkGenerator<Event> { @Override public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) { //只有遇到特定数据时,才发送水位线 if (event.user.equals("依琳")) { output.emitWatermark(new Watermark(event.timestamp - 1L)); } } @Override public void onPeriodicEmit(WatermarkOutput output) { //onEvent 已经发送了水位线,onPeriodicEmit不做处理即可 } }
在onEvent()中判断当前事件的user字段,只有遇到“依琳”这个特殊的值时,才调用output.emitWatermark()发出水位线。这个过程是完全依靠事件来触发的,所以水位线的生成一定在某个数据到来之后。
3、在自定义数据源中发送水位线
也可以在自定义的数据源中抽取事件时间,然后发送水位线。这里要注意的是,在自定义数据源中发送了水位线以后,就不能再在程序中使用assignTimestampsAndWatermarks方法 来 生 成 水 位 线 了 。 在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用assignTimestampsAndWatermarks方法生成水位线二者只能取其一。示例程序如下:
public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); env.addSource(new ClickSourceWithWatermark()).print(); env.execute(); }
ClickSourceWithWatermark 逻辑
public class ClickSourceWithWatermark implements ParallelSourceFunction<Event> { // 声明标志位 private Boolean running = true; @Override public void run(SourceContext<Event> ctx) throws Exception { //随机生成数据 Random random = new Random(); //随机范围 String[] users = {"令狐冲", "依琳", "任盈盈", "莫大", "风清扬"}; String[] urls = {"./home", "./cat", "./pay", "./info"}; //循环生成数据 while (running) { //生成数据 String user = users[random.nextInt(users.length)]; String url = urls[random.nextInt(urls.length)]; Event event = new Event(user, url, Calendar.getInstance().getTimeInMillis()); //发送数据 ctx.collect(event); //发送水位线 ctx.emitWatermark(new Watermark(event.timestamp - 1L)); } } @Override public void cancel() { running = false; } }
在自定义水位线中生成水位线相比assignTimestampsAndWatermarks方法更加灵活,可以任意的产生周期性的、非周期性的水位线,以及水位线的大小也完全由我们自定义。所以非常适合用来编写Flink的测试程序,测试Flink的各种各样的特性。