flink中的事件时间和水印

引言
在Flink流式引擎消费平台的项目中遇到数据处理顺序错乱的问题,导致项目处于一个不可用状态。本文记录了流式引擎中处理乱序方案中的基础知识点。

正文

事件时间(EventTime)和水印(WaterMark)诞生的背景

在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响

比如:某数据源中的某些数据由于某种原因(如:网络原因,外部存储自身原因)会有2秒的延时,也就是在实际时间的第1秒产生的数据有可能在第3秒中产生的数据之后到来。

假设在一个5秒的滚动窗口中,有一个EventTime是9秒的数据,在第11秒时候到来了。

图示:


那么对于一个Count聚合的Tumble(5s)的window,上面的情况如何处理才能window3=3,window2=3 呢?


时间类型

Flink支持不同的时间概念

1. Processing Time(处理时间)
处理时间是指当前机器处理该条事件的时间。
它是当数据流入到具体某个算子时候相应的系统时间。
它提供了最小的延时和最佳的性能。
但是在分布式和异步环境中, 处理时间不能提供确定性。
因为其对事件到达系统的速度和数据流在系统的各个operator之间处理的速度很敏感。
2. Event Time(事件时间)
事件时间是每个事件在其生产设备上发生的时间。
此时间通常在进入Flink之前嵌入到记录中,并且可以从每个记录中提取该事件时间戳。
事件时间对于乱序、延时、或者数据重放等情况,都能给出正确的结果。
事件时间依赖于事件本身,而跟物理时钟没有关系。
基于事件时间的程序必须指定如何生成事件时间水印(watermark),这是指示事件时间进度的机制。
事件时间处理通常存在一定的延时,因此需要为延时和无序的事件等待一段时间。
因此,使用事件时间编程通常需要与处理时间相结合。
3. Ingestion Time(摄入时间)
摄入时间是数据进入Flink框架的时间,是在Source Operator中设置的
与ProcessingTime相比可以提供更可预测的结果,因为摄入时间戳比较稳定(在源处只记录一次)
同一数据在流经不同窗口操作时将使用相同的时间戳
而对于ProcessingTime,同一数据在流经不同窗口算子会有不同的处理时间戳
Process time 与 Event time对比


如上图所示,在一个乱序的数据流里,使用event time类型的事件时间,可以保证数据流的顺序性。

设置时间特性
Flink程序的第一部分工作通常是设置时间特性,该设置用于定义数据源使用什么时间,在时间窗口处理中使用什么时间。

代码:

// 设置执行环境, 类似spark中初始化SparkContext
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();        
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        
//env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
//env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
1
2
3
4
5
6
7

Watermark (水印)

WaterMark 产生背景
流处理从事件产生,到数据流经source,再到operator,中间是有一个过程和时间的。

虽然大部分情况下,数据流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、背压等原因,导致乱序的产生(out-of-order或者说late element)。

但是对于late element(延迟数据),我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。

这个特别的机制,就是watermark。

WaterMark 介绍
Watermark是Flink为了处理EventTime时间类型的窗口计算提出的一种机制, 本质上也是一种时间戳。
Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。
当Operator通过基于Event Time的时间窗口来处理数据时,它必须在确定所有属于该时间窗口的消息全部流入此Operator后,才能开始处理数据。
但是由于消息可能是乱序的,所以Operator无法直接确认何时所有属于该时间窗口的消息全部流入此操作符。
WaterMark包含一个时间戳,Flink使用WaterMark标记所有小于该时间戳的消息都已流入
Flink的数据源在确认所有小于某个时间戳的消息都已输出到Flink流处理系统后,会生成一个包含该时间戳的WaterMark,插入到消息流中,输出到Flink流处理系统中,Flink算子按照时间窗口缓存所有流入的消息。
当Operator处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口的数据进行处理,并发送到下一个Operator节点,然后也将WaterMark发送到下一个Operator节点。


WaterMark 的产生方式
1. Punctuated
数据流中每一个递增的EventTime都会产生一个Watermark。
在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。
2. Periodic
周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。
在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。
代码:

package com.ronnie.flink.stream.test;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
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 javax.annotation.Nullable;
import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 *
 hello,2019-09-17 11:34:05.890
 hello,2019-09-17 11:34:07.890
 hello,2019-09-17 11:34:13.890
 hello,2019-09-17 11:34:08.890
 hello,2019-09-17 11:34:16.890
 hello,2019-09-17 11:34:19.890
 hello,2019-09-17 11:34:21.890
 */
public class WaterMarkTest {
    public static void main(String[] args) {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        env.setParallelism(1);

     // 设置多久查看一下当前的水位线... 默认200ms
        env.getConfig().setAutoWatermarkInterval(10000);

        System.err.println("interval : " + env.getConfig().getAutoWatermarkInterval());

        DataStreamSource<String> streamSource = env.socketTextStream("ronnie01", 9999);

        SingleOutputStreamOperator<String> watermarks = streamSource.assignTimestampsAndWatermarks(new MyWaterMark());

        watermarks.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                String[] split = value.split(",");
                String key = split[0];
                return new Tuple2<String, Integer>(key, 1);
          }
        }).keyBy(0)
          .timeWindow(Time.seconds(10))
          // 自定义的一个计算规则......
          .apply(new MyWindowFunction())
          .printToErr();

        try {
            env.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyWaterMark implements AssignerWithPeriodicWatermarks<String>{

    // 目前系统里所有数据的最大事件时间
    long currentMaxTimeStamp = 0;
    // 允许数据延迟5s
    long maxLateTime = 5000;

    Watermark wm = null;

    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    @Nullable
    @Override
    // 周期性地获取目前的水位线时间, 默认200ms
    public Watermark getCurrentWatermark() {
        // 未处理的延迟/乱序问题
       // wm = new Watermark(currentMaxTimeStamp);

        // 处理数据的延迟/乱序问题
        wm = new Watermark(currentMaxTimeStamp - maxLateTime);
        System.out.println(format.format(System.currentTimeMillis()) + " 获取当前水位线: " + wm + ","+ format.format(wm.getTimestamp()));
        return wm;
    }


    @Override
    public long extractTimestamp(String element, long previousElementTimestamp) {
        String[] split = element.split(",");

        String key = split[0];

        long timestamp = 0;

        try {
            //将2019-09-17 10:24:50.958 格式时间转成时间戳
            timestamp = format.parse(split[1]).getTime();
        } catch (ParseException e) {
            e.printStackTrace();
        }

        // 对比新数据的时间戳和目前最大的时间戳, 取大的值作为新的时间戳
        currentMaxTimeStamp= Math.max(timestamp, currentMaxTimeStamp);

        System.err.println(key +", 本条数据的时间戳: "+ timestamp + "," +format.format(timestamp)
                + "|目前数据中的最大时间戳: "+  currentMaxTimeStamp + ","+ format.format(currentMaxTimeStamp)
                + "|水位线时间戳: "+ wm + ","+ format.format(wm.getTimestamp()));

        return timestamp;
    }
}

class MyWindowFunction implements WindowFunction<Tuple2<String, Integer>, String, Tuple, TimeWindow>{

    @Override
    public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Integer>> input, Collector<String> out) throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        int sum = 0;

        for (Tuple2<String, Integer> tuple2:input){
         sum += tuple2.f1;
        }
        long start = window.getStart();
        long end = window.getEnd();

        out.collect("key:" + tuple.getField(0) + " value: " + sum + "| window_start :"
                + format.format(start) + "  window_end :" + format.format(end)
        );
    }
}
————————————————
版权声明:本文为CSDN博主「Ozan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhuyitao/article/details/108167338

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值