Flink

Flink

一、概念

Apache Flink 是一个实时计算框架分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算。

  • 特点:
    • 支持高吞吐、低延迟、高性能的流处理
    • 支持带有事件时间的窗口(Window)操作
    • 支持有状态计算的Exactly-once语义<Exactly-once:当任意条数据流转到某分布式系统中,如果系统在整个处理过程中对该任意条数据都仅精确处理一次,且处理结果正确,则被认为该系统满足Exactly-Once一致性>
    • 支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作
    • 支持具有反压功能的持续流模型
    • 支持基于轻量级分布式快照(Snapshot)实现的容错
    • 一个运行时同时支持Batch on Streaming处理和Streaming处理
    • Flink在JVM内部实现了自己的内存管理,避免了出现oom
    • 支持迭代计算
    • 支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存
二、Spark Streaming 与 Flink的区别

(主要区别)

  • Flink:实时处理模型,基于事件处理

  • Spark Streaming:微批模型

(次要区别)

  • 架构模型:Spark Streaming 在运行时的主要角色包括:Master、Worker、Driver、Executor, Flink 在运行时主要包:Jobmanager、Taskmanager 和 Slot。
  • 任务调度:Spark Streaming 连续不断的生成微小的数据批次,构建有向无环图 DAG, Spark Streaming 会依次创DStreamGraph、JobGenerator、JobScheduler。Flink 根据用户 提交的代码生成 StreamGraph,经过优化生成 JobGraph,然后提交给JobManager 进行处理, JobManager 会根据 JobGraph 生成 ExecutionGraph,ExecutionGraph 是 Flink 调度最核心的数据结构,JobManager 根据 ExecutionGraph 对 Job 进行调度。
  • 时间机制:Spark Streaming 支持的时间机制有限,只支持处理时间。 Flink 支持了流 处理程序在时间上的三个定义:处理时间、事件时间、注入时间。同时也支持 watermark 机制来处理滞后数据。
  • 容错机制:对于 Spark Streaming 任务,我们可以设置 checkpoint,然后假如发生故障 并重启,我们可以从上次 checkpoint 之处恢复,但是这个行为只能使得数据不丢失,可能会重复处理,不能做到恰好一次处理语义。Flink 则使用两阶段提交协议来解决这个问题。
三、实时计算框架
  • storm

    • 优势:
      框架简单,学习成本低
      实时性很好,可以提供毫秒级延迟
      稳定性很好,框架比较成熟
    • 劣势:
      编程成本较高
      框架处理逻辑和批处理完全不一样,无法共用代码
      框架Debug较为复杂
  • SparkStreaming(微批处理)

    • 优势:
      编程原语丰富,编程简单
      框架封装层级较高,封装性好
      可以共用批处理处理逻辑,兼容性好
      基于Spark,可以无缝内嵌Spark其他子项目,如Spark Sql,MLlib等
    • 劣势:
      微批处理,时间延迟大
      稳定性相对较差
      机器性能消耗较大
  • Flink(流式处理)

    • 优势
      Flink流处理为先的方法可提供低延迟,高吞吐率,近乎逐项处理的能力
      Flink的很多组件是自行管理的
      通过多种方式对工作进行分析进而优化任务
      提供了基于Web的调度视图
四、常用算子
  • 处理方式core(流处理、批处理、流批和一)

    package org.zz.shujia.flink.core;
    
    import org.apache.flink.api.common.RuntimeExecutionMode;
    import org.apache.flink.api.common.functions.FlatMapFunction;
    import org.apache.flink.api.common.typeinfo.Types;
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.streaming.api.datastream.DataStream;
    import org.apache.flink.streaming.api.datastream.KeyedStream;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.util.Collector;
    
    public class Demo02WordCountBatch {
        public static void main(String[] args) throws Exception {
    
            /**
             * BATCH:批处理
             * 1、底层基于MR模型
             * 2、只能用于处理有界的数据
             * STREAM:流处理
             * 1、底层基于持续流模型
             * 2、既能处理无界流也可以用于有界流
             *
             * 流批合一:在Flink中同一套DataStream的API既可以用作流处理也可以用作批处理
             */
    
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
            DataStream<String> ds = env.socketTextStream("master", 8888);
    
    //        env.setRuntimeMode(RuntimeExecutionMode.BATCH);     //批处理
            env.setRuntimeMode(RuntimeExecutionMode.STREAMING);  //流处理
    
    //方式一:
            DataStream<String> map = ds.flatMap(new FlatMapFunction<String, String>() {
    
                @Override
                public void flatMap(String line, Collector<String> collector) throws Exception {
                    String[] strings = line.split(",");
                    for (String string : strings) {
                        collector.collect(string);
                    }
                }
            });
    //方式二:
            DataStream<String> flatMap = ds.flatMap((line, collector) -> {
                String[] split = line.split(",");
                for (String s : split) {
                    collector.collect(s);
                }
            }, Types.STRING);
    
            DataStream<Tuple2<String, Integer>> map1 = flatMap.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT));
    
    
            KeyedStream<Tuple2<String, Integer>, String> keyBy = map1.keyBy(word -> word.f0);
    
            DataStream<Tuple2<String, Integer>> sum = keyBy.sum(1);
    
            sum.print();
    
            env.execute();
    
        }
    }
    
  • 读操作(Source)

    • FileSource
     /**
             * 新版读取文件的方式
             * 1、指定一个格式的方式
             * 2、指定一个路径
             * 3、通过monitorContinuously来实现对目录的实时监控,将读文件转换无界流进行处理
             */
    
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
            FileSource<String> fileSource = FileSource.forRecordStreamFormat(new TextLineInputFormat()
                            , new Path(""))
                    .monitorContinuously(Duration.ofMillis(5))// 指定时间间隔监控目录的变化
                    .build();
    
            DataStreamSource<String> source = env.fromSource(fileSource, WatermarkStrategy.noWatermarks(), "fileSource");
    
            source.print();
            env.execute();
    
    • SocketSource

      /**
       * Socket Source:
       * 一般用于代码调试及开发
       */
              DataStreamSource<String> socketDS = env.socketTextStream("master", 8888);
      
    • CollectionSource

              ArrayList<String> arr = new ArrayList<>();
      
              arr.add("java,scala,python");
              arr.add("java,scala,python");
              arr.add("java,scala,python");
              arr.add("java,scala,python");
              arr.add("java,scala,python");
      
              DataStreamSource<String> arrDS = env.fromCollection(arr
      
      
  • 写操作(sink)

    • FileSink

      StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
              env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
      
              DataStreamSource<String> file = env.readTextFile("spark/flink/data");
      
              DataStream<Tuple2<String, Integer>> stuNum = file
                      .map(line -> Tuple2.of(line.split(",")[4], 1), Types.TUPLE(Types.STRING,Types.INT))
                      .keyBy(kv -> kv.f0)
                      .sum(1);
      
      
              DataStream<String> nuwValue = stuNum.map(kv -> kv.f0 + "\t" + kv.f1);
      
      
              //存储
              FileSink<String> fileSink = FileSink.forRowFormat(new Path("flink/data/clazz_cnt"), new SimpleStringEncoder<String>("utf-8")).build();
      
              nuwValue.sinkTo(fileSink);
      
              env.execute();
      
  • 常用算子

    • Map
    /*
             * map操作:传入一条数据返回一条数据
             */
    studentDS.map(new MapFunction<String, String>() {
                @Override
                public String map(String s) throws Exception {
                    return s.split(",")[1];
                }
            }).print();
    
    • FlatMap

      /*
               * flatmap
              操作:传入一条数据返回一系列数据
               */
      source.flatMap(new FlatMapFunction<String, String>() {
                  /**
                   * 传入一条数据flatMap方法就会执行一次
                   */
                  @Override
                  public void flatMap(String value, Collector<String> out) throws Exception {
                      for (String word : value.split(",")) {
                          out.collect(word);
                      }
                  }
              }).print();
      
    • Filter

        /*
               * FLink中的算子不是懒执行的,不需要action算子触发
               * 统一由env.execute()触发Flink任务的执行
               */
      
      studentDS.filter(new FilterFunction<String>() {
                  @Override
                  public boolean filter(String s) throws Exception {
                      String[] strings = s.split(",");
                      String clazz = strings[4];
                      String gender = strings[3];
                      return clazz.startsWith("文科") && "女".equals(gender);
                  }
              });
      
    • KeyBy

      /*
               * keyBy:进行流上的分组
               * 让相同key的数据能够进入同一个线程对应的Task中进行处理
               * 同一个Task中也会有不同的key
               */
      DataStream<Tuple2<String, Integer>> kvDS = lineDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                  @Override
                  public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                      for (String word : s.split(",")) {
                          collector.collect(Tuple2.of(word, 1));
                      }
                  }
              });
      
      
              KeyedStream<Tuple2<String, Integer>, String> keyBy = kvDS.keyBy(kv -> kv.f0);
      
      
              keyBy.sum(1).print();
      
    • Reduce

       DataStream<Tuple2<String, Integer>> kvDS = lineDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                  @Override
                  public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                      for (String word : s.split(",")) {
                          collector.collect(Tuple2.of(word, 1));
                      }
                  }
              });
      
      
              KeyedStream<Tuple2<String, Integer>, String> keyBy = kvDS.keyBy(kv -> kv.f0);
      
      
              SingleOutputStreamOperator<Tuple2<String, Integer>> reduce = keyBy.reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                  @Override
                  public Tuple2<String, Integer> reduce(Tuple2<String, Integer> t1, Tuple2<String, Integer> t2) throws Exception {
                      return Tuple2.of(t1.f0, t1.f1 + t2.f1);
                  }
              });
      
五、核心组件
  • Client(作业客户端)

  • JobManager(作业管理器)

  • TaskManager(任务管理器)

工作流程:
  • 当 Flink 集群启动后,首先会启动一个 JobManager 和一个或多个的 TaskManager。
  • JobManager负责作业调度,收集TaskManager的Heartbeat和统计信息,
  • TaskManager 之间以流的形式进行数据的传输。
六、Flink事件时间

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Processing Time : 处理时间,当前机器处理该事件的时间(即进入某个算子时的系统时间),有着最好的性能和最低的延迟
  • Ingestion Time : 摄入时间,数据进入Flink框架的时间,在Source Operator中设置,每个事件拿到当前时间作为时间戳,后续的时间窗口基于该时间;相比ProcessingTime可以提供更可预测的结果
  • Event Time : 事件时间是每条事件在它产生的时候记录的时间,该时间记录在事件中,在处理的时候可以被提取出来;事件事件对于乱序、延时、或者数据重放等情况,都能给出正确都结果,事件时间依赖于事件本身,而跟物理时钟没有关系,利用事件时间编程必须如何制定如何生成事件时间的watermark;
    • 事件时间存在一定的延时,因此自然的需要延时和无序事件等待一段时间;因此,使用事件时间编程通常需要与处理时间相结合;
水位线Watermark

Watermark是Flink上为了处理EventTime时间类型的窗口计算提出的一种机制,本质上也是一种时间戳;WaterMark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现;

  • 当operator通过Event Time的时间窗口来处理数据时,它必须在确定所有属于该时间窗口的消息全部流入此操作符后,才能开始处理数据,但是由于消息可能是乱序的,所以operator无法直接确认任何所有属于该时间窗口的消息全部流入此操作符;

  • WaterMark包含一个时间戳,Flink使用WaterMark保证所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于某个时间戳的消息都已输出到Flink流处理器后,会生成一个包含该时间戳的WaterMark,插入到消息流中输出到Flink流处理系统中;

  • Flink operator算子按照时间窗口缓存所有流入的消息,当操作符处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口的数据进行处理并发送到下一个操作符节点,然后也将WaterMark发送到下一个操作符节点;

  • 一旦一个watermark到达了operator,operator可以将内部事件时间提前到watermark的时间戳

    两种水位线策略

    方式1、使用数据中最大的时间作为水位线
    方式2、将水位线前移5s,解决数据延时到达的问题(前移的太多就会导致整体任务延时较大)

     DataStream<Tuple2<String, Long>> assDS = wordTimeDS
                    .assignTimestampsAndWatermarks( //(分配时间戳与水位线)
                            WatermarkStrategy  //(水位线策略)
                                    // 方式1、使用数据中最大的时间作为水位线
                                    //  .<Tuple2<String, Long>>forMonotonousTimestamps()
                                    // 方式2、将水位线前移5s,解决数据延时到达的问题
                                    // 前移的太多就会导致整体任务延时较大
                                    .<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                                    // 告诉flink,数据中哪部分作为时间戳,即使用事件时间
                                    .withTimestampAssigner(TimestampAssignerSupplier.of((kv, ts) -> kv.f1)
                                    ));
    
七、Flink窗口
  • Time Window

     1、基于时间的窗口
          基于事件时间:
                 滑动:SlidingEventTimeWindows
                 滚动:TumblingEventTimeWindows
           基于处理时间:
                  滚动:TumblingProcessingTimeWindows
    
    案例:
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
    
            DataStream<String> wordDS = env.socketTextStream("master", 8888);
    
            // 如果后续需要基于事件时间进行统计 则需要设置事件时间以及水位线
            DataStream<Tuple2<String, Long>> assignDS = wordDS.map(line -> {
                        String[] split = line.split(",");
                        String word = split[0];
                        long ts = Long.parseLong(split[1]);
                        return Tuple2.of(word, ts);
                    }, Types.TUPLE(Types.STRING, Types.LONG))
                    .assignTimestampsAndWatermarks(WatermarkStrategy
                            .<Tuple2<String, Long>>forMonotonousTimestamps()
                            .withTimestampAssigner((kv, ts) -> kv.f1)
                    );
    
            // 每个5s统计最近10s内的单词数量
            assignDS.map(kv -> Tuple2.of(kv.f0, 1),Types.TUPLE(Types.STRING,Types.INT))
                    .keyBy(kv -> kv.f0)
                    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                    .sum(1)
                    .print();
    
            env.execute();
    
  • Session Window

      2、基于会话的窗口
     基于事件时间:EventTimeSessionWindows
     基于处理时间:ProcessingTimeSessionWindows
    
    DataStream<String> wordDS = env.socketTextStream("master", 8888);
    
            // 基于处理时间:ProcessingTimeSessionWindows
    //        wordDS.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT))
    //                .keyBy(kv->kv.f0)
    //                .window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)))
    //                .sum(1)
    //                .print();
    
            // 基于事件时间:EventTimeSessionWindows
            // 指定事件时间以及水位线
            DataStream<Tuple2<String, Long>> assignDS = wordDS.map(line -> {
                        String[] split = line.split(",");
                        String word = split[0];
                        long ts = Long.parseLong(split[1]);
                        return Tuple2.of(word, ts);
                    }, Types.TUPLE(Types.STRING, Types.LONG))
                    .assignTimestampsAndWatermarks(WatermarkStrategy
                            .<Tuple2<String, Long>>forMonotonousTimestamps()
                            .withTimestampAssigner((kv, ts) -> kv.f1)
                    );
    
            assignDS.map(word -> Tuple2.of(word.f0, 1), Types.TUPLE(Types.STRING, Types.INT))
                    .keyBy(kv->kv.f0)
                    .window(EventTimeSessionWindows.withGap(Time.seconds(5)))
                    .sum(1)
                    .print();
    
  • Count Window

/* 3、基于计数的窗口:可以直接在KeyBy之后直接.出来
 *       滑动:
 *       滚动:
 */
wordDS.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT))
                .keyBy(kv -> kv.f0)
//                .countWindow(5) // 每个key每5条数据统计一次
                .countWindow(10, 5) // 每个key每5条数据统计最近的10条数据
                .sum(1)
                .print();
八、卡口流量案例
public static void main(String[] args) throws Exception {
        /*
         * 需求:基于卡口过车数据统计道路的拥堵情况
         * 拥堵情况:通过车速以及车流量进行判断
         * 思路:通过对道路以及卡口进行分组,统计车流量以及平均车速
         * 写代码的思路:
         *      1、接入数据,通过Socket模拟实时过车数据
         *      2、提取数据中的时间,并设置事件时间及水位线
         *      3、按照道路及卡口分组,使用滑动窗口每隔1分钟统计最近10分钟内车流量信息
         *      4、打印数据
         */

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(1);

        DataStreamSource<String> lineDS = env.socketTextStream("master", 8888);

        // 对每条数据进行切分,并将其转换成Car的对象
        SingleOutputStreamOperator<Car> carDS = lineDS.map(line -> {
            String[] splits = line.split(",");
            return new Car(splits[0], Integer.parseInt(splits[1]), Integer.parseInt(splits[2])
                    , splits[3], splits[4], splits[5]
                    , splits[6], Long.parseLong(splits[7]) * 1000, Double.parseDouble(splits[8])
            );
        });

        // 设置事件时间以及水位线
        SingleOutputStreamOperator<Car> assignDS = carDS.assignTimestampsAndWatermarks(
                WatermarkStrategy
                        .<Car>forMonotonousTimestamps()
                        .withTimestampAssigner((car, ts) -> car.getTime())
        );

        // 按照卡口以及道路id进行分组
        assignDS.keyBy(car -> car.getRoadId() + "|" + car.getkId())
                // 使用滑动窗口,每隔1分钟统计最近10分钟内的卡口数据
                .window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(1)))
                // 统计车流量又需要统计平均车速
                // 没有直接的API可以使用,需要使用底层的API自定义窗口的计算逻辑
                .process(new ProcessWindowFunction<Car, Result, String, TimeWindow>() {
                    /**
                     *
                     * @param s 分组的Key,由卡口编号及道路编号拼接而成
                     * @param context Flink任务运行时的上下文环境
                     * @param elements 每个分组每个窗口接收到的数据,即最近10分钟内的数据
                     * @param out 用于将结果输出到下游
                     */
                    // 每个窗口会执行一次,即1分钟执行一次
                    @Override
                    public void process(String s, ProcessWindowFunction<Car, Result, String, TimeWindow>.Context context, Iterable<Car> elements, Collector<Result> out) throws Exception {
                        // 提取道路id以卡口id
                        String[] roadAndKId = s.split("\\|");
                        int cnt = 0;
                        double sumSpeed = 0;
                        for (Car car : elements) {
                            sumSpeed += car.getSpeed();
                            // 统计车流量
                            cnt++;
                        }
                        // 计算平均车速
                        double avgSpeed = sumSpeed / cnt;
                        // 构建Result对象,通过out进行输出
                        out.collect(new Result(roadAndKId[0], roadAndKId[1], cnt, avgSpeed));
                    }
                }).print();

        env.execute();

    }
}

class Result {
    private String roadId;
    private String kId;
    private Integer carCnt;
    private Double avgSpeed;

    public Result(String roadId, String kId, Integer carCnt, Double avgSpeed) {
        this.roadId = roadId;
        this.kId = kId;
        this.carCnt = carCnt;
        this.avgSpeed = avgSpeed;
    }

    @Override
    public String toString() {
        return "Result{" +
                "roadId='" + roadId + '\'' +
                ", kId='" + kId + '\'' +
                ", carCnt=" + carCnt +
                ", avgSpeed=" + avgSpeed +
                '}';
    }
}

class Car {
    private String car;
    private Integer cityId;
    private Integer countyId;
    private String kId;
    private String cameraId;
    private String direction;
    private String roadId;
    private Long time;
    private Double speed;

    public Car(String car, Integer cityId, Integer countyId, String kId, String cameraId, String direction, String roadId, Long time, Double speed) {
        this.car = car;
        this.cityId = cityId;
        this.countyId = countyId;
        this.kId = kId;
        this.cameraId = cameraId;
        this.direction = direction;
        this.roadId = roadId;
        this.time = time;
        this.speed = speed;
    }

    public String getCar() {
        return car;
    }

    public Integer getCityId() {
        return cityId;
    }

    public Integer getCountyId() {
        return countyId;
    }

    public String getkId() {
        return kId;
    }

    public String getCameraId() {
        return cameraId;
    }

    public String getDirection() {
        return direction;
    }

    public String getRoadId() {
        return roadId;
    }

    public Long getTime() {
        return time;
    }

    public Double getSpeed() {
        return speed;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值