好强大的flink

初识flink

一、认识flink

1、flink简介

  • flink是一个低延迟、高吞吐、统一的大数据计算引擎。

  • flink的计算平台可以实现毫秒级的延迟情况下,每秒钟处理上亿次的消息或者事件。

  • 提供一个Exactly-once的一致性语义,保证了数据的正确性;使得flink大数据引擎可以提供金融级的数据处理能力。

  • flink作为主攻流式计算的大数据引擎,不仅仅是一个高吞吐、低延迟的计算引擎,同时还提供很多高级的功能,例如它提供了有状态的计算,支持状态管理,支持强一致性的数据语义以及支持Event Time,WaterMark对消息乱序的处理。

2、发展历程

  • 诞生于欧洲的一个大数据研究项目StratoSphere,柏林大学的一个研究性项目。

  • flink计算的主流方向被定为Streaming,即用流式计算来做所有大数据的计算。

3、学flink的意义

  • 同时支持流处理和批处理的计算引擎:一个是spark,另一个是flink。

  • spark的技术理念基于批来模拟流的计算。

  • flink基于流计算来模拟批计算。

  • flink区别于起亚的流计算引擎的是statefule,即有状态计算。

  • flink提供了内置的对状态的一致性的处理,即使发生意外,其状态不会丢失、不会被多算少算,同时提供了非常高性能。

  • flink提供了内置的状态管理,可以把这些状态存储在flink内部,降低了计算引擎对外部系统的依赖以及部署使运维更加简单;对性能带来了极大的提升。

  • 同时Flink会定期将这些状态做Checkpoint持久化,把Checkpoint存储到一个分布式的持久化系统中,比如HDFS。

4、性能对比

  • flink的流式计算跟storm性能差不多,支持毫秒级计算,而spark则只能支持秒级计算。

  • spark

    • 以批处理为核心,用微批去模拟流式处理

    • 支持SQL处理,流处理,批处理

    • 对于流处理:因为是微批处理,所以实时性弱,吞吐量高,延迟度高

  • flink

    • 以流式处理为核心,用流处理去模拟批处理

    • 支持流处理,SQL处理,批处理

    • 对于流处理:实时性强,吞吐量高,延迟度低

  • storm

    • 一条一条处理数据,实时性强,吞吐量低,延迟度低

5、使用场景

  • 事件驱动应用

    • 欺诈识别

    • 异常检测

    • 基于规则的警报

    • 业务流程监控

  • 数据分析应用

    • 电信网络的质量监控

    • 分析移动应用程序中的产品更新和实验评估

    • 对消费者技术中的实时数据进行特别分析

    • 大规模图分析

  • 数据管道

    • 电子商务中的实时搜索索引构建

    • 电子商务中持续的ETL

6、flink的编程模型

  • 抽象层次

    • 最低级抽象只提供有状态流,它通过Process Function嵌入到DataStream API中。

    • 在实践中,大多数应用程序不需要上述低级抽象,而是针对DataStream API(有界/无界流)和DataSet API (有界数据集)。

7、DataSet案例

1、批处理

  • scala版

    • def main(args: Array[String]): Unit = {

        //隐式转换
        import org.apache.flink.api.scala._
        //获取文件信息并进行转换
        ExecutionEnvironment.getExecutionEnvironment
          .readTextFile("data/textfile")
          .flatMap({_.split(" ")})
          .map({(_,1)})
          .groupBy(0)
          .sum(1).print()
      }
  • java版

    • public static void main(String[] args) {

            //获取上下文对象
            ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
            //读取文件
            DataSource<String> source = environment.readTextFile("data/textfile");
            //使用flatmap算子
            FlatMapOperator<String, Tuple2<String,Integer>> flatMap = source.flatMap(new FlatMapFunction<String, Tuple2<String,Integer>>() {
                @Override
                public void flatMap(String line, Collector<Tuple2<String,Integer>> collector) throws Exception {
                    for (String words : line.split(" ")) {
                        collector.collect(new Tuple2<>(words, 1));
                    }
                }
            });
            //使用groupby算子
            UnsortedGrouping<Tuple2<String,Integer>> groupBy = flatMap.groupBy(0);
            //使用sum算子
            AggregateOperator<Tuple2<String,Integer>> sum = groupBy.sum(1);
            //打印结果
            try {
                sum.print();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

2、流式处理

  • scala版

    • def main(args: Array[String]): Unit = {

         //引入隐式转换
         import org.apache.flink.api.scala._
         //设置上下文环境
         val env=StreamExecutionEnvironment.getExecutionEnvironment
         //配置主机和端口
         //进行转换计算
         env.socketTextStream("node01",9999)
          .flatMap({_.split(" ")})
          .map((_,1))
          .keyBy(0)
          .sum(1)
          .print()
         env.execute("window WordCount")    
      }
    • 与spark的区别点

      • 不再使用spark的算子reducebykey

      • 运用keyby和sum算子

      • 必须引入隐式转换

      • 流式机必须手动触发,即env.execute("window WordCount")

      • 批处理时运用groupBy算子加上sum算子

  • java版

    • public static void main(String[] args) {
             //获取运行环境
             StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
             //配置端口和主机
             DataStreamSource<String> source = environment.socketTextStream("node01", 9999);
             //使用flatmap算子
             SingleOutputStreamOperator<Tuple2<String, Integer>> flatMap = source.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                 @Override
                 public void flatMap(String line, Collector<Tuple2<String, Integer>> collector) throws Exception {
                     for (String words : line.split(" ")) {
                         collector.collect(new Tuple2<>(words, 1));
                    }
                }
            });
             //使用keyBy算子
             KeyedStream<Tuple2<String, Integer>, Tuple> keyBy = flatMap.keyBy(0);
             //使用sum算子
             SingleOutputStreamOperator<Tuple2<String, Integer>> sum = keyBy.sum(1);
             //打印结果
             sum.print();
             //设置手动触发
             try {
                 environment.execute("window Stream");
            } catch (Exception e) {
                 e.printStackTrace();
            }
        }

3、程序和数据流

  • flink程序的基本构建是在流和转换操作上

  • 执行时flink程序映射到流数据上,由流和转换符组成,每一个数据流都以一个或者多个源开头,并以一个或多个接收器结束。

  • 数据流类似于任意有向无环图。

  • 代码书写流程

    • 创建ExecutionEnvironment/StreamExecutionEnvironment 执行环境对象

    • 通过执行环境对象创建出source(源头)对象

    • 基于source对象做各种转换

    • 定义结果输出位置

    • 最后调用StreamExecutionEnvironment/ExecutionEnvironment 的excute方法,触发执行

  • 注意

    • 每个flink程序由source operator + transformation operator + sink operator组成。

  • flink中的key

    • flink处理数据不是K,V格式编程模型,它是虚拟的key。

    • Flink中Java Api编程中的Tuple需要使用Flink中的Tuple,最多支持25个。

    • 批处理用groupBY 算子,流式处理用keyBy算子。

  • flink指定可以的三种方式

    • 使用Tuple来指定key

      • public static void main(String[] args) {
               StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
               DataStreamSource<String> ds = executionEnvironment.socketTextStream("node01", 9999);

               SingleOutputStreamOperator<Tuple3<String, String, Integer>> map = ds.map(new MapFunction<String, Tuple3<String, String, Integer>>() {
                   @Override
                   public Tuple3<String, String, Integer> map(String line) throws Exception {
                       String[] split = line.split(" ");
                       return new Tuple3<>(split[0], split[1], Integer.parseInt(split[2]));
                  }
              });
               //指定tuple的第一个元素,作为key
        //       KeyedStream<Tuple3<String, String, Integer>, Tuple> keyedStream = map.keyBy(0);
               //指定tuple的第二个元素,作为key
        //       KeyedStream<Tuple3<String, String, Integer>, Tuple> keyedStream = map.keyBy(1);
               //指定tuple的第一个元素和第二个元素,作为key
               KeyedStream<Tuple3<String, String, Integer>, Tuple> keyedStream = map.keyBy(0,1);
               WindowedStream<Tuple3<String, String, Integer>, Tuple, TimeWindow> windowStream = keyedStream.timeWindow(Time.seconds(5));

               SingleOutputStreamOperator<Tuple3<String, String, Integer>> reduce = windowStream.reduce(new ReduceFunction<Tuple3<String, String, Integer>>() {
                   @Override
                   public Tuple3<String, String, Integer> reduce(Tuple3<String, String, Integer> t1, Tuple3<String, String, Integer> t2) throws Exception {
                       return new Tuple3<>(t1.f0 + t2.f0, t1.f1 + t2.f1, t1.f2 + t2.f2);
                  }
              });

               reduce.print();

               try {
                   executionEnvironment.execute("keytest");
              } catch (Exception e) {
                   e.printStackTrace();
              }
        }
    • 使用Field Expression来指定key

      • public static void main(String[] args) {
              StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
              DataStreamSource<String> ds = executionEnvironment.socketTextStream("node01", 9999);
              SingleOutputStreamOperator<StudentInfo> map = ds.map(new MapFunction<String, StudentInfo>() {
                  @Override
                  public StudentInfo map(String line) throws Exception {
                      String[] split = line.split(" ");
                      String name = split[0];
                      String gender = split[1];
                      Integer age = Integer.parseInt(split[2]);
                      Integer grade = Integer.parseInt(split[3]);
                      Float chinese = Float.parseFloat(split[4]);
                      Float math = Float.parseFloat(split[5]);
                      Float english = Float.parseFloat(split[6]);
                      return new StudentInfo(name, gender, age, new Tuple2<>(grade, new Tuple3<>(chinese, math, english)));
                  }
              });
              //使用字段名来指明key
        //       KeyedStream<StudentInfo, Tuple> keyedStream = map.keyBy("gender");
               //使用tuple中的字段来指明key,即指明 年级字段作为key
        //       KeyedStream<StudentInfo, Tuple> keyedStream = map.keyBy("gradeAndScore.f0");
               //指明英语成绩字段作为key
               KeyedStream<StudentInfo, Tuple> keyedStream = map.keyBy("gradeAndScore.f1.f2");
               WindowedStream<StudentInfo, Tuple, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(5));
               SingleOutputStreamOperator<StudentInfo> reduce = timeWindow.reduce(new ReduceFunction<StudentInfo>() {
                   @Override
                   public StudentInfo reduce(StudentInfo t1, StudentInfo t2) throws Exception {
                       String name = t1.getName() + "-" + t2.getName();
                       String gender = t1.getGender() + "-" + t2.getGender();
                       Integer age = t1.getAge() + t2.getAge();

                       Tuple2<Integer, Tuple3<Float, Float, Float>> gs1 = t1.getGradeAndScore();
                       Tuple2<Integer, Tuple3<Float, Float, Float>> gs2 = t2.getGradeAndScore();
                       Tuple2<Integer, Tuple3<Float, Float, Float>> gs = new Tuple2<>(
                               gs1.f0 + gs2.f0,
                               new Tuple3<>(
                                       gs1.f1.f0 + gs2.f1.f0,
                                       gs1.f1.f1 + gs2.f1.f1,
                                       gs1.f1.f2 + gs2.f1.f2
                              )
                      );
                       return new StudentInfo(name, gender, age, gs);
                  }
              });
               reduce.print();
               try {
                   executionEnvironment.execute("keytest");
              } catch (Exception e) {
                   e.printStackTrace();
              }
        }
    • 使用Key Selector Functions来指定key

      • public static void main(String[] args) {
               StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
               DataStreamSource<String> ds = executionEnvironment.socketTextStream("node01", 9999);
               KeyedStream<String, String> keyedStream = ds.keyBy(new KeySelector<String, String>() {
                   @Override
                   public String getKey(String line) throws Exception {
                       return line.split(" ")[1];
                  }
              });
               WindowedStream<String, String, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(5));
               timeWindow.reduce(new ReduceFunction<String>() {
                   @Override
                   public String reduce(String t2, String t1) throws Exception {
                       return t2 + "#" + t1;
                  }
              }).print();
               try {
                   executionEnvironment.execute("keytest");
              } catch (Exception e) {
                   e.printStackTrace();
              }
        }

二、DataStream Operator

1、DataStream Source

  • 基于文件

    • readTextFile(路径)--读取text文件的数据

    • readFile(fileInputFormat,路径)--通过自定义的读取方式,来读取文件的数据。

  • 基于socket

    • socketTextStream 从socket端口中读取数据。

  • 基于集合

    • fromCollection(Collection) - 从collection集合里读取数据,从而形成一个数据流,集合里的元素类型需要一致。

    • fromElements(T ...) - 从数组里读取数据,从而形成一个数据流,集合里的元素类型需要一致。

    • generateSequence(from, to) - 创建一个数据流,数据源里的数据从from到to的数字。

  • 自定义source

    • addSource--自定义一个数据源,比如FlinkKafkaConsumer,从kafka里读数据。

2、DataStream Transformations

  • map

    • 采用一个元素并生成一个元素

  • flatmap

    • 一个元素并生成零个,一个或多个元素

  • filter

    • 过滤函数返回false的数据,true的数据保留

  • keyby

    • 指定key将K,V格式的数据流进行逻辑分区,将相同key的记录分在同一分区里。

  • aggregations

    • 对k,v格式的数据流进行聚合操作。

  • reduce

    • 对k,v的数据进行“减少操作”,这个操作逻辑自己写,加减乘除都行。

3、DataStream Sink

  • writeAsText() - 将计算结果输出成text文件。

  • writeAsCsv(…) - 将计算结果输出成csv文件。

  • print() - 将计算结果打印到控制台。

  • writeUsingOutputFormat() 自定义输出方式。

  • writeToSocket - 将计算结果输出到某台机器的端口上。

三、集群的搭建

1、架构

  • 图解

    • flink运行时包含了两种类型的处理器

      • jobmanager(master):用于协调分布式执行。调度task,协调检查点,协调失败时恢复等等。

      • taskmanager(worker):用于执行一个dataflow的task、数据缓冲和data stream的交换。

      • flink运行时至少存在一个jobmanager。高可用集群中会存在多个jobmanager,分为一个leader和多个standby。

      • flink运行时至少会存在一个taskmanager。

    • taskmanager连接到jobmanager,会告知自身的可用性以及资源等从而获得任务分配。

    • 客户端只用准备并发送dataflow给master,客户端可以连接着或者断开来等待接收计算的结果。

  • 启动jobmanager和taskmanager的方式

    • standlone cluster

    • yarn

    • mesos

    • container

2、搭建模式

  • standalone

    • 配置conf/flink-conf.yaml文件

      • env.java.home:/opt/sxt/jdk1.8.0
        jobmanager.rpc.address: node03
        jobmanager.rpc.port: 6123
        jobmanager.heap.size: 1024m
        taskmanager.heap.size: 1024m
        taskmanager.numberOfTaskSlots: 2
        parallelism.default: 1
    • 配置conf/slaves文件

      • node01
        node02
        node03
    • 启动集群

      • 进入flink目录下的bin中start-cluster.sh

    • 测试

      • 访问webUI界面

      • node03:8081

    • 命令行提交

      • flink run -c mainclass com.shsxt.flink.stream.test.WordCount /root/flink_text.jar

    • webui界面提交

  • yarn

    • 依赖

      • 至少的hadoop2.2以上版本

      • HDFS

    • 配置

      • 检测 YARN_CONF_DIR, HADOOP_CONF_DIR 或 HADOOP_CONF_PATH 环境变量是否设置了(按该顺序检测)。如果它们中有一个被设置了,那么它们就会用来读取配置。

      • 如果上面的策略失败了(如果正确安装了 YARN 的话,就不应该会发生),客户端会使用 HADOOP_HOME 环境变量。如果该变量设置了,客户端会尝试访问 $HADOOP_HOME/etc/hadoop (Hadoop 2) 和 $HADOOP_HOME/conf(Hadoop 1)。

  • flink on yarn两种模式

    • yarn-session模式

      • 预先在yarn上划分一部分资源给flink集群用。flink提交的所有任务共用这些资源。

      • 提交任务

        • 指明分配的资源

          • ./yarn-session.sh -n 3 -jm 1024 -tm 1024

        • 参数说明

          • -n 指明container容器个数,即 taskmanager的进程个数。
            -jm 指明jobmanager进程的内存大小
            -tm 指明每个taskmanager的进程内存大小
        • 在日志上查看jobmanager的地址

        • 提交任务

          • . /flink run -m node06:55695 /opt/sxt/flinkTest.jar

        • 参数解释

          • -c: 后面跟mainclass类,一般只有是jar没指明mainfest的时候用。
            -m:后面跟jobmanager地址。
            -p:任务的并行度,这个参数会覆盖flink-conf.yaml配置文件里的参数 parallelism.default
      • 停止集群

        • 先找ID

        • 执行命令

          • yarn application -kill application_id

    • single job模式

      • 每次提交任务都会创建一个新的flink集群,任务之间相互独立,互不影响,方便管理,任务完成之后,集群也会跟着消失。

      • 提交任务

        • ./flink run -m yarn-cluster -yn 2 /opt/sxt/flinkTest.jar

      • 参数解释

        • -m: 后面跟的是yarn-cluster,不需要指明地址。这是由于Single job模式是每次提交任务会新建flink集群,所以它的jobmanager是不固定的
          -yn: 指明taskmanager个数。
          其余参数可使用:./flink -h 来查看
  • flink on yarn的运行原理

    • 图解

    • 流程

      • 当启动一个新的集群flink yarn client会话,客户端首先会检查所请求的资源是否可用,然后会上传包含了flink配置文件和jar包到HDFS。

      • 客户端请求一个container资源去启动ApplicationMaster进程。

      • resourcemanager选一台NodeManager机器启动ApplicationMaster并注册容器的资源,该NodeManager做初始化工作,完成之后ApplicationMaster就启动了。

      • jobmanager和ApplicationMaster运行在同一个容器中,若成功启动,会为taskmanager生成一个新的flink配置文件,同样会上传到HDFS上。

      • ApplicationMaster开始为flink的taskmanager分配容器,在对应的NodeManager上面启动taskmanager

      • 初始化工作,从HDFS下载jar文件和修改过的配置文件。完成之后,flink就安装完成并准备接收任务了。

四、并行度

  • 并行度设置

    • 算子级别

      • 代码中调用setParallelism()方法来定义

    • 执行环境级别

      • 代码中streamexecutionenvironment调用setParallelism方法

    • 客户端级别

      • 提交任务时设置并行度 参数是 -p

    • 系统级别

      • 通过配置flink_home/conf/flink-conf.yaml中parallelism.default来定义并行度。

五、窗口机制

主要分为三种:翻滚窗口、滑动窗口、会话窗口

flink要操作窗口,先得将streamsource转成windowedstream

Window KeyedStream → WindowedStream可以在已经分区的KeyedStream上定义Windows,即K,V格式的数据。
WindowAll DataStream → AllWindowedStream 对常规的DataStream上定义Window,即非K,V格式的数据
Window Apply WindowedStream → DataStream AllWindowedStream → DataStream 将函数应用于整个窗口中的数据。如下图:
Window Reduce WindowedStream → DataStream 对窗口里的数据进行”reduce”减少聚合统计
Aggregations on windows WindowedStream → DataStream 对窗口里的数据进行聚合操作:windowedStream.sum(0);windowedStream.sum("key");windowedStream.min(0);windowedStream.min("key");windowedStream.max(0);windowedStream.max("key");
  • 翻滚窗口

    • 每一个事件只能属于一个窗口,翻滚窗口具有固定的尺寸,不重叠。

    • 基于时间驱动

      • public static void main(String[] args) {
              StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
              DataStreamSource<String> dataStreamSource = env.socketTextStream("node01", 9999);
              env.setParallelism(1);
              SingleOutputStreamOperator<Tuple2<String, Integer>> map = dataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
                  @Override
                  public Tuple2<String, Integer> map(String s) throws Exception {
                      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                      long timeMillis = System.currentTimeMillis();
                      int nextInt = new Random().nextInt(10);
                      return new Tuple2<>(s, nextInt);
                  }
              });
              KeyedStream<Tuple2<String, Integer>, Tuple> key = map.keyBy(0);
              WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> window = key.timeWindow(Time.seconds(10));
              window.apply(new MyTimeWindowFunction()).print();
              try {
                  env.execute();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
    • 基于事件驱动

      • public static void main(String[] args) {
              //设置执行环境,类似spark中初始化sparkcontext一样
              StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
              env.setParallelism(1);
              DataStreamSource<String> dataStreamSource = env.socketTextStream("node01",9999);
              SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = dataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
                  @Override
                  public Tuple2<String, Integer> map(String value) throws Exception {
                      SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                      long timeMillis = System.currentTimeMillis();
                      int random = new Random().nextInt(10);
                      System.err.println("value : " + value + " random : " + random  + " timestamp : " +  timeMillis + "|" + format.format(timeMillis));

                      return new Tuple2<>(value, random);
                  }
              });
              KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);
              //基于事件驱动,每相同3个事件(即3个相同的key的数据),划分一个窗口进行计算
              WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3);
        //       timeWindow.sum(1).print();
        //       countWindow.sum(1).print();
               //apply是窗口的应用函数,即apply里的函数将应用在此窗口的数据上。
        //       timeWindow.apply(new MyTimeWindowFunction()).print();
               countWindow.apply(new MyCountWindowFunction()).print();
               try {
                   //转换算子都是懒执行的,最后要显示调用 执行程序,
                   env.execute();
              } catch (Exception e) {
                   e.printStackTrace();
              }
          }
  • 滑动窗口

    • 滑动窗口可以有重叠的部分,一个元素可以对应多个窗口。

    • 也可以基于事件和时间,代码就不写了,和翻滚窗口一样。

  • 会话窗口

    • 会话窗口不重叠,没有固定的开始和结束时间。

    • 当会话窗口在一段时间内没有接收到元素时,会关闭窗口。后续元素将分配给新的会话窗口。

六、事件时间和水印

1.时间类型

  • 处理时间

    • 当前机器处理该条事件的时间。

  • 事件时间

    • 每个事件在其生产设备上发生的时间。

    • 事件时间对于乱序、延时或者数据重放等情况,都能给出正确的结果。

    • 事件时间依赖于事件本身,跟物理时钟没有关系。

  • 摄入时间

    • 数据进入flink框架的时间。

2.水印(watermark)

  • 产生原因:数据会出现乱序问题,对于延迟数据,我们又不能无限的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算,该机制就是watermark。

  • waterMark是flink为了处理事件时间类型的窗口计算提出的一种机制,本质上也是一种时间戳。

  • 原理

    • flink使用waterMark标记所有小于该时间戳的消息都已流入,flink的数据源在确认所有小于某个时间戳的消息都已输出到flink流处理系统后,会生成一个包含该时间戳的waterMark,插入到消息流中输出到flink流处理系统中Flink operator算子按照时间窗口缓存所有流入的消息,当操作符处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口的数据进行处理并发送到下一个操作符节点,然后也将WaterMark发送到下一个操作符节点。

  • waterMark产生的方式

    • Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。 在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。

    • Periodic - 周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。

  • 示例

    • 元数据

      • 用户ID,商品ID,商品类目ID,用户行为,发生时间
        58,16,5,fav,1569866397000
        834,22,0,buy,1569866397000
        56,33,0,cart,1569866397000
        162,43,1,pv,1569866397000
    • 需求

      • 求出每分钟访问量最高的前3名商品

    • 代码

      • public static void main(String[] args) throws Exception {

        // 创建 execution environment
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 告诉系统按照 EventTime 处理
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        // 为了打印到控制台的结果不乱序,我们配置全局的并发为1,改变并发对结果正确性没有影响
        env.setParallelism(1);

               DataStreamSource<String> textFile = env.readTextFile("data/UserBehavior1.csv");

               // 创建数据源,得到 UserBehavior 类型的 流
               SingleOutputStreamOperator<UserBehavior> ds = textFile.map(new MapFunction<String, UserBehavior>() {
                   @Override
                   public UserBehavior map(String value) throws Exception {
                       String[] split = value.split(",");
                       long userID = Long.valueOf(split[0]);
                       long itemID = Long.valueOf(split[1]);
                       int categoryId = Integer.valueOf(split[2]);
                       String behavior = split[3];
                       long timestap = Long.valueOf(split[4]);
                       return new UserBehavior(userID, itemID, categoryId, behavior, timestap);
                  }
              });


               // 抽取出时间和生成 watermark,水位线随时间而递增,即水位线和当前流中数据最大时间相等。
               SingleOutputStreamOperator<UserBehavior> outputStreamOperator = ds.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
                   @Override
                   public long extractAscendingTimestamp(UserBehavior userBehavior) {
                       // 原始数据的时间,作为水位线
                       return userBehavior.timestamp ;
                  }
              });

               // 过滤出只有点击的数据
               SingleOutputStreamOperator<UserBehavior> filterOutputStream = outputStreamOperator.filter(new FilterFunction<UserBehavior>() {
                   @Override
                   public boolean filter(UserBehavior userBehavior) throws Exception {
                       // 过滤出只有点击的数据
                       return userBehavior.behavior.equals("pv");
                  }
              });
               //按商品ID进行分组
               KeyedStream<UserBehavior, Tuple> keyedStream = filterOutputStream.keyBy("itemId");
               //每5分钟计算一下最近60分钟的数据
               WindowedStream<UserBehavior, Tuple, TimeWindow> windowedStream = keyedStream.timeWindow(Time.minutes(60), Time.minutes(5));

               //进行聚合计算.统计出每个商品的点击次数
               SingleOutputStreamOperator<ItemViewCount> apply = windowedStream.apply(new WindowFunction<UserBehavior, ItemViewCount, Tuple, TimeWindow>() {
                   @Override
                   public void apply(Tuple key, TimeWindow window, Iterable<UserBehavior> input, Collector<ItemViewCount> out) throws Exception {
                       Long itemId = key.getField(0);
                       long sum = 0;
                       //统计每个商品ID点击的次数。
                       Iterator<UserBehavior> iterator = input.iterator();
                       while (iterator.hasNext()) {
                           sum++;
                           iterator.next();
                      }

                       out.collect(ItemViewCount.of(itemId, window.getEnd(), sum));
                  }
              });

               KeyedStream<ItemViewCount, Tuple> windowEnd = apply.keyBy("windowEnd");

               SingleOutputStreamOperator<String> process = windowEnd.process(new TopNHotItems(3));
        //
               process.print();

               env.execute("Hot Items Job");
        }

        }
      • public class ItemViewCount {
           public long itemId;     // 商品ID
           public long windowEnd;  // 窗口结束时间戳
           public long viewCount;  // 商品的点击量

           public static ItemViewCount of(long itemId, long windowEnd, long viewCount) {
               ItemViewCount result = new ItemViewCount();
               result.itemId = itemId;
               result.windowEnd = windowEnd;
               result.viewCount = viewCount;
               return result;
          }

           @Override
           public String toString() {
               return "ItemViewCount{" +
                       "itemId=" + itemId +
                       ", windowEnd=" + windowEnd +
                       ", viewCount=" + viewCount +
                       '}';
          }
        }
      • public class TopNHotItems extends KeyedProcessFunction<Tuple, ItemViewCount, String> {

           private final int topSize;

           public TopNHotItems(int topSize) {
               this.topSize = topSize;
          }

           // 用于存储商品与点击数的状态,待收齐同一个窗口的数据后,再触发 TopN 计算
           private ListState<ItemViewCount> itemState;

           @Override
           public void open(Configuration parameters) throws Exception {
               super.open(parameters);
               ListStateDescriptor<ItemViewCount> itemsStateDesc = new ListStateDescriptor<>(
                       "itemState-state",
                       ItemViewCount.class);
               itemState = getRuntimeContext().getListState(itemsStateDesc);
          }

           @Override
           public void processElement(
                   ItemViewCount input,
                   Context context,
                   Collector<String> collector) throws Exception {

               // 每条数据都保存到状态中,即缓存起来.
               System.err.println(input);
               itemState.add(input);

               // 当wartermark超过注册时间,则触发 。
               // 注册 windowEnd+1 的 EventTime Timer, 当触发时,说明收齐了属于windowEnd窗口的所有商品数据
               context.timerService().registerEventTimeTimer(input.windowEnd + 1);
          }

           @Override
           public void onTimer(
                   long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
               int sum=0;
               // 获取收到的所有商品点击量
               List<ItemViewCount> allItems = new ArrayList<>();
               for (ItemViewCount item : itemState.get()) {
                   sum += item.viewCount;
                   allItems.add(item);
              }
               System.out.println(sum + "------");
               // 清除本次窗口的缓存数据,释放空间
               itemState.clear();
               // 按照点击量从大到小排序
               allItems.sort(new Comparator<ItemViewCount>() {
                   @Override
                   public int compare(ItemViewCount o1, ItemViewCount o2) {
                       return (int) (o2.viewCount - o1.viewCount);
                  }
              });
               // 将排名信息格式化成 String, 便于打印
               StringBuilder result = new StringBuilder();
               result.append("====================================\n");
               result.append("时间: ").append(new Timestamp(timestamp - 1)).append("\n");

               for (int i = 0; i < allItems.size() && i < topSize; i++) {
                   ItemViewCount currentItem = allItems.get(i);
                   // No1: 商品ID=12224 浏览量=2413
                   result.append("No").append(i).append(":")
                          .append(" 商品ID=").append(currentItem.itemId)
                          .append(" 浏览量=").append(currentItem.viewCount)
                          .append("\n");
              }
               result.append("====================================\n\n");

               // 控制输出频率,模拟实时滚动结果
               Thread.sleep(1000);

               out.collect(result.toString());
          }
        }
      • public class UserBehavior {
           public long userId;         // 用户ID
           public long itemId;         // 商品ID
           public int categoryId;      // 商品类目ID
           public String behavior;     // 用户行为, 包括("pv", "buy", "cart", "fav")
           public long timestamp;      // 行为发生的时间戳,单位秒

           public UserBehavior() {

          }

           public UserBehavior(long userId, long itemId, int categoryId, String behavior, long timestamp) {
               this.userId = userId;
               this.itemId = itemId;
               this.categoryId = categoryId;
               this.behavior = behavior;
               this.timestamp = timestamp;
          }
        }

七、累加器、广播变量和分布式缓存

  • 累加器

    • 可以在分布式统计数据,只有在任务结束之后才能获取累加器的最终结果。

    • 累加器的具体实现有:IntCounter,LongCounter和DoubleCounter。

    • 注意点:

      • 需要在算子内部创建累加器对象。

      • 通常在Rich函数中的open方法中注册累加器,指定累加器的名称。

      • 当前算子内任意位置可以使用累加器。

      • 必须当任务结束后,通过execute执行后的JobExecutionResult对象获取累加器的值。

    • 源码

      • public static void main(String[] args) throws Exception {

               ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

               //2:读取数据源
               DataSet<String> text = env.readTextFile("data/textFile");


               MapOperator<String, String> sxt = text.map(new RichMapFunction<String, String>() {

                   IntCounter intCounter = new IntCounter();

                   @Override
                   public void open(Configuration parameters) throws Exception {
                       getRuntimeContext().addAccumulator("my-accumulator", intCounter);
                  }

                   @Override
                   public String map(String value) throws Exception {
                       if (value.contains("sxt")) {
                           intCounter.add(1);
                      }



                       return value;
                  }
              });

               sxt.writeAsText("data/my.txt", FileSystem.WriteMode.OVERWRITE);


               JobExecutionResult counter = env.execute("counter");

               Integer result = counter.getAccumulatorResult("my-accumulator");

               System.out.println( result);

          }
  • 广播变量

    • 数据集合通过withBroadcastSet进行广播

    • 可通过getRuntimeContext().getBroadcastVariable访问

    • 源码

      • public static void main(String[] args) throws Exception {
         
              ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
         
              DataSource<String> textFile = environment.readTextFile("data/textfile");
         
              List<String> list = new ArrayList<>();
         
              list.add("shsxt");
              list.add("bjsxt");
         
              DataSource<String> whiteDs = environment.fromCollection(list);
         
              FilterOperator<String> f1 = textFile.filter(new RichFilterFunction<String>() {
         
                  List<String> whiteNames = null;
         
                  @Override
                  public void open(Configuration parameters) throws Exception {
                      whiteNames = getRuntimeContext().getBroadcastVariable("white-name");
                  }
         
                  @Override
                  public boolean filter(String value) throws Exception {
         
                      for (String whileName : whiteNames) {
                        if (value.contains(whileName)) {
                          return true;
                      }
                  }
       
                  return false;
              }
          });
       
          //f1 operator算子可以得到广播变量。
          FilterOperator<String> f2 = f1.withBroadcastSet(whiteDs, "white-name");
       
          f2.print();
  • 分布式缓存

    • flink在执行程序时会自动将文件或目录赋值到所有worker的本地文件系统。

    • 源码

      • public static void main(String[] args) throws Exception {
              ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
         
              //当前项目路径.
              String project_path =System.getProperty("user.dir");
         
              //可以是本地文件或hdfs文件,hdfs文件路径则以hdfs://开头
              environment.registerCachedFile("file:///" + project_path + "/data/textfile", "myfile");
         
              DataSource<String> elements = environment.fromElements("hadoop", "flink", "spark", "hbase");
         
              MapOperator<String, String> map = elements.map(new RichMapFunction<String, String>() {
         
                  @Override
                  public void open(Configuration parameters) throws Exception {
                      //在worker端获取缓存文件
                      File myfile = getRuntimeContext().getDistributedCache().getFile("myfile");
                      //读取缓存文件
                      List<String> list = FileUtils.readLines(myfile);
         
                      for (String line : list) {
                          System.out.println("[" + line + "]");
                      }
                  }
         
                  @Override
                  public String map(String value) throws Exception {
         
                      return value;
                  }
              });
         
              map.print();
        }
posted @ 2020-01-02 16:24  数据阮小白  阅读(333)  评论(0编辑  收藏  举报