一、关于Flink的Watermark

1.12版本之后默认时间语义为Event time(事件时间),并且实际使用也是以事件时间为主,故这边背景均以基于时间事件的来说明。

支持event time的流式处理框架需要一种能够测量event time 进度的方式;比如, 一个窗口算子创建了一个长度为1小时的窗口,那么这个算子需要知道事件时间已经到达了这个窗口的关闭时间, 从而在程序中去关闭这个窗口。

事件时间可以不依赖处理时间来表示时间的进度.例如,在程序中,即使处理时间和事件时间有相同的速度, 事件时间可能会轻微的落后处理时间.另外一方面,使用事件时间可以在几秒内处理已经缓存在Kafka中多周的数据, 这些数据可以照样被正确处理,就像实时发生的一样能够进入正确的窗口。

这种在Flink中去测量事件时间的进度的机制就是watermark(水印/水位)。watermark作为数据流的一部分在流动, 并且携带一个时间戳t。

Flink内置了两种水印生成器:

  1. Monotonously Increasing Timestamps(时间戳单调增长:其实就是允许的延迟为0),看源码可知该生成器也是继承了允许固定时间延迟的类,只是设置允许延迟为0。
static <T> WatermarkStrategy<T> forMonotonousTimestamps() {
	return (ctx) -> new AscendingTimestampsWatermarks<>();
}

public class AscendingTimestampsWatermarks<T> extends BoundedOutOfOrdernessWatermarks<T> {
	/**
	 * Creates a new watermark generator with for ascending timestamps.
	 */
	public AscendingTimestampsWatermarks() {
		super(Duration.ofMillis(0));
	}
}
  1. Fixed Amount of Lateness(允许固定时间的延迟)
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10));

二、乱序数据中对延迟数据的处理

已经添加了wartemark之后, 仍有数据会迟到怎么办? Flink的窗口, 也允许迟到数据.

当触发了窗口计算后, 会先计算当前的结果, 但是此时并不会关闭窗口.以后每来一条迟到数据, 则触发一次这条数据所在窗口计算(增量计算).

那么什么时候会真正的关闭窗口呢? wartermark 超过了窗口结束时间+等待时间

.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))

注意:允许迟到只能运用在event time上

举例文字说明:

如上图所画,数据流中数字代表事件时间戳,圆圈代表一个事件;一个乱序数据流从7秒开始,代码上设定10秒的滚动窗口,允许延迟时间为7秒,则Flink在对数据处理时的流程如下:

flink数据滞留指标 flink数据延迟_大数据

①7秒的事件时间会进入第一个0-10秒的窗口(window)
②14秒的事件时间会进入第二个10-20秒的窗口(window),第一个窗口并不会关闭
③8秒的事件时间会进入第一个0-10秒的窗口(window)
④17秒的事件时间会进入第二个10-20秒的窗口(window),水印超过窗口结束时间+等待时间(10+7),第一个窗口关闭并计算输出
⑤9秒的事件时间由于第一个窗口已经关闭,按代码设定从侧输出流输出

2.代码实例:

先上结果,运行Flink_Watermark_SideOutput类从hadoop102服务器9999端口接收数据,数据第二列为时间戳字段,7秒的数据进入第一个[0,10)的窗口,后续虽然进来14秒的数据进入第一个窗口,但第一个窗口并不会关闭,接着8秒进来的数据还是能进入了第一个[0,10)的窗口,当17秒的数据进来,触发了watermark的关闭条件,第一窗口关闭,最后一条9秒数据只能从侧输出流输出。

flink数据滞留指标 flink数据延迟_flink_02


Flink_Watermark_SideOutput类代码:

package com.test;

import com.test.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.time.Duration;

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

        SingleOutputStreamOperator<WaterSensor> stream = env
                .socketTextStream("hadoop102", 9999)  // 在socket终端只输入毫秒级别的时间戳
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] datas = value.split(",");
                        return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
                    }
                });
        // 创建水印生产策略
        WatermarkStrategy<WaterSensor> wms = WatermarkStrategy
                .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(0)) // 最大容忍的延迟时间
                .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() { // 指定时间戳
                    @Override
                    public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                        return element.getTs() * 1000;
                    }
                });

        SingleOutputStreamOperator<String> result = stream
                .assignTimestampsAndWatermarks(wms)
                .keyBy(WaterSensor::getId)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .allowedLateness(Time.seconds(7))
                .sideOutputLateData(new OutputTag<WaterSensor>("side_1") {
                }) // 设置侧输出流
                .process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
                    @Override
                    public void process(String key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                        String msg = "当前key: " + key
                                + " 窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ") 一共有 "
                                + elements.spliterator().estimateSize() + "条数据" +
                                "watermark: " + context.currentWatermark();
                        out.collect(context.window().toString());
                        out.collect(msg);
                    }
                });

        result.print();
        result.getSideOutput(new OutputTag<WaterSensor>("side_1"){}).print("side>>>");
        env.execute();
    }}

补充WaterSensor 实体类:

package com.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 水位传感器:用于接收水位数据
 *
 * id:传感器编号
 * ts:时间戳
 * vc:水位
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WaterSensor {
    private String id;
    private Long ts;
    private Integer vc;
}

学习交流,有任何问题还请随时评论指出交流。