JavaFlink原理、实战、源码分析(二)原理部分

目录

JavaFlink原理、实战、源码分析(一)https://blog.csdn.net/qq_36250202/article/details/112978869

JavaFlink原理、实战、源码分析(三)https://blog.csdn.net/qq_36250202/article/details/115732840

第五章 Flink 流处理 API

5.1 Environment

 5.1.2 createLocalEnvironment

5.1.3 createRemoteEnvironment

5.2 Source

5.2.1 从集合读取数据

5.3 Transform

5.3.1

map

5.3.2 KeyBy 

5.3.3 滚动聚合算子(Rolling Aggregation)

 5.3.4Reduce

5.3.5Split 和 Select(Select只能搭配Split使用)

 5.3.6 Connect 和 CoMap(只能是两条流)

5.3.7 Union(联合多条流)

 5.4 支持的数据类型

5.4.1 基础数据类型

5.4.2 Java 和 Scala 元组(Tuples)

 5.4.3 Scala 样例类(case classes)

5.4.4 Java 简单对象(POJOs)  

5.4.5 其它(Arrays, Lists, Maps, Enums, 等等)  

5.5 实现 UDF 函数——更细粒度的控制流

5.5.1 函数类(Function Classes)

5.5.2 匿名函数(Lambda Functions)

5.5.3 富函数(Rich Functions)

5.5.4数据的重分区 

 5.6 Sink  

5.6.1 Kafka 

 5.6.2 Redis(略)

5.6.3 Elasticsearch  (略)

 5.6.4 JDBC 自定义 sink

第六章 Flink 中的 Window

6.1 Window

6.1.1 Window 概述

6.1.2 Window 类型

6.2 Window API 

6.2.1 窗口分配器(window assigner)

6.2.2创建不同类型的窗口

6.2.3 window function

6.2.4 其它可选 API

第七章 时间语义与 Wartermark

7.1 Flink 中的时间语义

 7.2 EventTime 的引入

7.3 Watermark

watermark 的特点 

watermark 的传递

 7.3.2 Watermark 的引入

watermark 的设定

MyAssigner 有两种类型


JavaFlink原理、实战、源码分析(一)JavaFlink原理、实战、源码分析(一)原理部分_任错错的博客-CSDN博客

JavaFlink原理、实战、源码分析(三)JavaFlink原理、实战、源码分析(三)原理部分_任错错的博客-CSDN博客

第五章 Flink 流处理 API

JavaFlink原理、实战、源码分析(二)原理部分_第1张图片

5.1 Environment

创建一个执行环境,表示当前执行程序的上下文。 如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境,也就是说,getExecutionEnvironment 会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();

如果没有设置并行度,会以 flink-conf.yaml 中的配置为准,默认是 1。

 5.1.2 createLocalEnvironment

返回本地执行环境,需要在调用时指定默认的并行度。

LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(1);

5.1.3 createRemoteEnvironment

StreamExecutionEnvironment env = 
StreamExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123,"YOURPATH//WordCount.jar");

5.2 Source

5.2.1 从集合读取数据

 // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 从集合中读取数据
        DataStream dataStream = env.fromCollection(Arrays.asList(
                new SensorReading("sensor_1", 1547718199L, 35.8),
                new SensorReading("sensor_6", 1547718201L, 15.4),
                new SensorReading("sensor_7", 1547718202L, 6.7),
                new SensorReading("sensor_10", 1547718205L, 38.1)
        ));

        DataStream integerDataStream = env.fromElements(1, 2, 4, 67, 189);

        // 打印输出
        dataStream.print("data");
        integerDataStream.print("int");

        // 执行
        env.execute();

5.2.2 从文件读取数据

 public static void main(String[] args) throws Exception {
        ExecutionEnvironment env =ExecutionEnvironment.getExecutionEnvironment();

        // 从文件中读取数据
        String inputPath = "D:\\javaFlink\\src\\main\\resources\\hello.txt";
        DataSet inputDataSet = env.readTextFile(inputPath);
        DataSet> wordCountDataSet = inputDataSet.flatMap(new MyFlatMapper())
                .groupBy(0)
                .sum(1);
        wordCountDataSet.print();
        
    }

5.2.3 kafka 消息队列的数据作为来源

需要引入 kafka 连接器的依赖:

pom.xml


   org.apache.flink
   flink-connector-kafka-0.11_2.12
   1.10.1

具体代码如下:

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

        Properties properties = new Properties();
        //Kafka服务端的主机名和端口号
        properties.setProperty("bootstrap.servers", "locahost:9092");
        // 制定consumer group
        properties.setProperty("group.id", "consumer-group");
        // key序列化
        properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        // value序列化
        properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        //latest 表示从最新的数据消费,也就是新产生的数据.
        properties.setProperty("auto.offset.reset", "latest");

        // 从文件读取数据
        DataStream dataStream = env.addSource( new FlinkKafkaConsumer011("sensor", new SimpleStringSchema(), properties));

        // 打印输出
        dataStream.print();

        env.execute();
    }

 5.2.4 自定义 Source

除了以上的 source 数据来源,我们还可以自定义 source。需要做的,只是传入一个 SourceFunction 就可以。具体调用如下:

DataStream dataStream = env.addSource( new MySensor());

我们希望可以随机生成传感器数据,MySensorSource 具体的代码实现如下:

// 实现自定义的SourceFunction
    public static class MySensorSource implements SourceFunction{
        // 定义一个标识位,用来控制数据的产生
        private boolean running = true;

        @Override
        public void run(SourceContext ctx) throws Exception {
            // 定义一个随机数发生器
            Random random = new Random();

            // 设置10个传感器的初始温度
            HashMap sensorTempMap = new HashMap<>();
            for( int i = 0; i < 10; i++ ){
                sensorTempMap.put("sensor_" + (i+1), 60 + random.nextGaussian() * 20);
            }

            while (running){
                for( String sensorId: sensorTempMap.keySet() ){
                    // 在当前温度基础上随机波动
                    Double newtemp = sensorTempMap.get(sensorId) + random.nextGaussian();
                    sensorTempMap.put(sensorId, newtemp);
                    ctx.collect(new SensorReading(sensorId, System.currentTimeMillis(), newtemp));
                }
                // 控制输出频率
                Thread.sleep(1000L);
            }
        }

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

5.3 Transform

转换算子

5.3.1

map、flatMap、filter基本转换算子(简单算子)

map

JavaFlink原理、实战、源码分析(二)原理部分_第2张图片

 flatMap(打散,来一条数据打散成多条数据)

Filter(过滤)

JavaFlink原理、实战、源码分析(二)原理部分_第3张图片

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 1. map,把String转换成长度输出
        DataStream mapStream = inputStream.map(new MapFunction() {
            @Override
            public Integer map(String value) throws Exception {
                return value.length();
            }
        });

        // 2. flatmap,按逗号分字段
        DataStream flatMapStream = inputStream.flatMap(new FlatMapFunction() {
            @Override
            public void flatMap(String value, Collector out) throws Exception {
                String[] fields = value.split(",");
                for( String field: fields )
                    out.collect(field);
            }
        });

        // 3. filter, 筛选sensor_1开头的id对应的数据
        DataStream filterStream = inputStream.filter(new FilterFunction() {
            @Override
            public boolean filter(String value) throws Exception {
                return value.startsWith("sensor_1");
            }
        });

        // 打印输出
        mapStream.print("map");
        flatMapStream.print("flatMap");
        filterStream.print("filter");

        env.execute();
    }

打印输出

JavaFlink原理、实战、源码分析(二)原理部分_第4张图片

5.3.2 KeyBy 

JavaFlink原理、实战、源码分析(二)原理部分_第5张图片

DataStream KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同 key 的元素,在内部以 hash 的形式实现的。(hashCode重分区)

所有相同的key一定会分到对应的分区里

5.3.3 滚动聚合算子(Rolling Aggregation

这些算子可以针对 KeyedStream 的每一个支流做聚合。

⚫ sum()

⚫ min()

⚫ max()

⚫ minBy()

⚫ maxBy()

注:max、min只会变化当前比较字段值,其他字段值没有变化不是最新对应的,minby、maxby则都会获取比较最新值的对应字段值

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading类型
//        DataStream dataStream = inputStream.map(new MapFunction() {
//            @Override
//            public SensorReading map(String value) throws Exception {
//                String[] fields = value.split(",");
//                return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
//            }
//        });

        DataStream dataStream = inputStream.map( line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        } );

        // 分组
        KeyedStream keyedStream = dataStream.keyBy("id");
        KeyedStream keyedStream1 = dataStream.keyBy(data -> data.getId());

        // KeyedStream keyedStream1 = dataStream.keyBy(SensorReading::getId);

        // 滚动聚合,取当前最大的温度值
        DataStream resultStreamMax = keyedStream.max("temperature");
        DataStream resultStreamMaxBy = keyedStream.maxBy("temperature");

        resultStreamMax.print("resultMax");
        resultStreamMaxBy.print("resultMaxBy");
        env.execute();
    }

控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第6张图片

 5.3.4Reduce

KeyedStream DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。

​ Reduce适用于更加一般化的聚合操作场景。复杂场景,除了获取最大温度的整个传感器信息以外,还要求时间戳更新成最新的

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 分组
        KeyedStream keyedStream = dataStream.keyBy("id");

        // reduce聚合,取最大的温度值,以及当前最新的时间戳
        SingleOutputStreamOperator resultStream = keyedStream.reduce(new ReduceFunction() {
            @Override
            public SensorReading reduce(SensorReading value1, SensorReading value2) throws Exception {
                return new SensorReading(value1.getId(), value2.getTimestamp(), Math.max(value1.getTemperature(), value2.getTemperature()));
            }
        });

        keyedStream.reduce( (curState, newData) -> {
            return new SensorReading(curState.getId(), newData.getTimestamp(), Math.max(curState.getTemperature(), newData.getTemperature()));
        });

        resultStream.print();
        env.execute();
    }

txt

sensor_1,1547718199,35.8
sensor_6,1547718201,15.4
sensor_7,1547718202,6.7
sensor_10,1547718205,38.1
sensor_1,1547718207,36.3
sensor_1,1547718209,32.8
sensor_1,1547718211,37.2
sensor_1,1547718212,37.1

控制台打印输出

JavaFlink原理、实战、源码分析(二)原理部分_第7张图片

5.3.5Split Select(Select只能搭配Split使用)

Split

JavaFlink原理、实战、源码分析(二)原理部分_第8张图片

DataStream SplitStream:根据某些特征把一个 DataStream 拆分成两个或者多个 DataStream。

Select

JavaFlink原理、实战、源码分析(二)原理部分_第9张图片

SplitStreamDataStream:从一个 SplitStream 中获取一个或者多个DataStream。

需求:传感器数据按照温度高低(以 30 度为界),拆分成两个流。

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        } );

        // 1. 分流,按照温度值30度为界分为两条流
        SplitStream splitStream = dataStream.split(new OutputSelector() {
            @Override
            public Iterable select(SensorReading value) {
                //Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错
                return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
                //Lists.newArrayList()其实和new ArrayList()几乎一模一样, 唯一它帮你做的(其实是javac帮你做的), 就是自动推导(不是"倒")尖括号里的数据类型.
                //return (value.getTemperature() > 30) ? Lists.newArrayList("high"):Lists.newArrayList("low");
            }
        });

        DataStream highTempStream = splitStream.select("high");
        DataStream lowTempStream = splitStream.select("low");
        DataStream allTempStream = splitStream.select("high", "low");

        highTempStream.print("high");
        lowTempStream.print("low");
        allTempStream.print("all");

        env.execute();
    }

控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第10张图片

 5.3.6 Connect CoMap(只能是两条流)

JavaFlink原理、实战、源码分析(二)原理部分_第11张图片

DataStream,DataStream ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。

 CoMap,CoFlatMap

JavaFlink原理、实战、源码分析(二)原理部分_第12张图片

ConnectedStreams → DataStream:作用于 ConnectedStreams 上,功能与 map和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap处理。

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        } );

        // 1. 分流,按照温度值30度为界分为两条流
        SplitStream splitStream = dataStream.split(new OutputSelector() {
            @Override
            public Iterable select(SensorReading value) {
                //Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错
                return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
                //Lists.newArrayList()其实和new ArrayList()几乎一模一样, 唯一它帮你做的(其实是javac帮你做的), 就是自动推导(不是"倒")尖括号里的数据类型.
                //return (value.getTemperature() > 30) ? Lists.newArrayList("high"):Lists.newArrayList("low");
            }
        });

        DataStream highTempStream = splitStream.select("high");
        DataStream lowTempStream = splitStream.select("low");
        DataStream allTempStream = splitStream.select("high", "low");

        // 2. 合流 connect,将高温流转换成二元组类型,与低温流连接合并之后,输出状态信息
        DataStream> warningStream = highTempStream.map(new MapFunction>() {
            @Override
            public Tuple2 map(SensorReading value) throws Exception {
                return new Tuple2<>(value.getId(), value.getTemperature());
            }
        });

        ConnectedStreams, SensorReading> connectedStreams = warningStream.connect(lowTempStream);

        DataStream resultStream = connectedStreams.map(new CoMapFunction, SensorReading, Object>() {
            @Override
            public Object map1(Tuple2 value) throws Exception {
                return new Tuple3<>(value.f0, value.f1, "high temp warning");
            }

            @Override
            public Object map2(SensorReading value) throws Exception {
                return new Tuple2<>(value.getId(), "normal");
            }
        });

        resultStream.print();

        env.execute();
    } 
  

控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第13张图片

5.3.7 Union(联合多条流

JavaFlink原理、实战、源码分析(二)原理部分_第14张图片

  

DataStream DataStream:对两个或者两个以上的 DataStream 进行 union 操作,产生一个包含所有 DataStream 元素的新 DataStream。

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        } );

        // 1. 分流,按照温度值30度为界分为两条流
        SplitStream splitStream = dataStream.split(new OutputSelector() {
            @Override
            public Iterable select(SensorReading value) {
                //Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错
                return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
                //Lists.newArrayList()其实和new ArrayList()几乎一模一样, 唯一它帮你做的(其实是javac帮你做的), 就是自动推导(不是"倒")尖括号里的数据类型.
                //return (value.getTemperature() > 30) ? Lists.newArrayList("high"):Lists.newArrayList("low");
            }
        });

        DataStream highTempStream = splitStream.select("high");
        DataStream lowTempStream = splitStream.select("low");
        DataStream allTempStream = splitStream.select("high", "low");

        // 3. union联合多条流
//        warningStream.union(lowTempStream);
        DataStream unionAll = highTempStream.union(lowTempStream, allTempStream);
        unionAll.print("unionAll");
        env.execute();
    }

控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第15张图片

Connect Union 区别:

1. Union 之前两个流的类型必须是一样,Connect 可以不一样,在之后的 coMap中再去调整成为一样的。

2. Connect 只能操作两个流,Union 可以操作多个。

 5.4 支持的数据类型

        Flink 流应用程序处理的是以数据对象表示的事件流。所以在 Flink 内部,我们需要能够处理这些对象。它们需要被序列化和反序列化,以便通过网络传送它们;或者从状态后端、检查点和保存点读取它们。为了有效地做到这一点,Flink 需要明确知道应用程序所处理的数据类型。Flink 使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

        Flink 还具有一个类型提取系统,该系统分析函数的输入和返回类型,以自动获取类型信息,从而获得序列化器和反序列化器。但是,在某些情况下,例如 lambda函数或泛型类型,需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。

        Flink 支持 Java 和 Scala 中所有常见数据类型。使用最广泛的类型有以下几种。

5.4.1 基础数据类型

Flink 支持所有的 Java 和 Scala 基础数据类型,Int, Double, Long, String, …

DataStream numberStream = env.fromElements(1, 2, 3, 4);
numberStream.map(data -> data * 2);

5.4.2 Java Scala 元组(Tuples

DataStream> personStream = env.fromElements(
 new Tuple2("Adam", 17),
 new Tuple2("Sarah", 23) );
personStream.filter(p -> p.f1 > 18);

 5.4.3 Scala 样例类(case classes

case class Person(name: String, age: Int)
val persons: DataStream[Person] = env.fromElements(
Person("Adam", 17),
Person("Sarah", 23) )
persons.filter(p => p.age > 18)

5.4.4 Java 简单对象(POJOs 

public class Person {
public String name;
public int age;
 public Person() {}
 public Person(String name, int age) { 
this.name = name; 
this.age = age; 
} }
DataStream persons = env.fromElements( 
new Person("Alex", 42), 
new Person("Wendy", 23));

5.4.5 其它(Arrays, Lists, Maps, Enums, 等等)  

Flink 对 Java 和 Scala 中的一些特殊目的的类型也都是支持的,比如 Java 的ArrayList,HashMap,Enum 等等。

5.5 实现 UDF 函数——更细粒度的控制流

5.5.1 函数类(Function Classes

Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。例如MapFunction, FilterFunction, ProcessFunction 等等。

下面例子实现了 FilterFunction 接口:

DataStream flinkTweets = tweets.filter(new FlinkFilter());
public static class FlinkFilter implements FilterFunction {
 @Override
 public boolean filter(String value) throws Exception {
 return value.contains("flink");
}
}

还可以将函数实现成匿名类

DataStream flinkTweets = tweets.filter(new FilterFunction() {
 @Override
 public boolean filter(String value) throws Exception {
 return value.contains("flink");
 }
});

我们 filter 的字符串"flink"还可以当作参数传进去。

DataStream tweets = env.readTextFile("INPUT_FILE ");
DataStream flinkTweets = tweets.filter(new KeyWordFilter("flink"));
public static class KeyWordFilter implements FilterFunction {
 private String keyWord;
 KeyWordFilter(String keyWord) { this.keyWord = keyWord; }
 @Override
 public boolean filter(String value) throws Exception {
 return value.contains(this.keyWord);
 } }

5.5.2 匿名函数(Lambda Functions

DataStream tweets = env.readTextFile("INPUT_FILE");
DataStream flinkTweets = tweets.filter( tweet -> tweet.contains("flink") );

5.5.3 富函数(Rich Functions

        “富函数”是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都有其 Rich 版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

⚫ RichMapFunction

⚫ RichFlatMapFunction

⚫ RichFilterFunction

⚫ …

Rich Function 有一个生命周期的概念。典型的生命周期方法有:

⚫ open()方法是 rich function 的初始化方法,当一个算子例如 map 或者 filter被调用之前 open()会被调用。

⚫ close()方法是生命周期中的最后一个调用的方法,做一些清理工作。

⚫ getRuntimeContext()方法提供了函数的 RuntimeContext 的一些信息,例如函数执行的并行度,任务的名字,以及 state 状态

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        DataStream> resultStream = dataStream.map( new MyMapper() );

        resultStream.print();

        env.execute();
    }

    public static class MyMapper0 implements MapFunction>{
        @Override
        public Tuple2 map(SensorReading value) throws Exception {
            return new Tuple2<>(value.getId(), value.getId().length());
        }
    }

    // 实现自定义富函数类
    public static class MyMapper extends RichMapFunction>{
        @Override
        public Tuple2 map(SensorReading value) throws Exception {
//            getRuntimeContext().getState();
            return new Tuple2<>(value.getId(), getRuntimeContext().getIndexOfThisSubtask());
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            // 初始化工作,一般是定义状态,或者建立数据库连接
            System.out.println("open");
        }

        @Override
        public void close() throws Exception {
            // 一般是关闭连接和清空状态的收尾操作
            System.out.println("close");
        }
    }

控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第16张图片

5.5.4数据的重分区 

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

        // 从文件读取数据
        DataStream inputStream = env.readTextFile("E:\\尚硅谷\\Flink课程\\尚硅谷大数据技术之Flink(Java版)\\4.代码\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        dataStream.print("input");

        // 1. shuffle随机打散,轮询的分配到分区中
        DataStream shuffleStream = inputStream.shuffle();

        shuffleStream.print("shuffle");

        // 2. keyBy根据hashCode重分区,把相同的key分到一个分区中,但是一个分区不一定只会有一个key的数据

        dataStream.keyBy("id").print("keyBy");

        // 3. global把所有的分区全部发送到下游的第一个分区
        dataStream.global().print("global");

        env.execute();
    }

 控制台打印

JavaFlink原理、实战、源码分析(二)原理部分_第17张图片

 5.6 Sink  

          Flink 没有类似于 spark 中 foreach 方法,让用户进行迭代的操作。虽有对外的输出操作都要利用 Sink 完成。最后通过类似如下方式完成整个任务最终输出操作。

stream.addSink(new MySink(xxxx))

 官方提供了一部分的框架的 sink。除此以外,需要用户自定义实现 sink。

JavaFlink原理、实战、源码分析(二)原理部分_第18张图片

 JavaFlink原理、实战、源码分析(二)原理部分_第19张图片

5.6.1 Kafka 

pom.xml


 org.apache.flink
 flink-connector-kafka-0.11_2.12
 1.10.1

主函数中添加 sink:

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

//        // 从文件读取数据
//        DataStream inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");

        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "localhost:9092");
        properties.setProperty("group.id", "consumer-group");
        properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("auto.offset.reset", "latest");

        // 从文件读取数据
        DataStream inputStream = env.addSource( new FlinkKafkaConsumer011("sensor", new SimpleStringSchema(), properties));

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2])).toString();
        });

        dataStream.addSink( new FlinkKafkaProducer011("localhost:9092", "sinktest", new SimpleStringSchema()));

        env.execute();
    }

启动kafka

JavaFlink原理、实战、源码分析(二)原理部分_第20张图片

启动producer

 JavaFlink原理、实战、源码分析(二)原理部分_第21张图片

 启动idea

启动consumer

 5.6.2 Redis(略)

pom.xml


 org.apache.bahir
 flink-connector-redis_2.11
 1.0

5.6.3 Elasticsearch  (略)


 org.apache.flink
 flink-connector-elasticsearch6_2.12
 1.10.1

 5.6.4 JDBC 自定义 sink

pom.xml


 mysql
 mysql-connector-java
 5.1.44
   public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStream dataStream = env.addSource(new SourceTest4_UDF.MySensorSource());
//在 main 方法中增加,把明细保存到 mysql 中
        dataStream.addSink(new MyJdbcSink());

        env.execute();
    }

    // 实现自定义的SinkFunction
    public static class MyJdbcSink extends RichSinkFunction {
        // 声明连接和预编译语句
        Connection connection = null;
        PreparedStatement insertStmt = null;
        PreparedStatement updateStmt = null;

        @Override
        public void open(Configuration parameters) throws Exception {
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
            insertStmt = connection.prepareStatement("insert into sensor_temp (id, temp) values (?, ?)");
            updateStmt = connection.prepareStatement("update sensor_temp set temp = ? where id = ?");
        }

        // 每来一条数据,调用连接,执行sql
        @Override
        public void invoke(SensorReading value, Context context) throws Exception {
            // 直接执行更新语句,如果没有更新那么就插入
            updateStmt.setDouble(1, value.getTemperature());
            updateStmt.setString(2, value.getId());
            updateStmt.execute();
            if( updateStmt.getUpdateCount() == 0 ){
                insertStmt.setString(1, value.getId());
                insertStmt.setDouble(2, value.getTemperature());
                insertStmt.execute();
            }
        }

        @Override
        public void close() throws Exception {
            insertStmt.close();
            updateStmt.close();
            connection.close();
        }
    }

 // 实现自定义的SourceFunction
    public static class MySensorSource implements SourceFunction{
        // 定义一个标识位,用来控制数据的产生
        private boolean running = true;

        @Override
        public void run(SourceContext ctx) throws Exception {
            // 定义一个随机数发生器
            Random random = new Random();

            // 设置10个传感器的初始温度
            HashMap sensorTempMap = new HashMap<>();
            for( int i = 0; i < 10; i++ ){
                sensorTempMap.put("sensor_" + (i+1), 60 + random.nextGaussian() * 20);
            }

            while (running){
                for( String sensorId: sensorTempMap.keySet() ){
                    // 在当前温度基础上随机波动
                    Double newtemp = sensorTempMap.get(sensorId) + random.nextGaussian();
                    sensorTempMap.put(sensorId, newtemp);
                    ctx.collect(new SensorReading(sensorId, System.currentTimeMillis(), newtemp));
                }
                // 控制输出频率
                Thread.sleep(1000L);
            }
        }

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

启动查询mysql表

JavaFlink原理、实战、源码分析(二)原理部分_第22张图片

第六章 Flink 中的 Window

主要内容

• window 概念

• window 类型

• window API

6.1 Window

6.1.1 Window 概述

streaming 流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而 window 是一种切割无限数据为有限块进行处理的手段。

Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大 小的”buckets”桶,我们可以在这些桶上做计算操作。

JavaFlink原理、实战、源码分析(二)原理部分_第23张图片

• 一般真实的流都是无界的,怎样处理无界的数据?

• 可以把无限的数据流进行切分,得到有限的数据集进行处理 —— 也就是得到有界流

• 窗口(window)就是将无限流切割为有限流的一种方式,它会将流数据分发到有限大小的桶(bucket)中进行分析

6.1.2 Window 类型

Window 可以分成两类:

• 时间窗口(Time Window):按照时间生成 Window。

➢ 滚动时间窗口

➢ 滑动时间窗口

➢ 会话窗口

• 计数窗口(Count Window):按照指定的数据条数生成一个 Window,与时间无关。

➢ 滚动计数窗口

➢ 滑动计数窗口

1. 滚动窗口(Tumbling Windows)

        滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个 5 分钟大小的滚动窗 口,窗口的创建如下图所示:

JavaFlink原理、实战、源码分析(二)原理部分_第24张图片

将数据依据固定的窗口长度对数据进行切片。

特点:时间对齐,窗口长度固定,没有重叠。

适用场景:适合做 BI 统计等(做每个时间段的聚合计算)。

2. 滑动窗口(Sliding Windows)

滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动 间隔组成。

特点:时间对齐,窗口长度固定,可以有重叠。

        滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。

例如,你有 10 分钟的窗口和 5 分钟的滑动,那么每个窗口中 5 分钟的窗口里包含着上个 10 分钟产生的数据,如下图所示:

JavaFlink原理、实战、源码分析(二)原理部分_第25张图片

适用场景:对最近一个时间段内的统计(求某接口最近 5min 的失败率来决定是否要报警)。

 3. 会话窗口(Session Windows

由一系列事件组合一个指定时间长度的 timeout 间隙组成,类似于 web 应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。

特点:时间无对齐。

        session 窗口分配器通过 session 活动来对元素进行分组,session 窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个 session 窗口通过一个 session 间隔来配置,这个 session 间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的 session 将关闭并且后续的元素将被分配到新的 session 窗口中去。

JavaFlink原理、实战、源码分析(二)原理部分_第26张图片

6.2 Window API 

• 窗口分配器 —— window() 方法

➢ 我们可以用 .window() 来定义一个窗口,然后基于这个 window 去做一些聚 合或者其它处理操作。

    注意: window () 方法必须在 keyBy 之后才能用。

➢ Flink 提供了更加简单的 .timeWindow 和 .countWindow 方法,用于定义时间窗口和计数窗口。

6.2.1 窗口分配器(window assigner)

• window() 方法接收的输入参数是一个 WindowAssigner

• WindowAssigner 负责将每条输入的数据分发到正确的 window 中

• Flink 提供了通用的 WindowAssigner

➢ 滚动窗口(tumbling window)

➢ 滑动窗口(sliding window)

➢ 会话窗口(session window)

➢ 全局窗口(global window)

6.2.2创建不同类型的窗口

6.2.2.1 TimeWindow

      TimeWindow 是将指定时间范围内的所有数据组成一个 window,一次对一个window 里面的所有数据进行计算。

1. 滚动时间窗口 (tumbling time window)

       Flink 默认的时间窗口根据 Processing Time 进行窗口的划分,将 Flink 获取到的数据根据进入 Flink 的时间划分到不同的窗口中。

.timeWindow( Time.seconds(15) )

时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

2. 滑动窗口(SlidingEventTimeWindows)

        滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是 window_size,一个是 sliding_size。

        下面代码中的 sliding_size 设置为了 5s,也就是说,每 5s 就计算输出结果一次,每一次计算的 window 范围是 15s 内的所有元素。

.timeWindow( Time.seconds(15), Time.seconds(5) )

时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

6.2.2.2 CountWindow 

CountWindow 根据窗口中相同 key 元素的数量来触发执行,执行时只计算元素数量达到窗口大小的 key 对应的结果。

注意:CountWindow 的 window_size 指的是相同 Key 的元素的个数,不是输入的所有元素的总数。已验证

1 滚动计数窗口 (tumbling count window)

默认的 CountWindow 是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。

.countWindow( 5 )

2 滑动计数窗口(sliding count window)

下面代码中的 sliding_size 设置为了 2,也就是说,每收到两个相同 key 的数据就计算一次,每一次计算的 window 范围是 10 个元素。

.countWindow( 10, 2 )

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


        // socket文本流
        DataStream inputStream = env.socketTextStream("localhost", 7777);

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 开计数窗口测试
        SingleOutputStreamOperator avgTempResultStream = dataStream.keyBy("id")
                .countWindow(10, 2)
                .aggregate(new MyAvgTemp());

        avgTempResultStream.print();

        env.execute();
    }

    public static class MyAvgTemp implements AggregateFunction, Double>{
        @Override
        public Tuple2 createAccumulator() {
            return new Tuple2<>(0.0, 0);
        }

        @Override
        public Tuple2 add(SensorReading value, Tuple2 accumulator) {
            return new Tuple2<>(accumulator.f0 + value.getTemperature(), accumulator.f1 + 1);
        }

        @Override
        public Double getResult(Tuple2 accumulator) {
            return accumulator.f0 / accumulator.f1;
        }

        @Override
        public Tuple2 merge(Tuple2 a, Tuple2 b) {
            return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
        }
    }

6.2.2.3SessionWindow

 .window(EventTimeSessionWindows.withGap(Time.minutes(1)));

6.2.3 window function

window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两类:

⚫ 增量聚合函数(incremental aggregation functions)

每条数据到来就进行计算,保持一个简单的状态。典型的增量聚合函数有ReduceFunction, AggregateFunction。

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

        // socket文本流
        DataStream inputStream = env.socketTextStream("locahost", 7777);

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 开窗测试

        // 1. 增量聚合函数
        DataStream resultStream = dataStream.keyBy("id")
//                .countWindow(10, 2);
//                .window(EventTimeSessionWindows.withGap(Time.minutes(1)));
//                .window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
                .timeWindow(Time.seconds(15))
                .aggregate(new AggregateFunction() {
                    @Override
                    public Integer createAccumulator() {
                        return 0;
                    }

                    @Override
                    public Integer add(SensorReading value, Integer accumulator) {
                        return accumulator + 1;
                    }

                    @Override
                    public Integer getResult(Integer accumulator) {
                        return accumulator;
                    }

                    @Override
                    public Integer merge(Integer a, Integer b) {
                        return a + b;
                    }
                });

        resultStream.print();


        env.execute();
    }

控制台输出(每隔15输出一次聚合结果)

⚫ 全窗口函数(full window functions)

先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。ProcessWindowFunction 就是一个全窗口函数。

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

        // socket文本流
        DataStream inputStream = env.socketTextStream("localhost", 7777);

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 开窗测试
        // 2. 全窗口函数
        SingleOutputStreamOperator> resultStream2 = dataStream.keyBy("id")
                .timeWindow(Time.seconds(15))
//                .process(new ProcessWindowFunction() {
//                })
                .apply(new WindowFunction, Tuple, TimeWindow>() {
                    @Override
                    public void apply(Tuple tuple, TimeWindow window, Iterable input, Collector> out) throws Exception {
                        String id = tuple.getField(0);
                        Long windowEnd = window.getEnd();
                        Integer count = IteratorUtils.toList(input.iterator()).size();
                        out.collect(new Tuple3<>(id, windowEnd, count));
                    }
                });

        resultStream2.print();


        env.execute();
    }

6.2.4 其它可选 API

⚫ .trigger() —— 触发器

定义 window 什么时候关闭,触发计算并输出结果

⚫ .evitor() —— 移除器

定义移除某些数据的逻辑

⚫ .allowedLateness() —— 允许处理迟到的数据

⚫ .sideOutputLateData() —— 将迟到的数据放入侧输出流

⚫ .getSideOutput() —— 获取侧输出流

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

        // socket文本流
        DataStream inputStream = env.socketTextStream("localhost", 7777);

        // 转换成SensorReading类型
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 开窗测试
   

        // 3. 其它可选API
//实例化
        OutputTag outputTag = new OutputTag("late") {
        };

        SingleOutputStreamOperator sumStream = dataStream.keyBy("id")
                .timeWindow(Time.seconds(15))
//                .trigger()
//                .evictor()
//允许处理迟到的数据
                .allowedLateness(Time.minutes(1))
//将迟到的数据放入侧输出流
                .sideOutputLateData(outputTag)
                .sum("temperature");
//获取侧输出流
        sumStream.getSideOutput(outputTag).print("late");

        resultStream2.print();

        env.execute();
    }

JavaFlink原理、实战、源码分析(二)原理部分_第27张图片

第七章 时间语义与 Wartermark

7.1 Flink 中的时间语义

在 Flink 的流式处理中,会涉及到时间的不同概念,如下图所示:

JavaFlink原理、实战、源码分析(二)原理部分_第28张图片

Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。

Ingestion Time:是数据进入 Flink 的时间。

Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是 Processing Time。

一个例子——电影《星球大战》:

JavaFlink原理、实战、源码分析(二)原理部分_第29张图片

• 不同的时间语义有不同的应用场合

• 我们往往更关心事件时间(Event Time)

JavaFlink原理、实战、源码分析(二)原理部分_第30张图片

• 某些应用场合,不应该使用 Processing Time

• Event Time 可以从日志数据的时间戳(timestamp)中提取

➢ 2017-11-02 18:37:15.624 INFO Fail over to rm

例如,一条日志进入 Flink 的时间为 2017-11-12 10:00:00.123,到达 Window 的系统时间为 2017-11-12 10:00:01.234,日志的内容如下:

2017-11-02 18:37:15.624 INFO Fail over to rm2

对于业务来说,要统计 1min 内的故障日志个数,哪个时间是最有意义的?—— eventTime,因为我们要根据日志的生成时间进行统计。

 7.2 EventTime 的引入

Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所示:

• 我们可以直接在代码中,对执行环境调用 setStreamTimeCharacteristic方法,设置流的时间特性

• 具体的时间,还需要从数据中提取时间戳(timestamp)

StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

7.3 Watermark

        我们知道,流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的,虽然大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。

JavaFlink原理、实战、源码分析(二)原理部分_第31张图片

⚫ Watermark 是一种衡量 Event Time 进展的机制。

Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现。

⚫ 数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的。

⚫watermark 用来让程序自己平衡延迟和结果正确性

⚫ Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。

有序流的 Watermarker 如下图所示:(Watermark 设置为 0)

JavaFlink原理、实战、源码分析(二)原理部分_第32张图片

乱序流的 Watermarker 如下图所示:(Watermark 设置为 2) 

JavaFlink原理、实战、源码分析(二)原理部分_第33张图片

        当 Flink 接收到数据时,会按照一定的规则去生成 Watermark,这条 Watermark就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是基于数据携带的时间戳生成的,一旦 Watermark 比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。由于 event time 是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发。

       上图中,我们设置的允许最大延迟到达时间为 2s,所以时间戳为 7s 的事件对应的 Watermark 是 5s,时间戳为 12s 的事件的 Watermark 是 10s,如果我们的窗口 1是 1s~5s,窗口 2 是 6s~10s,那么时间戳为 7s 的事件到达时的 Watermarker 恰好触发窗口 1,时间戳为 12s 的事件到达时的 Watermark 恰好触发窗口 2。 Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。

watermark 的特点 

JavaFlink原理、实战、源码分析(二)原理部分_第34张图片

• watermark 是一条特殊的数据记录

• watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退

• watermark 与数据的时间戳相关

watermark 的传递

我们知道水位线是数据流中插入的一个标记,用来表示事件时间的进展,它会随着数据一起在任务间传递。如果只是直通式(forward)的传输,那很简单,数据和水位线都是按照本身的顺序依次传递、依次处理的;一旦水位线到达了算子任务, 那么这个任务就会将它内部的时钟设为这个水位线的时间戳。

在这里,“任务的时钟”其实仍然是各自为政的,并没有统一的时钟。实际应用中往往上下游都有多个并行子任务,为了统一推进事件时间的进展,我们要求上游任务处理完水位线、时钟改变之后,要把当前的水位线再次发出,广播给所有的下游子任务。这样,后续任务就不需要依赖原始数据中的时间戳(经过转化处理后,数据可能已经改变了),也可以知道当前事件时间了。

可是还有另外一个问题,那就是在“重分区”(redistributing)的传输模式下,一个任务有可能会收到来自不同分区上游子任务的数据。而不同分区的子任务时钟并不同步,所以同一时刻发给下游任务的水位线可能并不相同。这时下游任务又该听谁的呢?

这就要回到水位线定义的本质了:它表示的是“当前时间之前的数据,都已经到齐了”。这是一种保证,告诉下游任务“只要你接到这个水位线,就代表之后我不会再给你发更早的数据了,你可以放心做统计计算而不会遗漏数据”。所以如果一个任务收到了来自上游并行任务的不同的水位线,说明上游各个分区处理得有快有慢,进度各不相同比如上游有两个并行子任务都发来了水位线,一个是 5 秒,一个是 7 秒;这代表第一个并行任务已经处理完 5 秒之前的

所有数据,而第二个并行任务处理到了 7 秒。那这时自己的时钟怎么确定呢?当然也要以“这之前的数据全部到齐”为标准。如果我们以较大的水位线 7 秒作为当前时间,那就表示“7 秒前的数据都已经处理完”,这显然不是事实——第一个上游分区才处理到 5 秒,5~7 秒的数据还会不停地发来;而如果以最小的水位线 5 秒作为当前时钟就不会有这个问题了,因为确实所

有上游分区都已经处理完,不会再发 5 秒前的数据了。这让我们想到“木桶原理”:所有的上游并行任务就像围成木桶的一块块木板,它们中最短的那一块,决定了我们桶中的水位。

 

JavaFlink原理、实战、源码分析(二)原理部分_第35张图片

JavaFlink原理、实战、源码分析(二)原理部分_第36张图片 

我们可以用一个具体的例子,将水位线在任务间传递的过程完整梳理一遍。如上图 所示,当前任务的上游,有四个并行子任务,所以会接收到来自四个分区的水位线;而下游有三个并行子任务,所以会向三个分区发出水位线。具体过程如下:

  1. 上游并行子任务发来不同的水位线,当前任务会为每一个分区设置一个“分区水位线”(Partition  Watermark),这是一个分区时钟;而当前任务自己的时钟,就是所有分区时钟里最小的那个。
  2. 当有一个新的水位线(第一分区的 4)从上游传来时,当前任务会首先更新对应的分区时钟;然后再次判断所有分区时钟中的最小值,如果比之前大,说明事件时间有了进展,当前任务的时钟也就可以更新了。这里要注意,更新后的任务时钟,并不一定是新来的那个分区水位线,比如这里改变的是第一分区的时钟,但最小的分区时钟是第三分区的 3,于是当前任务时钟就推进到了 3。当时钟有进展时,当前任务就会将自己的时钟以水位线的形式,广播给下游所有子任务。
  3. 再次收到新的水位线(第二分区的 7)后,执行同样的处理流程。首先将第二个分区时钟更新为 7,然后比较所有分区时钟;发现最小值没有变化,那么当前任务的时钟也不变,也不会向下游任务发出水位线。
  1. 同样道理,当又一次收到新的水位线(第三分区的 6)之后,第三个分区时钟更新为6,同时所有分区时钟最小值变成了第一分区的 4,所以当前任务的时钟推进到 4,并发出时间戳为 4 的水位线,广播到下游各个分区任务。水位线在上下游任务之间的传递,非常巧妙地避免了分布式系统中没有统一时钟的问题, 每个任务都以“处理完之前所有数据”为标准来确定自己的时钟,就可以保证窗口处理的结果总是正确的。对于有多条流合并之后进行处理的场景,水位线传递的规则是类似的。关于 Flink 中的多流转换,我们会在后续章节中介绍。

 注:最小的做全局watermark;分区watermark用最大的递增,如果分区在比较的时候发现最小的已广播则当前分区小的不在重复广播(木桶原理)

 7.3.2 Watermark 的引入

watermark 的引入很简单,对于乱序数据,最常见的引用方式如下:
   public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//        env.setParallelism(1);
        // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.getConfig().setAutoWatermarkInterval(100);

        // socket文本流
        DataStream inputStream = env.socketTextStream("192.168.32.200", 7777);

        // 转换成SensorReading类型,分配时间戳和watermark
        DataStream dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        })
                // 升序数据设置事件时间和watermark
//                .assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
//                    @Override
//                    public long extractAscendingTimestamp(SensorReading element) {
//                        return element.getTimestamp() * 1000L;
//                    }
//                })
                // 乱序数据设置时间戳和watermark
                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor(Time.seconds(2)) {
                    @Override
                    public long extractTimestamp(SensorReading element) {
                        return element.getTimestamp() * 1000L;
                    }
                });

        OutputTag outputTag = new OutputTag("late") {
        };

        // 基于事件时间的开窗聚合,统计15秒内温度的最小值
        SingleOutputStreamOperator minTempStream = dataStream.keyBy("id")
                .timeWindow(Time.seconds(15))
                .allowedLateness(Time.minutes(1))
                .sideOutputLateData(outputTag)
                .minBy("temperature");

        minTempStream.print("minTemp");
        minTempStream.getSideOutput(outputTag).print("late");

        env.execute();
    }

注意:watermark是固定周期性或者是间断式生成的,(数据稠密用固定周期,数据稀疏用间断式

watermark 的设定

在 Flink 中,watermark 由应用程序开发人员生成,这通常需要对相应的领域有一定的了解
如果watermark设置的延迟太久,收到结果的速度可能就会很慢,解决办法是在水位线到达之前输出一个近似结果
而如果watermark到达得太早,则可能收到错误结果,不过 Flink 处理迟到数据的机制可以解决这个问题

MyAssigner 有两种类型

AssignerWithPeriodicWatermarks
AssignerWithPunctuatedWatermarks
以上两个接口都继承自 TimestampAssigner
Assigner with periodic watermarks
        周期性的生成 watermark :系统会周期性的将 watermark 插入到流中 ( 水位线也是一种特殊的事件!) 。默认周期是 200 毫秒。可以使用ExecutionConfig.setAutoWatermarkInterval()方法进行设置。
// 每隔 5 秒产生一个 watermark
env.getConfig.setAutoWatermarkInterval(5000);
        产生 watermark 的逻辑:每隔 5 秒钟, Flink 会调用AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。如果方法返回一个时间戳大于之前水位的时间戳,新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的 watermark。
例子,自定义一个周期性的时间戳抽取:
// 自定义周期性时间戳分配器
public static class MyPeriodicAssigner implements 
AssignerWithPeriodicWatermarks{
 
private Long bound = 60 * 1000L; // 延迟一分钟
 private Long maxTs = Long.MIN_VALUE; // 当前最大时间戳
 @Nullable
 @Override
 public Watermark getCurrentWatermark() {
 return new Watermark(maxTs - bound);
 }
 @Override
 public long extractTimestamp(SensorReading element, long previousElementTimestamp) 
{
 maxTs = Math.max(maxTs, element.getTimestamp());
 return element.getTimestamp();
 } }
Assigner with punctuated watermarks
       间断式地生成 watermark 。和周期性生成的方式不同,这种方式不是固定时间的,而是可以根据需要对每条数据进行筛选和处理。直接上代码来举个例子,我们只给sensor_1 的传感器的数据流插入 watermark
public static class MyPunctuatedAssigner implements 
AssignerWithPunctuatedWatermarks{
 private Long bound = 60 * 1000L; // 延迟一分钟
 @Nullable
 @Override
 public Watermark checkAndGetNextWatermark(SensorReading lastElement, long 
extractedTimestamp) {
 if(lastElement.getId().equals("sensor_1"))
 return new Watermark(extractedTimestamp - bound);
 else
 return null;
 }
 @Override
 public long extractTimestamp(SensorReading element, long previousElementTimestamp) 
{
 return element.getTimestamp();
 } }

 窗口起始点与偏移量

public WindowedStream timeWindow(Time size) {
        return this.environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime ? this.window(TumblingProcessingTimeWindows.of(size)) : this.window(TumblingEventTimeWindows.of(size));
    }

   public Collection assignWindows(Object element, long timestamp, WindowAssignerContext context) {
        if (timestamp > -9223372036854775808L) {
            long start = TimeWindow.getWindowStartWithOffset(timestamp, this.offset, this.size);
            return Collections.singletonList(new TimeWindow(start, start + this.size));
        } else {
            throw new RuntimeException("Record has Long.MIN_VALUE timestamp (= no timestamp marker). Is the time characteristic set to 'ProcessingTime', or did you forget to call 'DataStream.assignTimestampsAndWatermarks(...)'?");
        }
    }

对windowSize取余 ,offset最开始没有设置默认为0,

 public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
        //return timestamp - timestamp% windowSize; 
        return timestamp - (timestamp - offset + windowSize) % windowSize;

    }

你可能感兴趣的:(Flink,大数据,flink,java)