【拉钩学习】笔记分享の"轰炸"Flink-上卷(基础篇)

287 阅读25分钟

本文参考自 42讲轻松通关 Flink (lagou.com)
拉钩有多类优质专栏,本人小白一枚,大部分技术入门都来自拉钩以及Git,不定时在掘金发布自己的总结

「〇」预热

0.1 历史 & 当下

随着大数据时代的发展、海量数据的实时处理和多样业务的数据计算需求激增,传统的批处理方式和早期的流式处理框架也有自身的局限性,难以在延迟性、吞吐量、容错能力,以及使用便捷性等方面满足业务日益苛刻的要求。
在这种形势下,Flink 以其独特的天然流式计算特性更为先进的架构设计,极大地改善了以前的流式处理框架所存在的问题。

从我们最初认识的 Storm,再到 Spark 的异军突起,迅速占领了整个实时计算领域。
直到 2019 年 1 月底,阿里巴巴内部版本 Flink 正式开源!一石激起千层浪,Flink 开源的消息立刻刷爆朋友圈,整个大数据计算领域一直以来由 Spark 独领风骚,瞬间成为两强争霸的时代

0.2 何为流式

传统的分析方式通常是利用批查询,或将事件(生产上一般是消息)记录下来并基于此形成有限数据集(表)构建应用来完成。
为了得到最新数据的计算结果,必须先将它们写入表中并重新执行 SQL 查询,然后将结果写入存储系统比如 MySQL 中,再生成报告

Apache Flink 同时支持流式及批量分析应用,这就是我们所说的批流一体
Flink 在实际场景中承担了数据的实时采集实时计算下游发送

「I」基础

1.1 概念梳理

1.1.1 实时数仓 & ETL

ETL(Extract-Transform-Load)的目的是将业务系统的数据经过抽取清洗转换之后加载到数据仓库的过程

  • 传统离线数仓 ==> 将业务数据集中进行存储后,以固定的计算逻辑定时进行 ETL 和其他建模后产出报表等应用

    主要是构建 T+1 的离线数据,通过定时任务每天拉取增量数据,然后创建各个业务相关的主题维度数据,对外提供 T+1 的数据查询接口

  • 实时数据仓库 ==> 数据本身的价值随着时间的流逝会逐步减弱,因此数据发生后必须尽快的达到用户的手中,实时数仓的构建需求也应运而生......

实时数据仓库的建设是“数据智能 BI”必不可少的一环,也是大规模数据应用中必然面临的挑战

🤔 那么问题来了:为啥Flink总和我们说的"实时"挂钩呢?

  1. 状态管理,实时数仓里面会进行很多的聚合计算,这些都需要对于状态进行访问和管理,Flink 支持强大的状态管理;
  2. 丰富的 API,Flink 提供极为丰富的多层次 API,包括 Stream API、Table API 及 Flink SQL;
  3. 生态完善,实时数仓的用途广泛,Flink 支持多种存储(HDFS、ES 等);
  4. 批流一体,Flink 已经在将流计算和批计算的 API 进行统一。

1.1.2 特性

Flink 的主要特性包括:批流一体Exactly-Once强大的状态管理等。
同时,Flink 还支持运行在包括 YARN、 Mesos、Kubernetes 在内的多种资源管理框架上。

据阿里巴巴多年实践证明,Flink 已经可以扩展到数千核心,其状态可以达到 TB 级别,且仍能保持高吞吐、低延迟的特性

1.1.3 架构模型

4种不同级别的抽象

img

对于我们开发者来说,大多数应用程序不需要上图中的最低级别的 Low-level 抽象,而是针对 Core API 编程
比如 DataStream API(有界/无界流)和 DataSet API (有界数据集)—— 这些流畅的 API 提供了用于数据处理的通用构建块,比如各种形式用户指定的转换、连接、聚合、窗口、状态等。

Table APISQL 是 Flink 提供的更为高级的 API 操作,Flink SQL 是 Flink 实时计算为简化计算模型,降低用户使用实时计算门槛而设计的一套符合标准 SQL 语义的开发语言。

1.1.4 数据流模型

Flink 程序的基础构建模块是流(Streams)转换(Transformations)
每一个数据流起始于 1~N 个 Source,并终止于 0~N 个 Sink。数据流类似于 有向无环图(DAG)。

实际上面对复杂的生产环境,Flink 任务大都是并行进行和分布在各个计算节点上。
在 Flink 任务执行期间,每一个数据流都会有多个分区,并且每个算子都有多个算子任务并行进行

算子任务的数量是该特定算子的并行度(Parallelism),对并行度的设置是 Flink 任务进行调优的重要手段

img

从上图中可以看到,在上面的 map 和 keyBy/window 之间,以及 keyBy/window 和 Sink 之间,因为并行度的差异,数据流都进行了重新分配

1.1.5 窗口 & 时间

窗口时间是 Flink 中的核心概念之一。
在实际成产环境中,对数据流上的聚合需要由窗口来划定范围,比如 “计算过去的 5 分钟” 或者“最后 100 个元素的和”。

  • 窗口模型:滚动窗口(Tumbling Window)滑动窗口(Sliding Window)会话窗口(Session Window)
  • 时间语义:事件时间(Event Time)摄取时间(Ingestion Time)处理时间(Processing Time)

Flink 自身还支持了有状态的算子操作容错机制CheckpointExactly-once 语义等更多高级特性,来支持用户在不同的业务场景中的需求

1.2 入门案例 WordCount

我们首先会从环境搭建入手,介绍如何搭建本地调试环境的脚手架;
然后分别从DataSet(批处理)和 DataStream(流处理)两种方式如何进行单词计数开发;
最后介绍 Flink Table 和 SQL 的使用

1.2.1 环境搭建

Flink 以 Java 或 Scala 作为开发语言的开源大数据项目,通常我们推荐使用 Java 来作为开发语言,Maven 作为编译和包管理工具进行项目构建和编译。

  • JDK 1.8+
  • Maven 3.2.5
  • Git 建议跟随官网

1.2.2 实现功能

  1. Maven依赖

    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<flink.version>1.10.0</flink.version>
    		<java.version>1.8</java.version>
    		<scala.binary.version>2.11</scala.binary.version>
    		<maven.compiler.source>${java.version}</maven.compiler.source>
    		<maven.compiler.target>${java.version}</maven.compiler.target>
    	</properties>
    
    	<dependencies>
            <!-- 基础开发环境 DataSet & DataStream -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-java</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-clients_2.11</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <!-- Flink Table & SQL -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table-api-java-bridge_2.11</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table-planner-blink_2.11</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table-planner_2.11</artifactId>
                <version>${flink.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
                <version>${flink.version}</version>
            </dependency>
        </dependencies>
    
  2. DataSet 版

    import org.apache.flink.api.common.functions.FlatMapFunction;
    import org.apache.flink.api.java.DataSet;
    import org.apache.flink.api.java.ExecutionEnvironment;
    import org.apache.flink.api.java.operators.AggregateOperator;
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.streaming.api.datastream.DataStreamSource;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.util.Collector;
    
    public class WordCountByDStream {
    
       public static void main(String[] args) throws Exception {
          // 0. 获取运行环境
          ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
          // 1. 创建DataSet,这里我们的输入是一行一行的文本
          DataSet<String> text = env.fromElements(
                "Flink Spark Storm",
                "Flink Spark Scala",
                "Flink Scala"
          );
          // 2. 通过 Flink 内置的转换函数进行计算
          DataSet<Tuple2<String, Integer>> counts = text.flatMap(new LineSplitter())
                .groupBy(0)
                .sum(1);
          // 3. 打印结果
          counts.printToErr();
          /*
          (Storm,1)
          (Spark,2)
          (Scala,2)
          (Flink,3)
           */
       }
    
       /**
        * 计算过程
        */
       public static class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
          @Override
          public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
             // 分割文本
             String[] split = s.split("\\W+");
             for (String word : split) {
                if (word.length() > 0) {
                   collector.collect(new Tuple2<String, Integer>(word, 1));
                }
             }
          }
       }
    
    }
    
  3. DataStream 版

    import org.apache.flink.api.common.functions.FlatMapFunction;
    import org.apache.flink.api.common.functions.ReduceFunction;
    import org.apache.flink.api.java.DataSet;
    import org.apache.flink.api.java.ExecutionEnvironment;
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.streaming.api.datastream.DataStream;
    import org.apache.flink.streaming.api.datastream.DataStreamSource;
    import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.streaming.api.windowing.time.Time;
    import org.apache.flink.util.Collector;
    
    public class WordCountByDStream {
    
       public static void main(String[] args) throws Exception {
          // 0. 获取运行环境
          StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
          // 1. 监听本地 9000 端口
          DataStreamSource<String> text = env.socketTextStream("127.0.0.1", 9000, "\n");
          // 2. 数据处理(拆分、分组、窗口计算)
          DataStream<WordWithCount> windowCounts = text.flatMap(new FlatMapFunction<String, WordWithCount>() {
             @Override
             public void flatMap(String s, Collector<WordWithCount> collector) throws Exception {
                for (String word : s.split("\\s")) {
                   collector.collect(new WordWithCount(word, 1L));
                }
             }
          }) // 压平,把每行的单词转为<word,count>类型的数据
                .keyBy("word") // 针对相同的word数据进行分组
                .timeWindow(Time.seconds(5), Time.seconds(1)) // 指定计算数据的窗口大小和滑动窗口大小
                .reduce(new ReduceFunction<WordWithCount>() {
                   @Override
                   public WordWithCount reduce(WordWithCount a, WordWithCount b) throws Exception {
                      return new WordWithCount(a.word, a.count + b.count);
                   }
                });
          // 3. 结果输出(使用一个并行度)
          windowCounts.print().setParallelism(1);
          // *. 因为flink是懒加载的,所以必须调用execute方法,上面的代码才会执行
          env.execute("Socket Window WordCount");
       }
    
       /**
        * 用于存储单词以及单词出现的次数
        */
       public static class WordWithCount {
          public String word;
          public long count;
    
          public WordWithCount() {}
    
          public WordWithCount(String word, long count) {
             this.word = word;
             this.count = count;
          }
    
          @Override
          public String toString() {
             return word + " : " + count;
          }
       }
    
    }
    
  4. SQL 版

    import org.apache.flink.api.java.DataSet;
    import org.apache.flink.api.java.ExecutionEnvironment;
    import org.apache.flink.table.api.Table;
    import org.apache.flink.table.api.java.BatchTableEnvironment;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class WordCountBySQL {
    
       public static void main(String[] args) throws Exception {
          // 0. 获取运行环境
          ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
          BatchTableEnvironment sqlEnv = BatchTableEnvironment.create(env);
          // 1. mock数据
          List<WordWithCount> list = new ArrayList<WordWithCount>();
          String words = "Spark Flink Java Spark Flink Flink";
          // 2. 操作数据&数据输出
          for (String word : words.split("\\W+")) {
             list.add(new WordWithCount(word, 1));
          }
          DataSet<WordWithCount> input = env.fromCollection(list);
          /* DataSet 转 SQL(指定字段名) */
          Table table = sqlEnv.fromDataSet(input, "word,counts");
          table.printSchema();
          /* 注册为一个表 */
          sqlEnv.createTemporaryView("word_count", table);
          Table table02 = sqlEnv.sqlQuery("select word as word, sum(counts) as counts from word_count GROUP BY word");
          /* 将表转为DataSet */
          DataSet<WordWithCount> output = sqlEnv.toDataSet(table02, WordWithCount.class);
          output.printToErr();
       }
    
       /**
        * 用于存储单词以及单词出现的次数
        */
       public static class WordWithCount {
          public String word;
          public long counts;
    
          public WordWithCount() {}
    
          public WordWithCount(String word, long counts) {
             this.word = word;
             this.counts = counts;
          }
    
          @Override
          public String toString() {
             return word + " : " + counts;
          }
       }
    
    }
    

1.3 和其他框架的对比

1.3.1 Flink 核心概念

  • Streams(流),流分为有界流和无界流。
    有界流指的是有固定大小,不随时间增加而增长的数据,比如我们保存在 Hive 中的一个表;
    无界流指的是数据随着时间增加而增长,计算状态持续进行,比如我们消费 Kafka 中的消息,消息持续不断,那么计算也会持续进行不会结束。
  • State(状态),所谓的状态指的是在进行流式计算过程中的信息。
    一般用作容错恢复和持久化,流式计算在本质上是增量计算,也就是说需要不断地查询过去的状态。
    状态在 Flink 中有十分重要的作用,例如为了确保 Exactly-once 语义需要将数据写到状态中;
    此外,状态的持久化存储也是集群出现 Fail-over 的情况下自动重启的前提条件。
  • Time(时间),Flink 支持了 Event time、Ingestion time、Processing time 等多种时间语义,时间是我们在进行 Flink 程序开发时判断业务状态是否滞后和延迟的重要依据。
  • API:Flink 自身提供了不同级别的抽象来支持我们开发流式或者批量处理程序,由上而下可分为 SQL / Table API、DataStream API、ProcessFunction 三层,开发者可以根据需要选择不同层级的 API 进行开发。

在分布式运行环境中,Flink 提出了算子链的概念,Flink 将多个算子放在一个任务中,由同一个线程执行,减少线程之间的切换、消息的序列化/反序列化、数据在缓冲区的交换,减少延迟的同时提高整体的吞吐量

1.3.2 集群模型和角色

在实际生产中,Flink 都是以集群在运行,在运行的过程中包含了两类进程:

  • JobManager:它扮演的是集群管理者的角色,负责调度任务、协调 checkpoints、协调故障恢复、收集 Job 的状态信息,并管理 Flink 集群中的从节点 TaskManager。
  • TaskManager:实际负责执行计算的 Worker,在其上执行 Flink Job 的一组 Task;TaskManager 还是所在节点的管理员,它负责把该节点上的服务器信息比如内存、磁盘、任务运行情况等向 JobManager 汇报。
  • Client:用户在提交编写好的 Flink 工程时,会先创建一个客户端再进行提交,这个客户端就是 Client,Client 会根据用户传入的参数选择使用 yarn per job 模式、stand-alone 模式还是 yarn-session 模式将 Flink 程序提交到集群。

img

1.3.3 资源和资源组

在 Flink 集群中,一个 TaskManger 就是一个 JVM 进程,并且会用独立的线程来执行 task
为了控制一个 TaskManger 能接受多少个 task,Flink 提出了 Task Slot 的概念

我们可以简单的把 Task Slot 理解为 TaskManager 的计算资源子集
假如一个 TaskManager 拥有 5 个 slot,那么该 TaskManager 的计算资源会被平均分为 5 份,不同的 task 在不同的 slot 中执行,避免资源竞争。
但是需要注意的是,slot 仅仅用来做内存的隔离,对 CPU 不起作用。
因此 运行在同一个 JVM 的 task 可以共享 TCP 连接,减少网络传输,在一定程度上提高了程序的运行效率,降低了资源消耗。

img

与此同时,Flink 还允许将不能形成算子链的两个操作(比如下图中的 flatmap 和 key&sink) 放在一个 TaskSlot 里执行以达到资源共享的目的。

img

1.3.4 和其他框架对比

StomSpark StreamingFlink
架构经典的主从模式,并且强依赖 ZooKeeper本质是微批处理,每个 batch 都依赖 Driver经典的主从模式,程序启动后,会根据用户的代码处理成 Stream Graph,然后优化成为 JobGraph
ExecutionGraph 才是 Flink 真正能执行的数据结构,当很多个 ExecutionGraph 分布在集群中,就会形成一张网状的拓扑结构
容错只支持了 Record 级别的 ACK-FAIL,发送出去的每一条消息,都可以确定是被成功处理或失败处理
因此 Storm 支持 At Least Once 语义
可以配置对应的 checkpoint,也就是保存点。
当任务出现 failover 的时候,会从 checkpoint 重新加载,使得数据不丢失。
但是这个过程会导致原来的数据重复处理,不能做到 Exactly Once 语义
基于两阶段提交实现了精确的 Exactly Once 语义
反压 BackPressure简单粗暴,当下游消费者速度跟不上生产者的速度时会直接通知生产者,生产者停止生产数据构造了一个“速率控制器”,这个“速率控制器”会根据几个属性,如任务的结束时间、处理时长、处理消息的条数等计算一个速率。
在实现控制数据的接收速率中用到了一个经典的算法,即“PID 算法”。
使用了分布式阻塞队列。
我们知道在一个阻塞队列中,当队列满了以后发送者会被天然阻塞住,这种阻塞功能相当于给这个阻塞队列提供了反压的能力。

反压是分布式处理系统中经常遇到的问题,当消费者速度低于生产者的速度时,则需要消费者将信息反馈给生产者,使得生产者的速度能和消费者的速度进行匹配

1.4 DataSet & DataStream

Flink 很重要的一个特点是“流批一体”,然而事实上 Flink 并没有完全做到所谓的“流批一体”,即编写一套代码,可以同时支持流式计算场景和批量计算的场景。
目前截止 1.10 版本依然采用了 DataSet 和 DataStream 两套 API 来适配不同的应用场景

实际情况是,按照Flink的设计理念 用同一个引擎支持多种形式的计算,包括批处理、流处理和机器学习等
Flink 实现了计算**引擎级别的流批一体**,只不过作为开发者来说,依旧要使用两套代码罢了

1.4.1 DataSet API

由于 Flink DataSet 和 DataStream API 的高度相似,并且 Flink 在实时计算领域中应用的更为广泛。所以本文仅详细讲解 DataStream API 的使用

1.4.2 DataStream API

1.4.2.1 自定义实时数据源

在 1.2 节中,我们使用到了 WordCount 案例,通过监听某个 Socket 端口,在5秒滚动窗口内获取数据

那么接下来我们不去监听端口,自己 DIY 一个 能够发出随机数据的数据源:

  • 自定义Source

    import org.apache.flink.streaming.api.functions.source.SourceFunction;
    
    import java.util.Random;
    
    /**
     * @author Archie
     * @date 2021-10-08 6:08
     * @description
     */
    public class MyStreamingSource implements SourceFunction<MyStreamingSource.Item> {
    
       private boolean isRunning = true;
    
       /**
        * 产生一个源源不断的数据发送源
        * @param sourceContext
        * @throws Exception
        */
       @Override
       public void run(SourceContext<Item> sourceContext) throws Exception {
          while(isRunning){
             Item item = generateItem();
             sourceContext.collect(item);
             // 每秒产生一条数据
             Thread.sleep(1000);
          }
       }
    
       @Override
       public void cancel() {
          isRunning = false;
       }
    
       // 随机产生一条数据
       private Item generateItem(){
          int i = new Random().nextInt(100);
          Item item = new Item();
          item.setName("name" + i);
          item.setId(i);
          return item;
       }
    
       public class Item{
          private String name;
          private Integer id;
    
          Item() {
          }
    
          public String getName() {
             return name;
          }
    
          public void setName(String name) {
             this.name = name;
          }
    
          public Integer getId() {
             return id;
          }
    
          public void setId(Integer id) {
             this.id = id;
          }
    
          @Override
          public String toString() {
             return "Item{" +
                   "name='" + name + '\'' +
                   ", id=" + id +
                   '}';
          }
       }
    }
    
  • Main方法

    public static void main(String[] args) throws Exception {
       // 0. 获取运行环境
       StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
       // 1. 获取数据源(并发度为 1)
       DataStreamSource<MyStreamingSource.Item> items = env.addSource(new MyStreamingSource()).setParallelism(1);
       // 2. 过滤
       SingleOutputStreamOperator<MyStreamingSource.Item> filterItems = items.filter(item -> item.getId() % 2 == 0);
       // 3. 打印结果
       filterItems.print().setParallelism(1);
       String jobName = "user defined streaming source";
       env.execute(jobName);
    }
    

接下来,本文会基于该自定义数据源进行 DataStream API 的讲解

1.4.2.2 Map

Map 接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出。

img
public static void main(String[] args) throws Exception {
   // 0. 获取运行环境
   StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
   // 1. 获取数据源(并发度为 1)
   DataStreamSource<MyStreamingSource.Item> items = env.addSource(new MyStreamingSource()).setParallelism(1);
   // 2. Map
   SingleOutputStreamOperator<Object> mapItems = items.map(new MapFunction<MyStreamingSource.Item, Object>() {
      @Override
      public Object map(MyStreamingSource.Item item) throws Exception {
         return item.getName();
      }
   });
   // 3. 打印结果
   mapItems.print().setParallelism(1);
   String jobName = "user defined streaming source";
   env.execute(jobName);
}

注意,Map 算子是最常用的算子之一,官网中的表述是对一个 DataStream 进行映射,每次进行转换都会调用 MapFunction 函数。
从源 DataStream 到目标 DataStream 的转换过程中,返回的是 SingleOutputStreamOperator。
当然了,我们也可以在重写的 map 函数中使用 lambda 表达式。

SingleOutputStreamOperator<Object> mapItems = items.map(item -> item.getName());

甚至,还可以自定义自己的 Map 函数。通过重写 MapFunction 或 RichMapFunction 来自定义自己的 map 函数

    static class MyMapFunction extends RichMapFunction<MyStreamingSource.Item,String> {
        @Override
        public String map(MyStreamingSource.Item item) throws Exception {
            return item.getName();
        }
    }

在 RichMapFunction 中还提供了 open、close 等函数方法,重写这些方法还能实现更为复杂的功能,比如获取累加器、计数器等。

1.4.2.3 FlatMap

FlatMap 接受一个元素,返回零到多个元素
FlatMap 和 Map 有些类似,但是当返回值是列表的时候,FlatMap 会将列表“平铺”,也就是以单个元素的形式进行输出。

SingleOutputStreamOperator<Object> flatMapItems = items.flatMap(new FlatMapFunction<MyStreamingSource.Item, Object>() {
    @Override
    public void flatMap(MyStreamingSource.Item item, Collector<Object> collector) throws Exception {
        String name = item.getName();
        collector.collect(name);
    }
});

1.4.2.4 Filter

顾名思义,Fliter 的意思就是过滤掉不需要的数据,每个元素都会被 filter 函数处理,如果 filter 函数返回 true 则保留,否则丢弃。

SingleOutputStreamOperator<MyStreamingSource.Item> filterItems = items.filter(new FilterFunction<MyStreamingSource.Item>() {
    @Override
    public boolean filter(MyStreamingSource.Item item) throws Exception {
        return item.getId() % 2 == 0;
    }
});

当然,我们也可以在 filter 中使用 lambda 表达式:

SingleOutputStreamOperator<MyStreamingSource.Item> filterItems = items.filter( 
    item -> item.getId() % 2 == 0
);

1.4.2.5 KeyBy

在介绍 KeyBy 函数之前,需要你理解一个概念:KeyedStream

在使用 KeyBy 函数时会把 DataStream 转换成为 KeyedStream,事实上 KeyedStream 继承了 DataStream,KeyedStream 中的元素会根据用户传入的参数进行分组。

在实际业务中,我们经常会需要根据数据的某种属性或者单纯某个字段进行分组,然后对不同的组进行不同的处理。
举个例子,当我们需要描述一个用户画像时,则需要根据用户的不同行为事件进行加权;
再比如,我们在监控双十一的交易大盘时,则需要按照商品的品类进行分组,分别计算销售额。

img

在 1.2 小节 WordCount 中使用过 KeyBy

【注意!】: 在生产环境中使用 KeyBy 函数时要十分注意!该函数会把数据按照用户指定的 key 进行分组,那么相同分组的数据会被分发到一个 subtask 上进行处理,在大数据量和 key 分布不均匀的时非常容易出现数据倾斜和反压,导致任务失败

常见的解决方式是把所有数据加上随机前后缀

1.4.2.6 Aggregations

Aggregations 为聚合函数的总称,常见的聚合函数包括但不限于 sum、max、min 等。
Aggregations 也需要指定一个 key 进行聚合,官网给出了几个常见的例子:

keyedStream.sum(0);
keyedStream.sum("key");
keyedStream.min(0);
keyedStream.min("key");
keyedStream.max(0);
keyedStream.max("key");
keyedStream.minBy(0);
keyedStream.minBy("key");
keyedStream.maxBy(0);
keyedStream.maxBy("key");

max、min、sum 会分别返回最大值、最小值和汇总值;而 minBy 和 maxBy 则会把最小或者最大的元素全部返回

接下来以 max & maxBy 为例说明:

	public static void main(String[] args) throws Exception {
		// 0. 获取运行环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		// 1. 获取数据源
		List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
		data.add(new Tuple3<>(0,1,0));
		data.add(new Tuple3<>(0,1,1));
		data.add(new Tuple3<>(0,2,2));
		data.add(new Tuple3<>(0,1,3));
		data.add(new Tuple3<>(1,2,5));
		data.add(new Tuple3<>(1,2,9));
		data.add(new Tuple3<>(1,2,11));
		data.add(new Tuple3<>(1,2,13));
		// 2. max
		DataStreamSource<MyStreamingSource.Item> items = env.fromCollection(data);
		items.keyBy(0).max(2).printToErr();
		// 3. 打印结果
		String jobName = "user defined streaming source";
		env.execute(jobName);
		/*
			11> (1,2,5)
			11> (1,2,9)
			11> (1,2,11)
			11> (1,2,13)
			12> (0,1,0)
			12> (0,1,1)
			12> (0,1,2)
			12> (0,1,3)
		 */

		// 2. maxBy
		DataStreamSource<MyStreamingSource.Item> items02 = env.fromCollection(data);
		items02.keyBy(0).maxBy(2).printToErr();
		// 3. 打印结果
		jobName = "user defined streaming source";
		env.execute(jobName);
		/*
			11> (1,2,5)
			11> (1,2,9)
			11> (1,2,11)
			11> (1,2,13)
			12> (0,1,0)
			12> (0,1,1)
			12> (0,2,2)
			12> (0,1,3)
		 */

1.4.2.7 Reduce

Reduce 函数的原理是,会在每一个分组的 keyedStream 上生效,它会按照用户自定义的聚合逻辑进行分组聚合

img
List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
data.add(new Tuple3<>(0,1,0));
data.add(new Tuple3<>(0,1,1));
data.add(new Tuple3<>(0,2,2));
data.add(new Tuple3<>(0,1,3));
data.add(new Tuple3<>(1,2,5));
data.add(new Tuple3<>(1,2,9));
data.add(new Tuple3<>(1,2,11));
data.add(new Tuple3<>(1,2,13));
DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);

SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> reduce = items.keyBy(0).reduce(new ReduceFunction<Tuple3<Integer, Integer, Integer>>() {
    /**
     * 按照第一个元素进行分组,第三个元素分别求和,并且把第一个和第二个元素都置为 0
     */
    @Override
    public Tuple3<Integer,Integer,Integer> reduce(Tuple3<Integer, Integer, Integer> t1, Tuple3<Integer, Integer, Integer> t2) throws Exception {
        Tuple3<Integer,Integer,Integer> newTuple = new Tuple3<>();
        newTuple.setFields(0,0,(Integer)t1.getField(2) + (Integer) t2.getField(2));
        return newTuple;
    }

});

reduce.printToErr().setParallelism(1);
/*
(0,0,6) 和 (0,0,38)
*/

事实上 DataStream 的 API 远远不止这些,在看官方文档的时候要动手去操作验证一下,更为高级的 API 将会在实战课中用到的时候着重进行讲解

1.5 Flink SQL & Table

1.5.1 背景

Flink SQL 是 Flink 实时计算为简化计算模型,降低用户使用实时计算门槛而设计的一套符合标准 SQL 语义的开发语言

正是因为 Flink Table & SQL 的加入,可以说 Flink 在某种程度上做到了事实上的批流一体。

1.5.2 原理

你之前可能都了解过 Hive,在离线计算场景下 Hive 几乎扛起了离线数据处理的半壁江山。
它的底层对 SQL 的解析用到了 Apache Calcite /ˈkælsaɪt/,Flink 同样把 SQL 的解析、优化和执行教给了 Calcite。

img

从图中可以看到无论是「批查询 SQL」 还是「流式查询 SQL」,都会经过对应的转换器 Parser 转换成为节点树 SQLNode tree,然后生成逻辑执行计划 Logical Plan,逻辑执行计划在经过优化后生成真正可以执行的物理执行计划,交给 DataSet 或者 DataStream 的 API 去执行。

一个完整的 Flink Table & SQL Job 也是由 Source、Transformation、Sink 构成:

img
  • Source 部分来源于外部数据源,我们经常用的有 Kafka、MySQL 等;
  • Transformation 部分则是 Flink Table & SQL 支持的常用 SQL 算子,比如简单的 Select、Groupby 等,当然在这里也有更为复杂的多流 Join、流与维表的 Join 等;
  • Sink 部分是指的结果存储比如 MySQL、HBase 或 Kakfa 等。

1.5.3 动态表

与传统的表 SQL 查询相比,Flink Table & SQL 在处理流数据时会时时刻刻处于动态的数据变化中,所以便有了一个动态表的概念

动态表的查询与静态表一样,但是,在查询动态表的时候,SQL 会做连续查询,不会终止。

1.5.4 常用算子 & 内置函数

Flink Table & SQL 的开发一直在进行中,并没有支持所有场景下的计算逻辑
在使用原生的 Flink Table & SQL 时,务必查询官网当前版本对 Table & SQL 的支持程度,尽量选择场景明确,逻辑不是极其复杂的场景。

Flink SQL 和传统的 SQL 一样,支持了包含查询连接聚合等场景,另外还支持了包括窗口排序等场景

1.5.4.1 SELECT / AS / WHERE

和传统 SQL 用法一样,用于筛选和过滤数据

SELECT name,age FROM Table where name LIKE '%小明%';
SELECT * FROM Table WHERE age = 20;
SELECT name, age
FROM Table
WHERE name IN (SELECT name FROM Table2)

1.5.4.2 GROUP BY / DISTINCT / HAVING

SELECT DISTINCT name FROM Table;
SELECT name, SUM(score) as TotalScore FROM Table GROUP BY name;
SELECT name, SUM(score) as TotalScore FROM Table GROUP BY name HAVING
SUM(score) > 300;

1.5.4.3 JOIN

SELECT *
FROM User LEFT JOIN Product ON User.name = Product.buyer

SELECT *
FROM User RIGHT JOIN Product ON User.name = Product.buyer

SELECT *
FROM User FULL OUTER JOIN Product ON User.name = Product.buyer

1.5.4.4 WINDOW

  • 滚动窗口,窗口数据有固定的大小,窗口中的数据不会叠加;

    语法

    SELECT 
        [gk],
        [TUMBLE_START(timeCol, size)], 
        [TUMBLE_END(timeCol, size)], 
        agg1(col1), 
        ... 
        aggn(colN)
    FROM Tab1
    GROUP BY [gk], TUMBLE(timeCol, size)
    

    举例:计算每个用户每天的订单数量

    SELECT 
    	user, 
    	TUMBLE_START(timeLine, INTERVAL '1' DAY) as winStart,
    	SUM(amount) 
    FROM Orders 
    GROUP BY TUMBLE(timeLine, INTERVAL '1' DAY), user;
    
    • TUMBLE_START 和 TUMBLE_END 代表窗口的开始时间和窗口的结束时间
    • TUMBLE (timeLine, INTERVAL '1' DAY) 中的 timeLine 代表时间字段所在的列
    • INTERVAL '1' DAY 表示时间间隔为一天
  • 滑动窗口,窗口数据有固定大小,并且有生成间隔;

    语法

    SELECT 
        [gk], 
        [HOP_START(timeCol, slide, size)] ,
        [HOP_END(timeCol, slide, size)],
        agg1(col1), 
        ... 
        aggN(colN) 
    FROM Tab1
    GROUP BY [gk], HOP(timeCol, slide, size)
    

    举例:每间隔一小时计算一次过去 24 小时内每个商品的销量

    SELECT 
        product, 
        SUM(amount) 
    FROM Orders 
    GROUP BY HOP(rowtime, INTERVAL '1' HOUR, INTERVAL '1' DAY), product
    
    • INTERVAL '1' HOUR 代表滑动窗口生成的时间间隔
  • 会话窗口,窗口数据没有固定的大小,根据用户传入的参数进行划分,窗口数据无叠加;

    语法

    SELECT 
        [gk], 
        SESSION_START(timeCol, gap) AS winStart,
        SESSION_END(timeCol, gap) AS winEnd,
        agg1(col1),
         ... 
        aggn(colN)
    FROM Tab1
    GROUP BY [gk], SESSION(timeCol, gap)
    

    举例:计算每个用户过去 1 小时内的订单量

    SELECT 
        user, 
        SESSION_START(rowtime, INTERVAL '1' HOUR) AS sStart, 
        SESSION_ROWTIME(rowtime, INTERVAL '1' HOUR) AS sEnd, 
        SUM(amount) 
    FROM Orders 
    GROUP BY SESSION(rowtime, INTERVAL '1' HOUR), user
    

1.5.4.5 比较函数

img

1.5.4.6 逻辑函数

img

1.5.4.7 算数函数

img

1.5.4.8 字符串处理函数

img

1.5.4.9 时间函数

img

1.6 集群部署(HA)

1.6.0 环境准备

在绝大多数情况下,我们的 Flink 都是运行在 Unix 环境中的,推荐在 Mac OS 或者 Linux 环境下运行 Flink。如果是集群模式,那么可以在自己电脑上安装虚拟机,保证有一个 master 节点和两个 slave 节点。

同时,要注意在所有的机器上都应该安装 JDK 和 SSH。JDK 是我们运行 JVM 语言程序必须的,而 SSH 是为了在服务器之间进行跳转和执行命令所必须的。关于服务器之间通过 SSH 配置公钥登录,你可以直接搜索安装和配置方法,我们不做过度展开。

Flink 的安装包可以在这里下载。需要注意的是,如果你要和 Hadoop 进行集成,那么我们需要使用到对应的 Hadoop 依赖,下面将会详细讲解。

1.6.1 Local模式

Local 模式是 Flink 提供的最简单部署模式,一般用来本地测试和演示使用。

我们在这里下载 Apache Flink 1.10.0 for Scala 2.11 版本进行演示,该版本对应 Scala 2.11 版本。

将压缩包下载到本地,并且直接进行解压,使用 Flink 默认的端口配置,直接运行脚本启动:tar -zxvf flink-1.10.0-bin-scala_2.11.tgz

img

然后,我们可以直接运行脚本启动 Flink :./bin/start-cluster.sh

img

访问本地的 8081 端口,可以看到 Flink 的后台管理界面,验证 Flink 是否成功启动

img

尝试提交一个测试任务:./bin/flink run examples/batch/WordCount.jar

img

同样,在 Flink 的后台管理界面 Completed Jobs 一栏可以看到刚才提交执行的程序:

img

1.6.2 Standalone 模式

Standalone 模式是集群模式的一种,但是这种模式一般并不运行在生产环境中,原因和 on yarn 模式相比:

  • Standalone 模式的部署相对简单,可以支持小规模,少量的任务运行;
  • Stabdalone 模式缺少系统层面对集群中 Job 的管理,容易遭成资源分配不均匀;
  • 资源隔离相对简单,任务之间资源竞争严重。

工作中用的不多,读者可需要用到时可以自行度娘查看教程

1.6.3 On Yarn 模式和 HA 配置

img

Yarn 是 Hadoop 三驾马车之一,主要用来做资源管理。我们在 Flink on Yarn 模式中也是借助 Yarn 的资源管理优势,需要在三个节点中配置 YARN_CONF_DIR、HADOOP_CONF_DIR、HADOOP_CONF_PATH 中的任意一个环境变量即可

本课时中集群的高可用 HA 配置是基于独立的 ZooKeeper 集群。
当然,Flink 本身提供了内置 ZooKeeper 插件,可以直接修改 conf/zoo.cfg,并且使用 /bin/start-zookeeper-quorum.sh 直接启动

这里咱们使用 5 台虚拟机搭建 on yarn 的高可用集群:

img
  1. 添加环境变量:

    vi /etc/profile
    # 添加环境变量
    export HADOOP_CONF_DIR=/Software/hadoop-2.6.5/etc/hadoop
    # 环境变量生效
    source /etc/profile
    
  2. 下载对应的的依赖包,并将对应的 Hadoop 依赖复制到 flink 的 lib 目录下,对应的 hadoop 依赖可以在这里下载。

    img
  3. 修改 flink-conf.yaml 文件中的一些配置:

    # 高可用实现技术
    high-availability: zookeeper
    # 数据持久化地址
    high-availability.storageDir: hdfs://cluster/flinkha/
    # ZK集群地址
    high-availability.zookeeper.quorum: slave01:2181,slave02:2181,slave03:2181
    
  4. 分别修改 master、slave、zoo.cfg 三个配置文件

    1. master 文件:

      master01:8081
      master02:8081
      
    2. slave文件:

      slave01
      slave02
      slave03
      
    3. zoo.cfg文件:

      server.1=slave01:2888:3888
      server.2=slave02:2888:3888
      server.3=slave03:2888:3888
      
  5. 将整个修改好的 Flink 解压目录使用 scp 远程拷贝命令发送到从节点

    scp -r /SoftWare/flink-1.10.0 slave01:/SoftWare/
    scp -r /SoftWare/flink-1.10.0 slave02:/SoftWare/
    scp -r /SoftWare/flink-1.10.0 slave03:/SoftWare/
    
  6. 分别启动 Hadoop 和 ZooKeeper,然后在主节点,使用命令启动集群:

    /SoftWare/flink-1.10.0/bin/start-cluster.sh
    
  7. 同样直接访问 http://192.168.2.100:8081/ 端口,可以看到 Flink 的后台管理界面,验证 Flink 是否成功启动


任务启动方式

  1. 直接在 yarn 上运行任务

    相当于将 job 直接提交到 yarn 上,每个任务会根据用户的指定进行资源申请,任务之间互不影响

    ./bin/flink run -yjm 1024m -ytm 4096m -ys 2  ./examples/batch/WordCount.jar
    
  2. yarn session 模式

    需要先启动一个 yarn-session 会话,相当于启动了一个 yarn 任务,这个任务所占用的资源不会变化,并且一直运行。
    我们在使用 flink run 向这个 session 任务提交作业时,如果 session 的资源不足,那么任务会等待,直到其他资源释放。
    当这个 yarn-session 被杀死时,所有任务都会停止。

    1. 例如我们启动一个 yarn session 任务,该任务拥有 8G 内存、32 个槽位。

      ./bin/yarn-session.sh -tm 8192 -s 32
      
    2. 我们在 yarn 的界面上可以看到这个任务的 ID,然后向这个 session ID 提交 Flink 任务

      # application_xxxx 即为上述的 yarn session 任务 ID
      ./bin/flink run -m yarn-cluster -yid application_xxxx ./examples/batch/WordCount.jar