大数据篇---Flink学习

原创
2021/04/29 10:31
阅读数 923

第一部分 Flink 概述

第 1 节 什么是 Flink

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

  • Flink起源于2008年柏林理工大学的研究性项目Stratosphere

  • 2014年该项目被捐赠给了Apache软件基金会

  • Flink一跃成为Apache软件基金会的顶级项目之一

第 2 节 Flink 特点

Flink 是一个开源的流处理框架,它具有以下特点

  • 批流一体:统一批处理、流处理

  • 分布式:Flink程序可以运行在多台机器上

  • 高性能:处理性能比较高

  • 高可用:Flink支持高可用性(HA)

  • 准确:Flink可以保证数据处理的准确性

第 3 节 Flink 应用场景

Flink主要应用于流式数据分析场景

数据无处不在,绝大多数的企业所采取的处理数据的架构都会划分成两类:事务型处理、分析型处理

事务型处理

OLTP On-Line Transaction Processing :联机事务处理过程。

流程审批、数据录入、填报等

特点:线下工作线上化,数据保存在各自的系统中,互不相通(数据孤岛)

OLTP:联机事务处理系统是一种以事务元作为数据处理的单位、人机交互的计算机应用系统。

它能对数据进行即时更新其他操作,系统内的数据总是保持在最新状态。

用户可将<u>一组保持数据一致性的操作序列</u>指定为一个事务元,通过终端、个人计算机或其他设备输入事务元,经系统处理后返回结果,

OLTP主要用来记录某类业务事件的发生,如购买行为,当行为产生后,系统会记录是谁在何时何地做了何事,这样的一行(或多行)数据会以增删改的方式在数据库中进行数据的更新处理操作,要求实时性高、稳定性强、确保数据及时更新成功。

应用于飞机订票、银行出纳、股票交易、超市销售、饭店前后管理等实时系统

比如公司常见的业务系统如ERP,CRM,OA等系统都属于OLTP

ERP: Enterprise Resource Planning 企业资源计划

CRM:Customer Relationship Management 客户关系管理

OA:Office Automation 办公自动化

image-20200914170700254

期间没处理一条事件,应用都会通过执行远程数据库系统的事务来读取或更新状态。很多时候,多个应用会共享同一个数据库系统,有时候还会访问相同的数据库或表。

该设计在应用需要更新数据库扩缩容或更改表模式的时候容易导致问题。

分析型处理

当数据积累到一定的程度,我们需要对过去发生的事情做一个总结分析时,就需要把过去一段时间内产生的数据拿出来进行统计分析,从中获取我们想要的信息,为公司做决策提供支持,这时候就是在做OLAP了。

因为OLTP所产生的业务数据分散在不同的业务系统中,而OLAP往往需要将不同的业务数据集中到一起进行统一综合的分析,这时候就需要根据业务分析需求做对应的数据清洗后存储在数据仓库中,然后由数据仓库来统一提供OLAP分析

OLAP On-Line Analytical Processing :联机分析系统

分析报表,分析决策等

根据业务分析需求做对应的数据清洗后存储在数据仓库中称为ETL

ETL:Extract-Transform-Load: 从事务型数据库中提取数据,将其转化成通用的表示形式(可能包含数据验证,数据归一化,编码、去重、表模式转化等工作),最终加载到分析型数据库中。

OLAP的实现方案一:(数仓)

2717543-04110b9fe00113a6

如上图所示,数据实时写入 HBase,实时的数据更新也在 HBase 完成,为了应对 OLAP 需求,我们定时(通常是 T+1 或者 T+H)将 HBase 数据写成静态的文件(如:Parquet)导入到 OLAP 引擎(如:HDFS,比较常见的是Impala操作Hive)。这一架构能满足既需要随机读写,又可以支持 OLAP 分析的场景,但他有如下缺点:

  • 架构复杂。从架构上看,数据在 HBase、消息队列、HDFS 间流转,涉及环节太多,运维成本很高。并且每个环节需要保证高可用,都需要维护多个副本,存储空间也有一定的浪费。最后数据在多个系统上,对数据安全策略、监控等都提出了挑战。
  • 时效性低。数据从 HBase 导出成静态文件是周期性的,一般这个周期是一天(或一小时),在时效性上不是很高。
  • 难以应对后续的更新。真实场景中,总会有数据是「延迟」到达的。如果这些数据之前已经从 HBase 导出到 HDFS,新到的变更数据就难以处理了,一个方案是把原有数据应用上新的变更后重写一遍,但这代价又很高。

通常数据仓库中的查询可以分为两类:

1、普通查询:是定制的

2、即系查询:是用户自定义查询条件的

image-20200914153215779

  • 实时ETL

    集成流计算现有的诸多数据通道和SQL灵活的加工能力,对流式数据进行实时清洗、归并和结构化处理;同时,对离线数仓进行有效的补充和优化,并为数据实时传输提供可计算通道。

  • 实时报表

    实时化采集、加工流式数据存储;实时监控和展现业务、客户各类指标,让数据化运营实时化。

    如通过分析订单处理系统中的数据获知销售增长率;

    通过分析分析运输延迟原因或预测销售量调整库存;

  • 监控预警

    对系统和用户行为进行实时监测和分析,以便及时发现危险行为,如计算机网络入侵、诈骗预警等

  • 在线系统

    实时计算各类数据指标,并利用实时结果及时调整在线系统的相关策略,在各类内容投放、智能推送领域有大量的应用,如在客户浏览商品的同时推荐相关商品等

第 4 节 Flink 核心组成及生态发展

Flink核心组成

image-20200722171959131

  • Deploy层:
    • 可以启动单个JVM,让Flink以Local模式运行
    • Flink也可以以Standalone 集群模式运行,同时也支持Flink ON YARN,Flink应用直接提交到YARN上面运行
    • Flink还可以运行在GCE(谷歌云服务)和EC2(亚马逊云服务)
  • Core层(Runtime):在Runtime之上提供了两套核心的API,DataStream API(流处理)和DataSet API(批处理)
  • APIs & Libraries层:核心API之上又扩展了一些高阶的库和API
    • CEP流处理
    • Table API和SQL
    • Flink ML机器学习库
    • Gelly图计算

Flink生态发展

image-20200722171657610

  • 中间部分主要内容在上面Flink核心组成中已经提到

  • 输入Connectors(左侧部分)

    流处理方式:包含Kafka(消息队列)、AWS kinesis(实时数据流服务)、RabbitMQ(消息队列)、NIFI(数据管道)、Twitter(API)

    批处理方式:包含HDFS(分布式文件系统)、HBase(分布式列式数据库)、Amazon S3(文件系统)、MapR FS(文件系统)、ALLuxio(基于内存分布式文件系统)

  • 输出Connectors(右侧部分)

    流处理方式:包含Kafka(消息队列)、AWS kinesis(实时数据流服务)、RabbitMQ(消息队列)、NIFI(数据管道)、Cassandra(NOSQL数据库)、ElasticSearch(全文检索)、HDFS rolling file(滚动文件)

    批处理方式:包含HBase(分布式列式数据库)、HDFS(分布式文件系统)

第 5 节 Flink 处理模型:流处理与批处理

​ Flink 专注于无限流处理,有限流处理是无限流处理的一种特殊情况

img

无限流处理:

  • 输入的数据没有尽头,像水流一样源源不断
  • 数据处理从当前或者过去的某一个时间 点开始,持续不停地进行

有限流处理:

  • 从某一个时间点开始处理数据,然后在另一个时间点结束

  • 输入数据可能本身是有限的(即输入数据集并不会随着时间增长),也可能出于分析的目的被人为地设定为有限集(即只分析某一个时间段内的事件)

    Flink封装了DataStream API进行流处理,封装了DataSet API进行批处理。

    同时,Flink也是一个批流一体的处理引擎,提供了Table API / SQL统一了批处理和流处理

有状态的流处理应用:

image-20200915183019028

第 6 节 流处理引擎的技术选型

​ 市面上的流处理引擎不止Flink一种,其他的比如Storm、SparkStreaming、Trident等,实际应用时如何进行选型,给大家一些建议参考

  • 流数据要进行状态管理,选择使用Trident、Spark Streaming或者Flink
  • 消息投递需要保证At-least-once(至少一次)或者Exactly-once(仅一次)不能选择Storm
  • 对于小型独立项目,有低延迟要求,可以选择使用Storm,更简单
  • 如果项目已经引入了大框架Spark,实时处理需求可以满足的话,建议直接使用Spark中的Spark Streaming
  • 消息投递要满足Exactly-once(仅一次),数据量大、有高吞吐、低延迟要求,要进行状态管理或窗口统计,建议使用Flink

第二部分 Flink快速应用

​ 通过一个单词统计的案例,快速上手应用Flink,进行流处理(Streaming)和批处理(Batch)

第 1 节 单词统计案例(批数据)

1.1 需求

​ 统计一个文件中各个单词出现的次数,把统计结果输出到文件

步骤:

1、读取数据源

2、处理数据源

a、将读到的数据源文件中的每一行根据空格切分

b、将切分好的每个单词拼接1

c、根据单词聚合(将相同的单词放在一起)

d、累加相同的单词(单词后面的1进行累加)

3、保存处理结果

1.2 代码实现

  • 引入依赖

    <dependencies>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-java -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-java</artifactId>
                <version>1.11.1</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-java -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-java_2.12</artifactId>
                <version>1.11.1</version>
                <scope>provided</scope>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-clients_2.12</artifactId>
                <version>1.11.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-scala -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-scala_2.12</artifactId>
                <version>1.11.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-scala_2.12</artifactId>
                <version>1.11.1</version>
                <scope>provided</scope>
            </dependency>	
    
  • Java程序

    package com.lagou.edu.batch;
    
    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.tuple.Tuple2;
    import org.apache.flink.util.Collector;
    
    /**
     * 单词统计(批数据处理)
     */
    public class WordCount {
        public static void main(String[] args) throws Exception {
            // 输入路径和出入路径通过参数传入,约定第一个参数为输入路径,第二个参数为输出路径
            String inPath = args[0];
            String outPath = args[1];
            // 获取Flink批处理执行环境
            ExecutionEnvironment executionEnvironment = ExecutionEnvironment.getExecutionEnvironment();
            // 获取文件中内容
            DataSet<String> text = executionEnvironment.readTextFile(inPath);
            // 对数据进行处理
            DataSet<Tuple2<String, Integer>> dataSet = text.flatMap(new LineSplitter()).groupBy(0).sum(1);
            dataSet.writeAsCsv(outputFile,"\n","").setParallelism(1);
            // 触发执行程序
            executionEnvironment.execute("wordcount batch process");
        }
    
    
        static class LineSplitter implements FlatMapFunction<String, Tuple2<String,Integer>> {
            @Override
            public void flatMap(String line, Collector<Tuple2<String, Integer>> collector) throws Exception {
                for (String word:line.split(" ")) {
                    collector.collect(new Tuple2<>(word,1));
                }
            }
        }
    }
    
<properties>    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    <maven.compiler.source>11</maven.compiler.source>    <maven.compiler.target>11</maven.compiler.target></properties>

第 2 节 单词统计案例(流数据)

nc

netcat:

2.1 需求

​ Socket模拟实时发送单词,使用Flink实时接收数据,对指定时间窗口内(如5s)的数据进行聚合统计,每隔1s汇总计算一次,并且把时间窗口内计算结果打印出来。

2.2 代码实现

package com.lagou.edu.stream;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
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;

/**
 * 	Socket模拟实时发送单词,使用Flink实时接收数据,对指定时间窗口内(如5s)的数据进行聚合统计,每隔1s汇总计算一次,并且把时间窗口内计算结果打印出来。
 teacher2 ip : 113.31.105.128
 */
public class WordCount {

    public static void main(String[] args) throws Exception {
        // 监听的ip和端口号,以main参数形式传入,约定第一个参数为ip,第二个参数为端口
        String ip = args[0];
        int port = Integer.parseInt(args[1]);
        // 获取Flink流执行环境
        StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        // 获取socket输入数据
        DataStreamSource<String> textStream = streamExecutionEnvironment.socketTextStream(ip, port, "\n");

        SingleOutputStreamOperator<Tuple2<String, Long>> tuple2SingleOutputStreamOperator = textStream.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String s, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] splits = s.split("\\s");
                for (String word : splits) {
                    collector.collect(Tuple2.of(word, 1l));
                }
            }
        });

        SingleOutputStreamOperator<Tuple2<String, Long>> word = tuple2SingleOutputStreamOperator.keyBy("word")
                .timeWindow(Time.seconds(2), Time.seconds(1))
                .sum(1);
        // 打印数据
        word.print();
        // 触发任务执行
        streamExecutionEnvironment.execute("wordcount stream process");

    }
}

​ Flink程序开发的流程总结如下:

1)获得一个执行环境

2)加载/创建初始化数据

3)指定数据操作的算子

4)指定结果数据存放位置

5)调用execute()触发执行程序

注意:Flink程序是延迟计算的,只有最后调用execute()方法的时候才会真正触发执行程序

第三部分 Flink体系结构

第 1 节 Flink的重要角色

​ Flink是非常经典的Master/Slave结构实现,JobManager是Master,TaskManager是Slave。

  • JobManager处理器(Master)

    • 协调分布式执行,它们用来调度task,协调检查点(CheckPoint),协调失败时恢复等

    • Flink运行时至少存在一个master处理器,如果配置高可用模式则会存在多个master处理器,它们其中有一个是leader,而其他的都是standby。

  • JobManager接收的应用包括jar和JobGraph

  • TaskManager处理器(Slave)

    ​ 也称之为Worker

    • 主要职责是从JobManager处接收任务, 并部署和启动任务, 接收上游的数据并处理
    • Task Manager 是在 JVM 中的一个或多个线程中执行任务的工作节点
    • TaskManager在启动的时候会向ResourceManager注册自己的资源信息(Slot的数量等)
  • ResourceManager

    针对不同的环境和资源提供者,如(YARN,Me搜索,Kubernetes或独立部署),Flink提供了不同的ResourceManager

    作用:负责管理Flink的处理资源单元---Slot

  • Dispatcher:

    作用:提供一个REST接口来让我们提交需要执行的应用。

    一旦一个应用提交执行,Dispatcher会启动一个JobManager,并将应用转交给他。

    Dispatcher还会启动一个webUI来提供有关作业执行信息

    注意:某些应用的提交执行的方式,有可能用不到Dispatcher

各个组件之间的关系:

image-20200917154621166

第 2 节 Flink运行架构

2.1 Flink程序结构

​ Flink程序的基本构建块是转换(请注意,Flink的DataSet API中使用的DataSet也是内部流 )。从概念上讲,流是(可能永无止境的)数据记录流,而转换是将一个或多个流输入,并产生一个或多个输出流。

image-20200731001906709

​ 上图表述了Flink的应用程序结构,有Source(源头)、Transformation(转换)、Sink(接收器)三个重要组成部分

  • Source

    ​ 数据源,定义Flink从哪里加载数据,Flink 在流处理和批处理上的 source 大概有 4 类:基于本地集合的 source、基于文件的 source、基于网络套接字的 source、自定义的 source。自定义的 source 常见的有 Apache kafka、RabbitMQ 等。

  • Transformation

    ​ 数据转换的各种操作,也称之为算子,有 Map / FlatMap / Filter / KeyBy / Reduce / Window等,可以将数据转换计算成你想要的数据。

  • Sink

    ​ 接收器,Flink 将转换计算后的数据发送的地点 ,定义了结果数据的输出方向,Flink 常见的 Sink 大概有如下几类:写入文件、打印出来、写入 socket 、自定义的 sink 。自定义的 sink 常见的有 Apache kafka、RabbitMQ、MySQL、ElasticSearch、Apache Cassandra、HDFS等。

2.2 Task和SubTask

  • Task 是一个阶段多个功能相同 SubTask 的集合,类似于 Spark 中的 TaskSet。

  • SubTask(子任务)

    ​ SubTask 是 Flink 中任务最小执行单元,是一个 Java 类的实例,这个 Java 类中有属性和方法,完成具体的计算逻辑

    ​ 比如一个执行操作map,分布式的场景下会在多个线程中同时执行,每个线程中执行的都叫做一个SubTask(在2.3节的图中也能够体现)

2.3 Operator chain(操作器链)

​ Flink的所有操作都称之为Operator,客户端在提交任务的时候会对Operator进行优化操作,能进行合并的Operator会被合并为一个Operator,合并后的Operator称为Operator chain,实际上就是一个执行链,每个执行链会在TaskManager上一个独立的线程中执行。shuffle

image-20200731002729966

2.4 Flink中的数据传输

在运行过程中,应用中的任务会持续进行数据交换。

为了有效利用网络资源和提高吞吐量,Flink在处理任务间的数据传输过程中,采用了缓冲区机制。

2.5 任务槽和槽共享

​ 任务槽也叫做task-slot、槽共享也叫做slot sharing

· 每个TaskManager是一个JVM的进程, 可以在不同的线程中执行一个或多个子任务。

​ 为了控制一个worker能接收多少个task。worker通过task slot来进行控制(一个worker至少有一个task slot)

  • 任务槽

    ​ 每个task slot表示TaskManager拥有资源的一个固定大小的子集。 一般来说:我们分配槽的个数都是和CPU的核数相等,比如6核,那么就分配6个槽.

    ​ Flink将进程的内存进行了划分到多个Slot中。假设一个TaskManager机器有3个slot,那么每个slot占有1/3的内存(平分)。

    ​ 内存被划分到不同的slot之后可以获得如下好处:

    • TaskManager最多能同时并发执行的任务是可以控制的,那就是3个,因为不能超过slot的数量
    • slot有独占的内存空间,这样在一个TaskManager中可以运行多个不同的作业,作业之间不受影响
  • 槽共享

    ​ 默认情况下,Flink允许子任务subtast(map[1] map[2] keyby[1] keyby[2] 共享插槽,即使它们是不同任务的子任务,只要它们来自同一个作业。结果是一个槽可以保存作业的整个管道。

第四部分 Flink安装和部署

​ Flink支持多种安装模式

  • local(本地):单机模式,一般本地开发调试使用
  • StandAlone 独立模式:Flink自带集群,自己管理资源调度,生产环境也会有所应用
  • Yarn模式:计算资源统一由Hadoop YARN管理,生产环境应用较多

第 1 节 环境准备工作

1.1 基础环境

  • jdk1.8及以上【配置JAVA_HOME环境变量】

  • ssh免密码登录【集群内节点之间免密登录】

1.2 安装包下载

配套资料文件夹中提供,使用Flink1.11.1版本

1.3 集群规划

hdp-1 hdp-2 hdp-3
JobManager+TaskManager TaskManager TaskManager

1.4 StandAlone模式部署

Step1、Flink安装包上传到hdp-1对应目录并解压

Step2、修改 flink/conf/flink-conf.yaml 文件

 jobmanager.rpc.address: hdp-1
 taskmanager.numberOfTaskSlots: 2

Step3、修改 /conf/slave文件

hdp-1
hdp-2
hdp-3

Step4、standalone模式启动

bin目录下执行./start-cluster.sh

Step5、jps进程查看核实

3857 TaskManagerRunner
3411 StandaloneSessionClusterEntrypoint
3914 Jps

Step6、查看Flink的web页面 ip:8081/#/overview

web

Step7、集群模式下运行example测试

./flink run ../examples/streaming/WordCount.jar
./flink <ACTION> [OPTIONS] [ARGUMENTS]

The following actions are available:

Action "run" compiles and runs a program.

  Syntax: run [OPTIONS] <jar-file> <arguments>
  "run" action options:
     -c,--class <classname>               Class with the program entry point
                                          ("main()" method). Only needed if the
                                          JAR file does not specify the class in
                                          its manifest.
     -C,--classpath <url>                 Adds a URL to each user code
                                          classloader  on all nodes in the
                                          cluster. The paths must specify a
                                          protocol (e.g. file://) and be
                                          accessible on all nodes (e.g. by means
                                          of a NFS share). You can use this
                                          option multiple times for specifying
                                          more than one URL. The protocol must
                                          be supported by the {@link
                                          java.net.URLClassLoader}.
     -d,--detached                        If present, runs the job in detached
                                          mode
     -n,--allowNonRestoredState           Allow to skip savepoint state that
                                          cannot be restored. You need to allow
                                          this if you removed an operator from
                                          your program that was part of the
                                          program when the savepoint was
                                          triggered.
     -p,--parallelism <parallelism>       The parallelism with which to run the
                                          program. Optional flag to override the
                                          default value specified in the
                                          configuration.
     -py,--python <pythonFile>            Python script with the program entry
                                          point. The dependent resources can be
                                          configured with the `--pyFiles`
                                          option.
     -pyarch,--pyArchives <arg>           Add python archive files for job. The
                                          archive files will be extracted to the
                                          working directory of python UDF
                                          worker. Currently only zip-format is
                                          supported. For each archive file, a
                                          target directory be specified. If the
                                          target directory name is specified,
                                          the archive file will be extracted to
                                          a name can directory with the
                                          specified name. Otherwise, the archive
                                          file will be extracted to a directory
                                          with the same name of the archive
                                          file. The files uploaded via this
                                          option are accessible via relative
                                          path. '#' could be used as the
                                          separator of the archive file path and
                                          the target directory name. Comma (',')
                                          could be used as the separator to
                                          specify multiple archive files. This
                                          option can be used to upload the
                                          virtual environment, the data files
                                          used in Python UDF (e.g.: --pyArchives
                                          file:///tmp/py37.zip,file:///tmp/data.
                                          zip#data --pyExecutable
                                          py37.zip/py37/bin/python). The data
                                          files could be accessed in Python UDF,
                                          e.g.: f = open('data/data.txt', 'r').
     -pyexec,--pyExecutable <arg>         Specify the path of the python
                                          interpreter used to execute the python
                                          UDF worker (e.g.: --pyExecutable
                                          /usr/local/bin/python3). The python
                                          UDF worker depends on Python 3.5+,
                                          Apache Beam (version == 2.19.0), Pip
                                          (version >= 7.1.0) and SetupTools
                                          (version >= 37.0.0). Please ensure
                                          that the specified environment meets
                                          the above requirements.
     -pyfs,--pyFiles <pythonFiles>        Attach custom python files for job.
                                          These files will be added to the
                                          PYTHONPATH of both the local client
                                          and the remote python UDF worker. The
                                          standard python resource file suffixes
                                          such as .py/.egg/.zip or directory are
                                          all supported. Comma (',') could be
                                          used as the separator to specify
                                          multiple files (e.g.: --pyFiles
                                          file:///tmp/myresource.zip,hdfs:///$na
                                          menode_address/myresource2.zip).
     -pym,--pyModule <pythonModule>       Python module with the program entry
                                          point. This option must be used in
                                          conjunction with `--pyFiles`.
     -pyreq,--pyRequirements <arg>        Specify a requirements.txt file which
                                          defines the third-party dependencies.
                                          These dependencies will be installed
                                          and added to the PYTHONPATH of the
                                          python UDF worker. A directory which
                                          contains the installation packages of
                                          these dependencies could be specified
                                          optionally. Use '#' as the separator
                                          if the optional parameter exists
                                          (e.g.: --pyRequirements
                                          file:///tmp/requirements.txt#file:///t
                                          mp/cached_dir).
     -s,--fromSavepoint <savepointPath>   Path to a savepoint to restore the job
                                          from (for example
                                          hdfs:///flink/savepoint-1537).
     -sae,--shutdownOnAttachedExit        If the job is submitted in attached
                                          mode, perform a best-effort cluster
                                          shutdown when the CLI is terminated
                                          abruptly, e.g., in response to a user
                                          interrupt, such as typing Ctrl + C.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "info" shows the optimized execution plan of the program (JSON).

  Syntax: info [OPTIONS] <jar-file> <arguments>
  "info" action options:
     -c,--class <classname>           Class with the program entry point
                                      ("main()" method). Only needed if the JAR
                                      file does not specify the class in its
                                      manifest.
     -p,--parallelism <parallelism>   The parallelism with which to run the
                                      program. Optional flag to override the
                                      default value specified in the
                                      configuration.


Action "list" lists running and scheduled programs.

  Syntax: list [OPTIONS]
  "list" action options:
     -a,--all         Show all programs and their JobIDs
     -r,--running     Show only running programs and their JobIDs
     -s,--scheduled   Show only scheduled programs and their JobIDs
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "stop" stops a running program with a savepoint (streaming jobs only).

  Syntax: stop [OPTIONS] <Job ID>
  "stop" action options:
     -d,--drain                           Send MAX_WATERMARK before taking the
                                          savepoint and stopping the pipelne.
     -p,--savepointPath <savepointPath>   Path to the savepoint (for example
                                          hdfs:///flink/savepoint-1537). If no
                                          directory is specified, the configured
                                          default will be used
                                          ("state.savepoints.dir").
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "cancel" cancels a running program.

  Syntax: cancel [OPTIONS] <Job ID>
  "cancel" action options:
     -s,--withSavepoint <targetDirectory>   **DEPRECATION WARNING**: Cancelling
                                            a job with savepoint is deprecated.
                                            Use "stop" instead.
                                            Trigger savepoint and cancel job.
                                            The target directory is optional. If
                                            no directory is specified, the
                                            configured default directory
                                            (state.savepoints.dir) is used.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "savepoint" triggers savepoints for a running job or disposes existing ones.

  Syntax: savepoint [OPTIONS] <Job ID> [<target directory>]
  "savepoint" action options:
     -d,--dispose <arg>       Path of savepoint to dispose.
     -j,--jarfile <jarfile>   Flink program JAR file.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode

Step8、查看结果文件

注意:集群搭建完毕后,Flink程序就可以达成Jar,在集群环境下类似于Step7中一样提交执行计算任务

打jar包插件:

<build>
        <plugins>
            <!-- 打jar插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

1.5 Yarn模式部署

(1)启动一个YARN session(Start a long-running Flink cluster on YARN);

image-20200921141637845

修改/etc/profile

export HADOOP_CONF_DIR=$HADOOP_HOME
export YARN_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HADOOP_CLASSPATH=`hadoop classpath`

配置文件 yarn-site.xml

<property>
    <name>yarn.nodemanager.pmem-check-enabled</name>
    <value>false</value>
</property>
<property>
    <name>yarn.nodemanager.vmem-check-enabled</name>
    <value>false</value>
</property>
  <property>
    <name>yarn.resourcemanager.address</name>
    <value>teacher2:8032</value>
  </property>
  <property>
    <name>yarn.resourcemanager.scheduler.address</name>
    <value>teacher2:8030</value>
  </property>
  <property>
    <name>yarn.resourcemanager.resource-tracker.address</name>
    <value>teacher2:8031</value>
  </property>

注意:yarn-site的修改需要在集群的每一台机器上执行

启动hadoop (hdfs-yarn) yarn-session.sh -h

Optional
     -at,--applicationType <arg>     Set a custom application type for the application on YARN
     -D <property=value>             use value for given property
     -d,--detached                   If present, runs the job in detached mode
     -h,--help                       Help for the Yarn session CLI.
     -id,--applicationId <arg>       Attach to running YARN session
     -j,--jar <arg>                  Path to Flink jar file
     -jm,--jobManagerMemory <arg>    Memory for JobManager Container with optional unit (default: MB)
     -m,--jobmanager <arg>           Address of the JobManager to which to connect. Use this flag to connect to a different JobManager than the one specified in the configuration.
     -nl,--nodeLabel <arg>           Specify YARN node label for the YARN application
     -nm,--name <arg>                Set a custom name for the application on YARN
     -q,--query                      Display available YARN resources (memory, cores)
     -qu,--queue <arg>               Specify YARN queue.
     -s,--slots <arg>                Number of slots per TaskManager
     -t,--ship <arg>                 Ship files in the specified directory (t for transfer)
     -tm,--taskManagerMemory <arg>   Memory per TaskManager Container with optional unit (default: MB)
     -yd,--yarndetached              If present, runs the job in detached mode (deprecated; use non-YARN specific option instead)
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths for high availability mode
[root@hdp-1 bin]# 

/export/servers/flink/bin/yarn-session.sh -n 2 -tm 800 -s 1 -d

申请2个CPU、1600M内存::

bin/yarn-session.sh -n 2 -tm 800 -s 1 -d

# -n 表示申请2个容器,这里指的就是多少个taskmanager

# -s 表示每个TaskManager的slots数量

# -tm 表示每个TaskManager的内存大小

# -d 表示以后台程序方式运行

l 解释

上面的命令的意思是,同时向Yarn申请3个container

(即便只申请了两个,因为ApplicationMaster和Job Manager有一个额外的容器。一旦将Flink部署到YARN群集中,它就会显示Job Manager的连接详细信息)

2 个 Container 启动 TaskManager -n 2,每个 TaskManager 拥有1个 Task Slot -s 1,并且向每个 TaskManager 的 Container 申请 800M 的内存,以及一个ApplicationMaster--Job Manager。

如果不想让Flink YARN客户端始终运行,那么也可以启动分离的 YARN会话。该参数被称为-d或--detached。在这种情况下,Flink YARN客户端只会将Flink提交给集群,然后关闭它自己

yarn-session.sh(开辟资源) + flink run(提交任务)

- 使用Flink中的yarn-session(yarn客户端),会启动两个必要服务JobManager和TaskManager

- 客户端通过flink run提交作业

- yarn-session会一直启动,不停地接收客户端提交的作业

- 这种方式创建的Flink集群会独占资源。

- 如果有大量的小作业/任务比较小,或者工作时间短,适合使用这种方式,减少资源创建的时间.

(2)直接在YARN上提交运行Flink作业(Run a Flink job on YARN)

image-20200921144352862

bin/flink run -m yarn-cluster -yn 2 -yjm 1024 -ytm 1024 /export/servers/flink/examples/batch/WordCount.jar

# -m jobmanager的地址

# -yn 表示TaskManager的个数

停止yarn-cluster:

yarn application -kill application_1527077715040_0003

rm -rf /tmp/.yarn-properties-root

image-20200921145741521

第五部分 Flink常用API详解

​ DataStream API主要分为3块:DataSource、Transformation、Sink

  • DataSource是程序的数据源输入,可以通过StreamExecutionEnvironment.addSource(sourceFuntion)为程序添加一个数据源
  • Transformation是具体的操作,它对一个或多个输入数据源进行计算处理,比如Map、FlatMap和Filter等操作
  • Sink是程序的输出,它可以把Transformation处理之后的数据输出到指定的存储介质中。

第 1 节 Flink DataStream常用API

1.1 DataSource

​ Flink针对DataStream提供了大量已经实现的DataSource(数据源接口),比如如下4种

1)基于文件

​ readTextFile(path)

读取文本文件,文件遵循TextInputFormat逐行读取规则并返回

tip:本地Idea读hdfs需要:

依赖:

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-hadoop-compatibility_2.11</artifactId>
            <version>1.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.8.5</version>
        </dependency>	
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.8.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.8.5</version>
        </dependency>

2)基于Socket

​ socketTextStream

从Socket中读取数据,元素可以通过一个分隔符分开

3)基于集合

​ fromCollection(Collection)

通过Java的Collection集合创建一个数据流,集合中的所有元素必须是相同类型的

如果满足以下条件,Flink将数据类型识别为POJO类型(并允许“按名称”字段引用):

  • 该类是共有且独立的(没有非静态内部类)
  • 该类有共有的无参构造方法
  • 类(及父类)中所有的不被static、transient修饰的属性要么有公有的(且不被final修饰),要么是包含共有的getter和setter方法,这些方法遵循java bean命名规范。

实例:

package com.lagou.edu.streamsource;

import org.apache.flink.api.common.functions.FilterFunction;
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 java.util.ArrayList;

public class StreamFromCollection {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//        DataStreamSource<String> data = env.fromElements("spark", "flink");
        ArrayList<People> peopleList = new ArrayList<People>();
        peopleList.add(new People("lucas", 18));
        peopleList.add(new People("jack", 30));
        peopleList.add(new People("jack", 40));
        DataStreamSource<People> data = env.fromCollection(peopleList);
//        DataStreamSource<People> data = env.fromElements(new People("lucas", 18), new People("jack", 30), new People("jack", 40));
        SingleOutputStreamOperator<People> filtered = data.filter(new FilterFunction<People>() {
            public boolean filter(People people) throws Exception {
                return people.age > 20;
            }
        });
        filtered.print();
        env.execute();


    }

    public static class People{
        public String name;
        public Integer age;

        public People(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "People{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

4)自定义输入

可以使用StreamExecutionEnvironment.addSource(sourceFunction)将一个流式数据源加到程序中。

Flink提供了许多预先实现的源函数,但是也可以编写自己的自定义源,方法是为非并行源implements SourceFunction,或者为并行源 implements ParallelSourceFunction接口,或者extends RichParallelSourceFunction。

​ Flink也提供了一批内置的Connector(连接器),如下表列了几个主要的

连接器 是否提供Source支持 是否提供Sink支持
Apache Kafka
ElasticSearch
HDFS
Twitter Streaming PI

Kafka连接器

a、依赖:

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_2.11</artifactId>
            <version>1.11.1</version>
        </dependency>

b、代码:

import org.apache.flink.api.java.tuple.Tuple2;
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.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.util.Collector;

import java.util.Properties;

public class StreamFromKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers","hdp-2:9092");
        FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<String>("mytopic2", new SimpleStringSchema(), properties);
        DataStreamSource<String> data = env.addSource(consumer);
        SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOne = data.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                for (String word : s.split(" ")) {
                    collector.collect(Tuple2.of(word, 1));
                }
            }
        });
        SingleOutputStreamOperator<Tuple2<String, Integer>> result = wordAndOne.keyBy(0).sum(1);
        result.print();
        env.execute();
    }
}

c、启动kafka

./kafka-server-start.sh -daemon ../config/server.properties

d、创建topic

bin/kafka-topics.sh --create --zookeepper teacher1:2181 --replication-factor 1 --partitions 1 --topic mytopic2

e、启动控制台kafka生产者

./kafka-console-consumer.sh --bootstrap-server hdp-2:9092 --topic animal

为非并行源implements SourceFunction,或者为并行源 implements ParallelSourceFunction接口,或者extends RichParallelSourceFunction。

为非并行源implements SourceFunction

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.SourceFunction;

public class NoParalleSource implements SourceFunction<String> {
    private Long count = 1l;
    private boolean isRunning = true;

    public void run(SourceContext<String> ctx) throws Exception {
        while (isRunning) {
            count ++;
            ctx.collect(String.valueOf(count));
            Thread.sleep(1000);
        }
    }

    public void cancel() {
        isRunning = false;
    }
}

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromNoParalleSource {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new NoParalleSource());
        data.print();
        env.execute();
    }
}

为并行源 implements arallelSourceFunction接口

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;

public class ParalleSource implements ParallelSourceFunction<String> {
    long count = 0;
    boolean isRunning = true;
    @Override
    public void run(SourceContext<String> ctx) throws Exception {
        while(isRunning) {
            ctx.collect(String.valueOf(count));
            count ++;
            Thread.sleep(1000);
        }
    }

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

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromParllelSource {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new ParalleSource());
        data.print();
        env.execute();
    }
}

extends RichParallelSourceFunction

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.RichSourceFunction;

public class ParallelSourceRich extends RichParallelSourceFunction<String> {
    long count = 0;
    boolean isRunning = true;
    @Override
    public void run(SourceContext<String> ctx) throws Exception {
        while(isRunning) {
            ctx.collect(String.valueOf(count));
            count ++;
            Thread.sleep(1000);
        }
    }

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

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromRichSourceFunction {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new ParallelSourceRich());
        data.print();
        env.execute();
    }
}

总结自定义数据源:flinkkafkaconnector源码初探:

open方法:初始化

run方法:从kafka拉取数据

1.2 Transformation

Flink针对DataStream提供了大量的已经实现的算子

Map DataStream → DataStream Takes one element and produces one element. A map function that doubles the values of the input stream:

DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
    @Override
    public Integer map(Integer value) throws Exception {
        return 2 * value;
    }
});

FlatMap DataStream → DataStream Takes one element and produces zero, one, or more elements. A flatmap function that splits sentences to words:

dataStream.flatMap(new FlatMapFunction<String, String>() {
    @Override
    public void flatMap(String value, Collector<String> out)
        throws Exception {
        for(String word: value.split(" ")){
            out.collect(word);
        }
    }
});

Filter DataStream → DataStream Evaluates a boolean function for each element and retains those for which the function returns true. A filter that filters out zero values:

dataStream.filter(new FilterFunction<Integer>() {
    @Override
    public boolean filter(Integer value) throws Exception {
        return value != 0;
    }
});

KeyBy DataStream → KeyedStream Logically partitions a stream into disjoint partitions. All records with the same key are assigned to the same partition. Internally, keyBy() is implemented with hash partitioning. There are different ways to specify keys.

This transformation returns a KeyedStream, which is, among other things, required to use keyed state.

dataStream.keyBy(value -> value.getSomeKey()) // Key by field "someKey"
dataStream.keyBy(value -> value.f0) // Key by the first element of a Tuple

Attention A type cannot be a key if:

it is a POJO type but does not override the hashCode() method and relies on the Object.hashCode() implementation. it is an array of any type. Reduce KeyedStream → DataStream A "rolling" reduce on a keyed data stream. Combines the current element with the last reduced value and emits the new value.

A reduce function that creates a stream of partial sums:

keyedStream.reduce(new ReduceFunction<Integer>() {
    @Override
    public Integer reduce(Integer value1, Integer value2)
    throws Exception {
        return value1 + value2;
    }
});

Fold KeyedStream → DataStream A "rolling" fold on a keyed data stream with an initial value. Combines the current element with the last folded value and emits the new value.

A fold function that, when applied on the sequence (1,2,3,4,5), emits the sequence "start-1", "start-1-2", "start-1-2-3", ...

DataStream<String> result =
  keyedStream.fold("start", new FoldFunction<Integer, String>() {
    @Override
    public String fold(String current, Integer value) {
        return current + "-" + value;
    }
  });

Aggregations KeyedStream → DataStream Rolling aggregations on a keyed data stream. The difference between min and minBy is that min returns the minimum value, whereas minBy returns the element that has the minimum value in this field (same for max and maxBy).

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");

Window KeyedStream → WindowedStream Windows can be defined on already partitioned KeyedStreams. Windows group the data in each key according to some characteristic (e.g., the data that arrived within the last 5 seconds). See windows for a complete description of windows.

dataStream.keyBy(value -> value.f0).window(TumblingEventTimeWindows.of(Time.seconds(5))); // Last 5 seconds of data

WindowAll DataStream → AllWindowedStream Windows can be defined on regular DataStreams. Windows group all the stream events according to some characteristic (e.g., the data that arrived within the last 5 seconds). See windows for a complete description of windows.

WARNING: This is in many cases a non-parallel transformation. All records will be gathered in one task for the windowAll operator.

dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5))); // Last 5 seconds of data

Window Apply WindowedStream → DataStream AllWindowedStream → DataStream Applies a general function to the window as a whole. Below is a function that manually sums the elements of a window.

Note: If you are using a windowAll transformation, you need to use an AllWindowFunction instead.

windowedStream.apply (new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() {
    public void apply (Tuple tuple,
            Window window,
            Iterable<Tuple2<String, Integer>> values,
            Collector<Integer> out) throws Exception {
        int sum = 0;
        for (value t: values) {
            sum += t.f1;
        }
        out.collect (new Integer(sum));
    }
});

// applying an AllWindowFunction on non-keyed window stream
allWindowedStream.apply (new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() {
    public void apply (Window window,
            Iterable<Tuple2<String, Integer>> values,
            Collector<Integer> out) throws Exception {
        int sum = 0;
        for (value t: values) {
            sum += t.f1;
        }
        out.collect (new Integer(sum));
    }
});

Window Reduce WindowedStream → DataStream Applies a functional reduce function to the window and returns the reduced value.

windowedStream.reduce (new ReduceFunction<Tuple2<String,Integer>>() {
    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
        return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1);
    }
});

Window Fold WindowedStream → DataStream Applies a functional fold function to the window and returns the folded value. The example function, when applied on the sequence (1,2,3,4,5), folds the sequence into the string "start-1-2-3-4-5":

windowedStream.fold("start", new FoldFunction<Integer, String>() {
    public String fold(String current, Integer value) {
        return current + "-" + value;
    }
});

Aggregations on windows WindowedStream → DataStream Aggregates the contents of a window. The difference between min and minBy is that min returns the minimum value, whereas minBy returns the element that has the minimum value in this field (same for max and maxBy).

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

Union DataStream → DataStream Union of two or more data streams creating a new stream containing all the elements from all the streams. Note: If you union a data stream with itself you will get each element twice in the resulting stream.

dataStream.union(otherStream1, otherStream2, ...);

Window Join DataStream,DataStream → DataStream Join two data streams on a given key and a common window.

dataStream.join(otherStream)
    .where(<key selector>).equalTo(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(3)))
    .apply (new JoinFunction () {...});

Interval Join KeyedStream,KeyedStream → DataStream Join two elements e1 and e2 of two keyed streams with a common key over a given time interval, so that e1.timestamp + lowerBound <= e2.timestamp <= e1.timestamp + upperBound

// this will join the two streams so that
// key1 == key2 && leftTs - 2 < rightTs < leftTs + 2
keyedStream.intervalJoin(otherKeyedStream)
    .between(Time.milliseconds(-2), Time.milliseconds(2)) // lower and upper bound
    .upperBoundExclusive(true) // optional
    .lowerBoundExclusive(true) // optional
    .process(new IntervalJoinFunction() {...});

Window CoGroup DataStream,DataStream → DataStream Cogroups two data streams on a given key and a common window.

dataStream.coGroup(otherStream)
    .where(0).equalTo(1)
    .window(TumblingEventTimeWindows.of(Time.seconds(3)))
    .apply (new CoGroupFunction () {...});

Connect DataStream,DataStream → ConnectedStreams "Connects" two data streams retaining their types. Connect allowing for shared state between the two streams.

DataStream<Integer> someStream = //...
DataStream<String> otherStream = //...

ConnectedStreams<Integer, String> connectedStreams = someStream.connect(otherStream);

CoMap, CoFlatMap ConnectedStreams → DataStream Similar to map and flatMap on a connected data stream

connectedStreams.map(new CoMapFunction<Integer, String, Boolean>() {
    @Override
    public Boolean map1(Integer value) {
        return true;
    }

@Override
public Boolean map2(String value) {
    return false;
}

});
connectedStreams.flatMap(new CoFlatMapFunction<Integer, String, String>() {

   @Override
   public void flatMap1(Integer value, Collector<String> out) {
       out.collect(value.toString());
   }

   @Override
   public void flatMap2(String value, Collector<String> out) {
       for (String word: value.split(" ")) {
         out.collect(word);
       }
   }
});

Split DataStream → SplitStream Split the stream into two or more streams according to some criterion.

SplitStream<Integer> split = someDataStream.split(new OutputSelector<Integer>() {
    @Override
    public Iterable<String> select(Integer value) {
        List<String> output = new ArrayList<String>();
        if (value % 2 == 0) {
            output.add("even");
        }
        else {
            output.add("odd");
        }
        return output;
    }
});

Select SplitStream → DataStream Select one or more streams from a split stream.

SplitStream<Integer> split;
DataStream<Integer> even = split.select("even");
DataStream<Integer> odd = split.select("odd");
DataStream<Integer> all = split.select("even","odd");

Iterate DataStream → IterativeStream → DataStream Creates a "feedback" loop in the flow, by redirecting the output of one operator to some previous operator. This is especially useful for defining algorithms that continuously update a model. The following code starts with a stream and applies the iteration body continuously. Elements that are greater than 0 are sent back to the feedback channel, and the rest of the elements are forwarded downstream. See iterations for a complete description.

IterativeStream<Long> iteration = initialStream.iterate();
DataStream<Long> iterationBody = iteration.map (/*do something*/);
DataStream<Long> feedback = iterationBody.filter(new FilterFunction<Long>(){
    @Override
    public boolean filter(Long value) throws Exception {
        return value > 0;
    }
});
iteration.closeWith(feedback);
DataStream<Long> output = iterationBody.filter(new FilterFunction<Long>(){
    @Override
    public boolean filter(Long value) throws Exception {
        return value <= 0;
    }
});

1.3 Sink

Flink针对DataStream提供了大量的已经实现的数据目的地(Sink),具体如下所示

  • writeAsText():讲元素以字符串形式逐行写入,这些字符串通过调用每个元素的toString()方法来获取

  • print()/printToErr():打印每个元素的toString()方法的值到标准输出或者标准错误输出流中

  • 自定义输出:addSink可以实现把数据输出到第三方存储介质中

    Flink提供了一批内置的Connector,其中有的Connector会提供对应的Sink支持,如1.1节中表所示

案例:将流数据下沉到redis中

1、依赖:

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-redis_2.11</artifactId>
            <version>1.1.5</version>
        </dependency>

2、关键代码:

//封装
        SingleOutputStreamOperator<Tuple2<String, String>> l_wordsData = data.map(new MapFunction<String, Tuple2<String, String>>() {
            @Override
            public Tuple2<String, String> map(String value) throws Exception {
                return new Tuple2<>("l_words", value);
            }
        });

        FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("hdp-1").setPort(6379).build();

        RedisSink<Tuple2<String, String>> redisSink = new RedisSink<>(conf, new MyRedisMapper());
        l_wordsData.addSink(redisSink);
 public static class MyMapper implements RedisMapper<Tuple2<String,String>> {

        @Override
        public RedisCommandDescription getCommandDescription() {
            return new RedisCommandDescription(RedisCommand.LPUSH);
        }

        @Override
        public String getKeyFromData(Tuple2<String,String> data) {
            return data.f0;
        }

        @Override
        public String getValueFromData(Tuple2<String,String> data) {
            return data.f1;
        }
    }

案例2:将流数据下沉到redis中--自定义

package com.lagou.bak;

import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class SinkToMySql extends RichSinkFunction<Student> {
    PreparedStatement preparedStatement = null;
    Connection connection = null;

    @Override
    public void open(Configuration parameters) throws Exception {
        String url = "jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC";
        String user = "root";
        String password = "lucas";
        connection = DriverManager.getConnection(url, user, password);
        String sql = "insert into student (name,age) values (?,?)";
        preparedStatement = connection.prepareStatement(sql);

    }

    @Override
    public void invoke(Student stu, Context context) throws Exception {
        preparedStatement.setString(1,stu.getName());
        preparedStatement.setInt(2,stu.getAge());
        preparedStatement.executeUpdate();
    }

    @Override
    public void close() throws Exception {
        if(connection != null) {
            connection.close();
        }
        if (preparedStatement != null) {
            preparedStatement.close();
        }
    }
}

启动类:

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class StreamMySqlRun {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Student> data = env.fromElements(new Student("lucas", 18), new Student("jack", 30));
        data.addSink(new SinkToMySql());
        env.execute();
    }
}

案例3:下沉到Kafka

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;

public class StreamToKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        String brokerList = "teacher2:9092";
        String topic = "mytopic2";
        FlinkKafkaProducer producer = new FlinkKafkaProducer(brokerList, topic, new SimpleStringSchema());
        data.addSink(producer);
        env.execute();

    }
}

第 2 节 Flink DataSet常用API

​ DataSet API同DataStream API一样有三个组成部分,各部分作用对应一致,此处不再赘述

2.1 DataSource

对DataSet批处理而言,较为频繁的操作是读取HDFS中的文件数据,因为这里主要介绍两个DataSource组件

  • 基于集合

    fromCollection(Collection),主要是为了方便测试使用

  • 基于文件

    readTextFile(path),基于HDFS中的数据进行计算分析

2.2 Transformation

Transformation Description
Map 在算子中得到一个元素并生成一个新元素data.map { x => x.toInt }
FlatMap 在算子中获取一个元素, 并生成任意个数的元素data.flatMap { str => str.split(" ") }
MapPartition 类似Map, 但是一次Map一整个并行分区data.mapPartition { in => in map { (_, 1) } }
Filter 如果算子返回true则包含进数据集, 如果不是则被过滤掉data.filter { _ > 100 }
Reduce 通过将两个元素合并为一个元素, 从而将一组元素合并为一个元素data.reduce { _ + _ }
ReduceGroup 将一组元素合并为一个或者多个元素data.reduceGroup { elements => elements.sum }
Aggregate 讲一组值聚合为一个值, 聚合函数可以看作是内置的Reduce函数data.aggregate(SUM, 0).aggregate(MIN, 2)data.sum(0).min(2)
Distinct 去重
Join 按照相同的Key合并两个数据集input1.join(input2).where(0).equalTo(1)同时也可以选择进行合并的时候的策略, 是分区还是广播, 是基于排序的算法还是基于哈希的算法input1.join(input2, JoinHint.BROADCAST_HASH_FIRST).where(0).equalTo(1)
OuterJoin 外连接, 包括左外, 右外, 完全外连接等left.leftOuterJoin(right).where(0).equalTo(1) { (left, right) => ... }
CoGroup 二维变量的Reduce运算, 对每个输入数据集中的字段进行分组, 然后join这些组input1.coGroup(input2).where(0).equalTo(1)
Cross 笛卡尔积input1.cross(input2)
Union 并集input1.union(input2)
Rebalance 分区重新平衡, 以消除数据倾斜input.rebalance()
Hash-Partition 按照Hash分区input.partitionByHash(0)
Range-Partition 按照Range分区input.partitionByRange(0)
CustomParititioning 自定义分区input.partitionCustom(partitioner: Partitioner[K], key)
First-n 返回数据集中的前n个元素input.first(3)
partitionByHash 按照指定的key进行hash分区
sortPartition 指定字段对分区中的数据进行排序

​ Flink针对DataSet也提供了大量的已经实现的算子,和DataStream计算很类似

  • Map:输入一个元素,然后返回一个元素,中间可以进行清洗转换等操作
  • FlatMap:输入一个元素,可以返回0个、1个或者多个元素
    • Filter:过滤函数,对传入的数据进行判断,符合条件的数据会被留下
    • Reduce:对数据进行聚合操作,结合当前元素和上一次Reduce返回的值进行聚合操作,然后返回一个新的值
    • Aggregations:sum()、min()、max()等

2.3 Sink

​ Flink针对DataStream提供了大量的已经实现的数据目的地(Sink),具体如下所示

  • writeAsText():将元素以字符串形式逐行写入,这些字符串通过调用每个元素的toString()方法来获取

  • writeAsCsv():将元组以逗号分隔写入文件中,行及字段之间的分隔是可配置的,每个字段的值来自对象的toString()方法

  • print()/pringToErr():打印每个元素的toString()方法的值到标准输出或者标准错误输出流中

    Flink提供了一批内置的Connector,其中有的Connector会提供对应的Sink支持,如1.1节中表所示

第3节 Flink Table API和SQLAPI

Apache Flink提供了两种顶层的关系型API,分别为Table API和SQL,Flink通过Table API&SQL实现了批流统一。其中Table API是用于Scala和Java的语言集成查询API,它允许以非常直观的方式组合关系运算符(例如select,where和join)的查询。Flink SQL基于Apache Calcite 实现了标准的SQL,用户可以使用标准的SQL处理数据集。Table API和SQL与Flink的DataStream和DataSet API紧密集成在一起,用户可以实现相互转化,比如可以将DataStream或者DataSet注册为table进行操作数据。值得注意的是,Table API and SQL目前尚未完全完善,还在积极的开发中,所以并不是所有的算子操作都可以通过其实现。

依赖:

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table</artifactId>
            <version>1.11.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>

        <!-- Either... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge_2.11</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- or... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>

基于TableAPI的案例:

package com.lagou.table;

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.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

import static org.apache.flink.table.api.Expressions.$;

public class TableApiDemo {
    public static void main(String[] args) throws Exception {
        //Flink执行环境env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //用env,做出Table环境tEnv
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
        //获取流式数据源
        DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });
        //将流式数据源做成Table
        Table table = tEnv.fromDataStream(data, $("name"), $("age"));
        //对Table中的数据做查询
        Table name = table.select($("name"));
        //将处理结果输出到控制台
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);
        result.print();
        env.execute();

    }
}

第六部分 Flink Window窗口机制

Flink Window 背景

​ Flink认为Batch是Streaming的一个特例,因此Flink底层引擎是一个流式引擎,在上面实现了流处理和批处理。而Window就是从Streaming到Batch的桥梁。

​ 通俗讲,Window是用来对一个无限的流设置一个有限的集合,从而在有界的数据集上进行操作的一种机制。流上的集合由Window来划定范围,比如“计算过去10分钟”或者“最后50个元素的和”。

​ Window可以由时间(Time Window)(比如每30s)或者数据(Count Window)(如每100个元素)驱动。DataStream API提供了Time和Count的Window。

Flink Window 总览

  • Window 是flink处理无限流的核心,Windows将流拆分为有限大小的“桶”,我们可以在其上应用计算。
  • Flink 认为 Batch 是 Streaming 的一个特例,所以 Flink 底层引擎是一个流式引擎,在上面实现了流处理和批处理。
  • 而窗口(window)就是从 Streaming 到 Batch 的一个桥梁。
  • Flink 提供了非常完善的窗口机制。
  • 在流处理应用中,数据是连续不断的,因此我们不可能等到所有数据都到了才开始处理。
  • 当然我们可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。
  • 在这种情况下,我们必须定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口内的数据进行计算。
  • 窗口可以是基于时间驱动的(Time Window,例如:每30秒钟)
  • 也可以是基于数据驱动的(Count Window,例如:每一百个元素)
  • 同时基于不同事件驱动的窗口又可以分成以下几类:
    • 翻滚窗口 (Tumbling Window, 无重叠)
    • 滑动窗口 (Sliding Window, 有重叠)
    • 会话窗口 (Session Window, 活动间隙)
    • 全局窗口 (略)
  • Flink要操作窗口,先得将StreamSource 转成WindowedStream

步骤:

1、获取流数据源

2、获取窗口

3、操作窗口数据

4、输出窗口数据

第 1 节 时间窗口(TimeWindow)

1.1 滚动时间窗口(Tumbling Window)

image-20200731072656176

​ 将数据依据固定的窗口长度对数据进行切分

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

代码示例

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * 翻滚窗口:窗口不重叠
 * 1、基于时间驱动
 * 2、基于事件驱动
 */
public class TumblingWindow {

    public static void main(String[] args) {
    //设置执行环境,类似spark中初始化sparkContext
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teache2", 7777);

        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.out.println("value: " + value + " random: " + random + "timestamp: " + timeMillis + "|" + format.format(timeMillis));

                return new Tuple2<String, Integer>(value, random);
            }
        });

        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);


        // 基于时间驱动,每隔10s划分一个窗口
        WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(10));

        // 基于事件驱动, 每相隔3个事件(即三个相同key的数据), 划分一个窗口进行计算
        // WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3);

        // apply是窗口的应用函数,即apply里的函数将应用在此窗口的数据上。
        timeWindow.apply(new MyTimeWindowFunction()).print();
        // countWindow.apply(new MyCountWindowFunction()).print();

        try {
            // 转换算子都是lazy init的, 最后要显式调用 执行程序
            env.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
1.2.1 基于时间驱动

​ 场景:我们需要统计每一分钟中用户购买的商品的总数,需要将用户的行为事件按每一分钟进行切分,这种切分被成为翻滚时间窗口(Tumbling Time Window)

package com.lagou.edu.flink.window;

import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;

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

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

        int sum = 0;

        for(Tuple2<String,Integer> tuple2 : input){
            sum +=tuple2.f1;
        }

        long start = window.getStart();
        long end = window.getEnd();

        out.collect("key:" + tuple.getField(0) + " value: " + sum + "| window_start :"
                + format.format(start) + "  window_end :" + format.format(end)
        );

    }
}
1.2.2 基于事件驱动

​ 场景:当我们想要每100个用户的购买行为作为驱动,那么每当窗口中填满100个”相同”元素了,就会对窗口进行计算。

package com.lagou.edu.flink.window;

import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;

public class MyCountWindowFunction implements WindowFunction<Tuple2<String, Integer>, String, Tuple, GlobalWindow> {

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

        int sum = 0;

        for (Tuple2<String, Integer> tuple2 : input){
            sum += tuple2.f1;
        }
        //无用的时间戳,默认值为: Long.MAX_VALUE,因为基于事件计数的情况下,不关心时间。
        long maxTimestamp = window.maxTimestamp();

        out.collect("key:" + tuple.getField(0) + " value: " + sum + "| maxTimeStamp :"
                + maxTimestamp + "," + format.format(maxTimestamp)
        );
    }
}

1.2 滑动时间窗口(Sliding Window)

image-20200731073053682

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

​ 特点:窗口长度固定,可以有重叠

1.2.1 基于时间的滑动窗口

​ 场景: 我们可以每30秒计算一次最近一分钟用户购买的商品总数

1.2.2 基于事件的滑动窗口

​ 场景: 每10个 “相同”元素计算一次最近100个元素的总和

代码实现

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * 滑动窗口:窗口可重叠
 * 1、基于时间驱动
 * 2、基于事件驱动
 */
public class SlidingWindow {

    public static void main(String[] args) {
        // 设置执行环境, 类似spark中初始化SparkContext
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teacher2", 7777);

        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<String, Integer>(value, random);
            }
        });
        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);

        //基于时间驱动,每隔5s计算一下最近10s的数据
     //   WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(10), Time.seconds(5));
        //基于事件驱动,每隔2个事件,触发一次计算,本次窗口的大小为3,代表窗口里的每种事件最多为3个
        WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3, 2);

     //   timeWindow.sum(1).print();

        countWindow.sum(1).print();

     //   timeWindow.apply(new MyTimeWindowFunction()).print();

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

1.3 会话窗口(Session Window)

image-20200731073350282

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

session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况

session窗口在一个固定的时间周期内不再收到元素,即非活动间隔产生,那么这个窗口就会关闭。

一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。

特点

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

​ 与翻滚窗口和滑动窗口相反, 当会话窗口在一段时间内没有接收到元素时会关闭会话窗口。

​ 后续的元素将会被分配给新的会话窗口

案例描述

​ 计算每个用户在活跃期间总共购买的商品数量,如果用户30秒没有活动则视为会话断开

代码实现

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.ProcessingTimeSessionWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

public class SessionWindow {

    public static void main(String[] args) {

        // 设置执行环境, 类似spark中初始化sparkContext

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teacher2", 7777);

        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<String, Integer>(value, random);
            }
        });
        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);

        //如果连续10s内,没有数据进来,则会话窗口断开。
        WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> window = keyedStream.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));

        // window.sum(1).print();
        
        window.apply(new MyTimeWindowFunction()).print();

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

第七部分 Flink Time

1.1 Time

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

image-20200921112633884

- EventTime[事件时间]

事件发生的时间,例如:点击网站上的某个链接的时间,每一条日志都会记录自己的生成时间

如果以EventTime为基准来定义时间窗口那将形成EventTimeWindow,要求消息本身就应该携带EventTime

- IngestionTime[摄入时间]

数据进入Flink的时间,如某个Flink节点的source operator接收到数据的时间,例如:某个source消费到kafka中的数据

如果以IngesingtTime为基准来定义时间窗口那将形成IngestingTimeWindow,以source的systemTime为准

- ProcessingTime[处理时间]

某个Flink节点执行某个operation的时间,例如:timeWindow处理数据时的系统时间,默认的时间属性就是Processing Time

如果以ProcessingTime基准来定义时间窗口那将形成ProcessingTimeWindow,以operator的systemTime为准

在Flink的流式处理中,绝大部分的业务都会使用EventTime,一般只在EventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。

如果要使用EventTime,那么需要引入EventTime的时间属性,引入方式如下所示:

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //设置使用事件时间

1.2.数据延迟产生的问题

l 示例1

现在假设,你正在去往地下停车场的路上,并且打算用手机点一份外卖。

选好了外卖后,你就用在线支付功能付款了,这个时候是11点50分。恰好这时,你走进了地下停车库,而这里并没有手机信号。因此外卖的在线支付并没有立刻成功,而支付系统一直在Retry重试“支付”这个操作。

当你找到自己的车并且开出地下停车场的时候,已经是12点05分了。这个时候手机重新有了信号,手机上的支付数据成功发到了外卖在线支付系统,支付完成。

在上面这个场景中你可以看到,支付数据的事件时间是11点50分,而支付数据的处理时间是12点05分

一般在实际开发中会以事件时间作为计算标准

l 示例2

image-20200921111842449

一条日志进入Flink的时间为2019-08-12 10:00:01,摄入时间

到达Window的系统时间为2019-08-12 10:00:02,处理时间

日志的内容为:2019-08-12 09:58:02 INFO Fail over to rm2 ,事件时间

对于业务来说,要统计1h内的故障日志个数,哪个时间是最有意义的?---事件时间

EventTime,因为我们要根据日志的生成时间进行统计。

l 示例3

某 App 会记录用户的所有点击行为,并回传日志(在网络不好的情况下,先保存在本地,延后回传)。

A 用户在 11:02 对 App 进行操作,B 用户在 11:03 操作了 App,

但是 A 用户的网络不太稳定,回传日志延迟了,导致我们在服务端先接受到 B 用户 11:03 的消息,然后再接受到 A 用户 11:02 的消息,消息乱序了。

l 示例4

在实际环境中,经常会出现,因为网络原因,数据有可能会延迟一会才到达Flink实时处理系统。

我们先来设想一下下面这个场景:

image-20200921111903993

  1. 使用时间窗口来统计10分钟内的用户流量

  2. 有一个时间窗口

- 开始时间为:2017-03-19 10:00:00

- 结束时间为:2017-03-19 10:10:00

  1. 有一个数据,因为网络延迟

- 事件发生的时间为:2017-03-19 10:10:00

- 但进入到窗口的时间为:2017-03-19 10:10:02,延迟了2秒中

  1. 时间窗口并没有将59这个数据计算进来,导致数据统计不正确

这种处理方式,根据消息进入到window时间,来进行计算。在网络有延迟的时候,会引起计算误差。

如何解决?---使用水印解决网络延迟问题

通过上面的例子,我们知道,在进行数据处理的时候应该按照事件时间进行处理,也就是窗口应该要考虑到事件时间

但是窗口不能无限的一直等到延迟数据的到来,需要有一个触发窗口计算的机制

也就是我们接下来要学的watermaker水位线/水印机制

1.3 使用Watermark解决

水印(watermark)就是一个时间戳,Flink可以给数据流添加水印,

可以理解为:收到一条消息后,额外给这个消息添加了一个时间字段,这就是添加水印。

- 水印并不会影响原有Eventtime事件时间

- 当数据流添加水印后,会按照水印时间来触发窗口计算

也就是说watermark水印是用来触发窗口计算的

- 一般会设置水印时间,比事件时间小几秒钟,表示最大允许数据延迟达到多久

(即水印时间 = 事件时间 - 允许延迟时间)10:09:57 = 10:10:00 - 3s

- 当接收到的 水印时间 >= 窗口结束时间,则触发计算 如等到一条数据的水印时间为10:10:00 >= 10:10:00 才触发计算,也就是要等到事件时间为10:10:03的数据到来才触发计算

(即事件时间 - 允许延迟时间 >= 窗口结束时间 或 事件时间 >= 窗口结束时间 + 允许延迟时间)

image-20200921111932658

总结:watermaker是用来解决延迟数据的问题

如窗口10:00:00~10:10:00

而数据到达的顺序是: A 10:10:00 ,B 10:09:58

如果没有watermaker,那么A数据将会触发窗口计算,B数据来了窗口已经关闭,则该数据丢失

那么如果有了watermaker,设置允许数据迟到的阈值为3s

那么该窗口的结束条件则为 水印时间>=窗口结束时间10:10:00,也就是需要有一条数据的水印时间= 10:10:00

而水印时间10:10:00= 事件时间- 延迟时间3s

也就是需要有一条事件时间为10:10:03的数据到来,才会真正的触发窗口计算

而上面的 A 10:10:00 ,B 10:09:58都不会触发计算,也就是会被窗口包含,直到10:10:03的数据到来才会计算窗口10:00:00~10:10:00的数据

Watermark案例

步骤

1、获取数据源

2、转化

3、声明水印(watermark)

4、分组聚合,调用window的操作

5、保存处理结果

注意:

当使用EventTimeWindow时,所有的Window在EventTime的时间轴上进行划分,

也就是说,在Window启动后,会根据初始的EventTime时间每隔一段时间划分一个窗口,

如果Window大小是3秒,那么1分钟内会把Window划分为如下的形式:

[00:00:00,00:00:03)

[00:00:03,00:00:06)

[00:00:03,00:00:09)

[00:00:03,00:00:12)

[00:00:03,00:00:15)

[00:00:03,00:00:18)

[00:00:03,00:00:21)

[00:00:03,00:00:24)

...

[00:00:57,00:00:42)

[00:00:57,00:00:45)

[00:00:57,00:00:48)

...

如果Window大小是10秒,则Window会被分为如下的形式:

[00:00:00,00:00:10)

[00:00:10,00:00:20)

...

[00:00:50,00:01:00)

l 注意:

1.窗口是左闭右开的,形式为:[window_start_time,window_end_time)。

2.Window的设定基于第一条消息的事件时间,也就是说,Window会一直按照指定的时间间隔进行划分,不论这个Window中有没有数据,EventTime在这个Window期间的数据会进入这个Window。

3.Window会不断产生,属于这个Window范围的数据会被不断加入到Window中,所有未被触发的Window都会等待触发,只要Window还没触发,属于这个Window范围的数据就会一直被加入到Window中,直到Window被触发才会停止数据的追加,而当Window触发之后才接受到的属于被触发Window的数据会被丢弃。

4.Window会在以下的条件满足时被触发执行:

(1)在[window_start_time,window_end_time)窗口中有数据存在

(2)watermark时间 >= window_end_time;

5.一般会设置水印时间,比事件时间小几秒钟,表示最大允许数据延迟达到多久

(即水印时间 = 事件时间 - 允许延迟时间)

当接收到的 水印时间 >= 窗口结束时间且窗口内有数据,则触发计算

(即事件时间 - 允许延迟时间 >= 窗口结束时间 或 事件时间 >= 窗口结束时间 + 允许延迟时间)

1.4 代码实现

数据源:

01,1586489566000 01,1586489567000 01,1586489568000 01,1586489569000 01,1586489570000 01,1586489571000 01,1586489572000 01,1586489573000

2020-04-10 11:32:46 2020-04-10 11:32:47 2020-04-10 11:32:48 2020-04-10 11:32:49 2020-04-10 11:32:50

代码:

package com.lagou.Time;

import org.apache.commons.math3.fitting.leastsquares.EvaluationRmsChecker;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import sun.nio.cs.StreamEncoder;

import javax.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;

public class WaterDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        SingleOutputStreamOperator<Tuple2<String, Long>> maped = data.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                String[] split = value.split(",");
                return new Tuple2<>(split[0], Long.valueOf(split[1]));
            }
        });
        SingleOutputStreamOperator<Tuple2<String, Long>> watermarks = maped.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
            Long currentMaxTimestamp = 0l;
            final Long maxOutOfOrderness = 10000l;
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

            @Nullable
            @Override
            public Watermark getCurrentWatermark() {
                return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
            }

            @Override
            public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
                long timestamp = element.f1;
//                System.out.println("timestamp:" + timestamp + "..." );
//                System.out.println("..." +  sdf.format(timestamp));

                currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
                System.out.println("key:" + element.f0
                        + "...eventtime:[" + element.f1 + "|" + sdf.format(element.f1)
                        /*+ "],currentMaxTimestamp:[" + currentMaxTimestamp + "|" + sdf.format(currentMaxTimestamp)*/
                        /*+ "],watermark:[" + getCurrentWatermark().getTimestamp() + "| " + sdf.format(getCurrentWatermark().getTimestamp() + "]")*/);
                System.out.println("currentMaxTimestamp" + currentMaxTimestamp + "..." + sdf.format(currentMaxTimestamp));
                System.out.println("watermark:" + getCurrentWatermark().getTimestamp() + "..." + sdf.format(getCurrentWatermark().getTimestamp()));
                return timestamp;
            }
        });
        SingleOutputStreamOperator<String> res = watermarks.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(3))).apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
            @Override
            public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
                String key = tuple.toString();
                ArrayList<Long> list = new ArrayList<>();
                Iterator<Tuple2<String, Long>> it = input.iterator();
                while (it.hasNext()) {
                    Tuple2<String, Long> next = it.next();
                    list.add(next.f1);
                }
                Collections.sort(list);
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                String result = key + "," + list.size() + "," + sdf.format(list.get(0)) + "," + sdf.format(list.get(list.size() - 1)) + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
                out.collect(result);
            }
        });
        res.print();
        env.execute();
    }
}

第八部分 Flink的State--状态原理及原理剖析

State:用来保存计算结果或缓存数据。

Sum

状态类型

Flink根据是否需要保存中间结果,把计算分为有状态计算和无状态计算

有状态计算:依赖之前或之后的事件

无状态计算:独立

根据数据结构不同,Flink定义了多种state,应用于不同的场景

  • ValueState:即类型为T的单值状态。这个状态与对应的key绑定,是最简单的状态了。它可以通过update方法更新状态值,通过value()方法获取状态值。
  • ListState:即key上的状态值为一个列表。可以通过add方法往列表中附加值;也可以通过get()方法返回一个Iterable<T>来遍历状态值。
  • ReducingState:这种状态通过用户传入的reduceFunction,每次调用add方法添加值的时候,会调用reduceFunction,最后合并到一个单一的状态值。
  • FoldingState:跟ReducingState有点类似,不过它的状态值类型可以与add方法中传入的元素类型不同(这种状态将会在Flink未来版本中被删除)。
  • MapState:即状态值为一个map。用户通过putputAll方法添加元素

State按照是否有key划分为KeyedState和OperatorState

Keyed State:KeyedStream流上的每一个Key都对应一个State

案例:利用state求平均值

原始数据:(1,3)(1,5)(1,7)(1,4)(1,2)

思路:

1、读数据源

2、将数据源根据key分组

3、按照key分组策略,对流式数据调用状态化处理

​ 在处理过程中:

​ a、实例化出一个状态实例

<T> ValueState<T> getState(ValueStateDescriptor<T> stateProperties);

ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        })
                        , Tuple2.of(0L, 0L)
                );
getRuntimeContext().getState(descriptor);

/**
	 * Creates a new {@code ValueStateDescriptor} with the given name and default value.
	 *
	 * @deprecated Use {@link #ValueStateDescriptor(String, TypeInformation)} instead and manually
	 * manage the default value by checking whether the contents of the state is {@code null}.
	 *
	 * @param name The (unique) name for the state.
	 * @param typeInfo The type of the values in the state.
	 * @param defaultValue The default value that will be set when requesting state without setting
	 *                     a value before.
	 */
	@Deprecated
	public ValueStateDescriptor(String name, TypeInformation<T> typeInfo, T defaultValue) {
		super(name, typeInfo, defaultValue);
	}

​ b、随着流式数据的到来,更新状态

sum.update(currentSum);
void update(T value) throws IOException;

4、输出计算结果

keyed State:代码:

package com.lagou.bak;

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import javax.swing.plaf.IconUIResource;

public class StateTest1 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Tuple2<Long, Long>> data = env.fromElements(Tuple2.of(1l, 3l), Tuple2.of(1l, 5l), Tuple2.of(1l, 7l), Tuple2.of(1l, 4l), Tuple2.of(1l, 2l));
        KeyedStream<Tuple2<Long, Long>, Long> keyed = data.keyBy(value -> value.f0);
//        keyed.
//        keyed.print();
        SingleOutputStreamOperator<Tuple2<Long, Long>> flatMaped = keyed.flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
            private transient ValueState<Tuple2<Long, Long>> sum;

            @Override
            public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {
                //获取当前状态值
                Tuple2<Long, Long> currentSum = sum.value();

                //更新
                currentSum.f0 += 1;
                currentSum.f1 += value.f1;
                System.out.println("...currentSum:"+ currentSum);

                //更新状态值
                sum.update(currentSum);

                //如果count>=2 清空状态值,重新计算
                if(currentSum.f0 >= 5) {
                    out.collect(new Tuple2<>(value.f0,currentSum.f1 / currentSum.f0));
                    sum.clear();
                }
            }

            @Override
            public void open(Configuration parameters) throws Exception {
                ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        }),
                        Tuple2.of(0L, 0L)
                );
//                ValueStateDescriptor<Tuple2<Long, Long>> descriptor1 = new ValueStateDescriptor<>("average", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
//                }));
                sum = getRuntimeContext().getState(descriptor);
            }
        });
        flatMaped.print();
        env.execute();
    }
}

image-20201007120248948

Operator State代码:

ListCheckPointed

CheckPointedFunction

见状态存储

flink_State

flink_state2

(1)Keyed State

表示和Key相关的一种State,只能用于KeydStream类型数据集对应的Functions和 Operators之上。Keyed State是 Operator State的特例,区别在于 Keyed State 事先按照key对数据集进行了分区,每个 Key State 仅对应ー个 Operator和Key的组合。Keyed State可以通过 Key Groups 进行管理,主要用于当算子并行度发生变化时,自动重新分布Keyed State数据。在系统运行过程中,一个Keyed算子实例可能运行一个或者多个Key Groups的keys。

(2)Operator State

与 Keyed State不同的是, Operator State只和并行的算子实例绑定,和数据元素中的key无关,每个算子实例中持有所有数据元素中的一部分状态数据。Operator State支持当算子实例并行度发生变化时自动重新分配状态数据。

同时在 Flink中 Keyed State和 Operator State均具有两种形式,其中一种为**托管状态( Managed State)形式,由 Flink Runtime中控制和管理状态数据,并将状态数据转换成为内存 Hash tables或 ROCKSDB的对象存储,然后将这些状态数据通过内部的接口持久化到 Checkpoints 中,任务异常时可以通过这些状态数据恢复任务。另外一种是原生状态(Raw State)**形式,由算子自己管理数据结构,当触发 Checkpoint过程中, Flink并不知道状态数据内部的数据结构,只是将数据转换成bys数据存储在 Checkpoints中,当从Checkpoints恢复任务时,算子自己再反序列化出状态的数据结构。Datastream API支持使用 Managed State和 Raw State两种状态形式,在 Flink中推荐用户使用 Managed State管理状态数据,主要原因是 Managed State 能够更好地支持状态数据的重平衡以及更加完善的内存管理。

状态描述

flink_StateDescriptor

State 既然是暴露给用户的,那么就需要有一些属性需要指定:state 名称、val serializer、state type info。在对应的statebackend中,会去调用对应的create方法获取到stateDescriptor中的值。Flink通过StateDescriptor来定义一个状态。这是一个抽象类,内部定义了状态名称、类型、序列化器等基础信息。与上面的状态对应,从StateDescriptor派生了ValueStateDescriptor, ListStateDescriptor等descriptor

  • ValueState getState(ValueStateDescriptor)
  • ReducingState getReducingState(ReducingStateDescriptor)
  • ListState getListState(ListStateDescriptor)
  • FoldingState getFoldingState(FoldingStateDescriptor)
  • MapState getMapState(MapStateDescriptor)

广播状态

什么是广播状态?

<img src="Flink大数据讲义.assets/image-20201009145618218.png" alt="image-20201009145618218" style="zoom:80%;" />

所有并行实例,这些实例将它们维持为状态。不广播另一个流的事件,而是将其发送到同一运营商的各个实例,并与广播流的事件一起处理。 新的广播状态非常适合需要加入低吞吐量和高吞吐量流或需要动态更新其处理逻辑的应用程序。我们将使用后一个用例的具体示例来解释广播状态

广播状态下的动态模式评估

想象一下,一个电子商务网站将所有用户的交互捕获为用户操作流。运营该网站的公司有兴趣分析交互以增加收入,改善用户体验,以及检测和防止恶意行为。 该网站实现了一个流应用程序,用于检测用户事件流上的模式。但是,公司希望每次模式更改时都避免修改和重新部署应用程序。相反,应用程序在从模式流接收新模式时摄取第二个模式流并更新其活动模式。在下文中,我们将逐步讨论此应用程序,并展示它如何利用Apache Flink中的广播状态功能。 img 我们的示例应用程序摄取了两个数据流。第一个流在网站上提供用户操作,并在上图的左上方显示。用户交互事件包括操作的类型(用户登录,用户注销,添加到购物车或完成付款)和用户的ID,其由颜色编码。图示中的用户动作事件流包含用户1001的注销动作,其后是用户1003的支付完成事件,以及用户1002的“添加到购物车”动作。

第二流提供应用将执行的动作模式。评估。模式由两个连续的动作组成。在上图中,模式流包含以下两个:

  1. 模式#1:用户登录并立即注销而无需浏览电子商务网站上的其他页面。
  2. 模式#2:用户将商品添加到购物车并在不完成购买的情况下注销。

这些模式有助于企业更好地分析用户行为,检测恶意行为并改善网站体验。例如,如果项目被添加到购物车而没有后续购买,网站团队可以采取适当的措施来更好地了解用户未完成购买的原因并启动特定程序以改善网站转换(如提供折扣代码,限时免费送货优惠等)

在右侧,该图显示了操作员的三个并行任务,即摄取模式和用户操作流,评估操作流上的模式,并在下游发出模式匹配。为简单起见,我们示例中的运算符仅评估具有两个后续操作的单个模式。当从模式流接收到新模式时,替换当前活动模式。原则上,还可以实现运算符以同时评估更复杂的模式或多个模式,这些模式可以单独添加或移除。

我们将描述模式匹配应用程序如何处理用户操作和模式流。

img

首先,将模式发送给操作员。该模式被广播到运营商的所有三个并行任务。任务将模式存储在其广播状态中。由于广播状态只应使用广播数据进行更新,因此所有任务的状态始终预期相同。

img

接下来,第一个用户操作按用户ID分区并发送到操作员任务。分区可确保同一用户的所有操作都由同一任务处理。上图显示了操作员任务消耗第一个模式和前三个操作事件后应用程序的状态。

当任务收到新的用户操作时,它会通过查看用户的最新和先前操作来评估当前活动的模式。对于每个用户,操作员将先前的操作存储在键控状态。由于上图中的任务到目前为止仅为每个用户收到了一个操作(我们刚刚启动了应用程序),因此不需要评估该模式。最后,用户键控状态中的先前操作被更新为最新动作,以便能够在同一用户的下一个动作到达时查找它。 img

在处理前三个动作之后,下一个事件(用户1001的注销动作)被运送到处理用户1001的事件的任务。当任务接收到动作时,它从广播状态中查找当前模式并且用户1001的先前操作。由于模式匹配两个动作,因此任务发出模式匹配事件。最后,任务通过使用最新操作覆盖上一个事件来更新其键控状态。 img

当新模式到达模式流时,它被广播到所有任务,并且每个任务通过用新模式替换当前模式来更新其广播状态。

img

一旦用新模式更新广播状态,匹配逻辑就像之前一样继续,即,用户动作事件由密钥分区并由负责任务评估。

如何使用广播状态实现应用程序?

到目前为止,我们在概念上讨论了该应用程序并解释了它如何使用广播状态来评估事件流上的动态模式。接下来,我们将展示如何使用Flink的DataStream API和广播状态功能实现示例应用程序。

让我们从应用程序的输入数据开始。我们有两个数据流,操作和模式。在这一点上,我们并不关心流来自何处。这些流可以从Apache Kafka或Kinesis或任何其他系统中摄取。并与各两个字段的POJO:

DataStream<Action> actions = ???`
`DataStream<Pattern> patterns = ???
Action``Pattern
  • ActionLong userIdString action
  • Pattern:,String firstAction``String secondAction

作为第一步,我们在属性上键入操作流。接下来,我们准备广播状态。广播状态始终表示为 Flink提供的最通用的状态原语。由于我们的应用程序一次只评估和存储一个,我们将广播状态配置为具有键类型和值类型。使用 广播状态,我们在流上应用转换并接收 。在我们获得了keyed Stream和广播流之后,我们都流式传输并应用了一个userId

package com.lagou.state;

import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.KeyedBroadcastProcessFunction;
import org.apache.flink.util.Collector;

public class BroadCastDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        //两套数据流,1:用户行为   2 : 模式
        UserAction ac1 = new UserAction(1001l, "login");
        UserAction ac2 = new UserAction(1003l, "pay");
        UserAction ac3 = new UserAction(1002l, "car");
        UserAction ac4 = new UserAction(1001l, "logout");
        UserAction ac5 = new UserAction(1003l, "car");
        UserAction ac6 = new UserAction(1002l, "logout");
        DataStreamSource<UserAction> actions = env.fromElements(ac1, ac2, ac3, ac4, ac5, ac6);

        MyPattern myPattern1 = new MyPattern("login", "logout");
        MyPattern myPattern2 = new MyPattern("car", "logout");
        DataStreamSource<MyPattern> patterns = env.fromElements(myPattern1);

        KeyedStream<UserAction, Long> keyed = actions.keyBy(value -> value.getUserId());


        //将模式流广播到下游的所有算子
        MapStateDescriptor<Void, MyPattern> bcStateDescriptor = new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class));
        BroadcastStream<MyPattern> broadcastPatterns = patterns.broadcast(bcStateDescriptor);


        SingleOutputStreamOperator<Tuple2<Long, MyPattern>> process = keyed.connect(broadcastPatterns).process(new PatternEvaluator());



        //将匹配成功的结果输出到控制台
        process.print();
        env.execute();

    }
    public static class PatternEvaluator extends KeyedBroadcastProcessFunction<Long,UserAction,MyPattern, Tuple2<Long,MyPattern>> {
        ValueState<String> prevActionState;

        @Override
        public void open(Configuration parameters) throws Exception {
            //初始化KeyedState
            prevActionState = getRuntimeContext().getState(new ValueStateDescriptor<String>("lastAction", Types.STRING));
        }

        //没来一个Action数据,触发一次执行
        @Override
        public void processElement(UserAction value, ReadOnlyContext ctx, Collector<Tuple2<Long, MyPattern>> out) throws Exception {
            //把用户行为流和模式流中的模式进行匹配
            ReadOnlyBroadcastState<Void, MyPattern> patterns = ctx.getBroadcastState(new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class)));
            MyPattern myPattern = patterns.get(null);
            String prevAction = prevActionState.value();
            if(myPattern != null && prevAction != null) {
                if (myPattern.getFirstAction().equals(prevAction) && myPattern.getSecondAction().equals(value.getUserAction())) {
                    //如果匹配成...
                    out.collect(new Tuple2<>(ctx.getCurrentKey(),myPattern));
                } else {
                    //如果匹配不成功...
                }
            }
            prevActionState.update(value.getUserAction());
        }

        //每次来一个模式Pattern的时候触发执行
        @Override
        public void processBroadcastElement(MyPattern value, Context ctx, Collector<Tuple2<Long, MyPattern>> out) throws Exception {
            BroadcastState<Void, MyPattern> bcstate = ctx.getBroadcastState(new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class)));
            bcstate.put(null,value);
        }
    }
}

  • processBroadcastElement()为广播流的每个记录调用。在我们的 函数中,我们只是使用键将接收到的记录放入广播状态(记住,我们只存储一个模式)。PatternEvaluator Pattern``null MapState
  • processElement()为键控流的每个记录调用。它提供对广播状态的只读访问,以防止修改导致跨函数的并行实例的不同广播状态。 从广播状态检索当前模式的方法和从键控状态检索用户的先前动作。如果两者都存在,则检查先前和当前操作是否与模式匹配,并且如果是这种情况则发出模式匹配记录。最后,它将键控状态更新为当前用户操作。processElement()``PatternEvaluator
  • onTimer()在先前注册的计时器触发时调用。定时器可以在任何处理方法中注册,并用于执行计算或将来清理状态。我们在示例中没有实现此方法以保持代码简洁。但是,当用户在一段时间内未处于活动状态时,它可用于删除用户的最后一个操作,以避免由于非活动用户而导致状态增长。

您可能已经注意到了处理方法的上下文对象。上下文对象提供对其他功能的访问,例如KeyedBroadcastProcessFunction

  • 广播状态(读写或只读,取决于方法),
  • A,可以访问记录的时间戳,当前的水印,以及可以注册计时器,TimerService
  • 当前密钥(仅适用于 ),和processElement()
  • 一种将函数应用于每个注册密钥的键控状态的方法(仅适用于)processBroadcastElement()

在具有就像任何其他ProcessFunction完全进入状态弗林克和时间特性,因此可以用来实现复杂的应用程序逻辑。广播状态被设计为适应不同场景和用例的多功能特性。虽然我们只讨论了一个相当简单且受限制的应用程序,但您可以通过多种方式使用广播状态来实现应用程序的要求。KeyedBroadcastProcessFunction

结论

在这篇博文中,我们向您介绍了一个示例应用程序,以解释Apache Flink的广播状态以及它如何用于评估事件流上的动态模式。我们还讨论了API并展示了我们的示例应用程序的源代码。

状态存储

Flink 的一个重要特性就是有状态计算(stateful processing)。Flink 提供了简单易用的 API 来存储和获取状态。但是,我们还是要理解 API 背后的原理,才能更好的使用。

一. State 存储方式

Flink 为 state 提供了三种开箱即用的后端存储方式(state backend):

  1. Memory State Backend
  2. File System (FS) State Backend
  3. RocksDB State Backend
1.1 MemoryStateBackend

MemoryStateBackend 将工作状态数据保存在 taskmanager 的 java 内存中。key/value 状态和 window 算子使用哈希表存储数值和触发器。进行快照时(checkpointing),生成的快照数据将和 checkpoint ACK 消息一起发送给 jobmanager,jobmanager 将收到的所有快照保存在 java 内存中。 MemoryStateBackend 现在被默认配置成异步的,这样避免阻塞主线程的 pipline 处理。 MemoryStateBackend 的状态存取的速度都非常快,但是不适合在生产环境中使用。这是因为 MemoryStateBackend 有以下限制:

  • 每个 state 的默认大小被限制为 5 MB(这个值可以通过 MemoryStateBackend 构造函数设置)
  • 每个 task 的所有 state 数据 (一个 task 可能包含一个 pipline 中的多个 Operator) 大小不能超过 RPC 系统的帧大小(akka.framesize,默认 10MB)
  • jobmanager 收到的 state 数据总和不能超过 jobmanager 内存

MemoryStateBackend 适合的场景:

  • 本地开发和调试
  • 状态很小的作业

下图表示了 MemoryStateBackend 的数据存储位置:

img

1.2 FsStateBackend

FsStateBackend 需要配置一个 checkpoint 路径,例如“hdfs://namenode:40010/flink/checkpoints” 或者 “file:///data/flink/checkpoints”,我们一般配置为 hdfs 目录 FsStateBackend 将工作状态数据保存在 taskmanager 的 java 内存中。进行快照时,再将快照数据写入上面配置的路径,然后将写入的文件路径告知 jobmanager。jobmanager 中保存所有状态的元数据信息(在 HA 模式下,元数据会写入 checkpoint 目录)。 FsStateBackend 默认使用异步方式进行快照,防止阻塞主线程的 pipline 处理。可以通过 FsStateBackend 构造函数取消该模式:

new FsStateBackend(path, false);

FsStateBackend 适合的场景:

  • 大状态、长窗口、大键值(键或者值很大)状态的作业
  • 适合高可用方案

@FsStateBackend state 存储位置 | center

1.3 RocksDBStateBackend

RocksDBStateBackend 也需要配置一个 checkpoint 路径,例如:“hdfs://namenode:40010/flink/checkpoints” 或者 “file:///data/flink/checkpoints”,一般配置为 hdfs 路径。 RocksDB 是一种可嵌入的持久型的 key-value 存储引擎,提供 ACID 支持。由 Facebook 基于 levelDB 开发,使用 LSM 存储引擎,是内存和磁盘混合存储。 RocksDBStateBackend 将工作状态保存在 taskmanager 的 RocksDB 数据库中;checkpoint 时,RocksDB 中的所有数据会被传输到配置的文件目录,少量元数据信息保存在 jobmanager 内存中( HA 模式下,会保存在 checkpoint 目录)。 RocksDBStateBackend 使用异步方式进行快照。 RocksDBStateBackend 的限制:

  • 由于 RocksDB 的 JNI bridge API 是基于 byte[] 的,RocksDBStateBackend 支持的每个 key 或者每个 value 的最大值不超过 2^31 bytes((2GB))。
  • 要注意的是,有 merge 操作的状态(例如 ListState),可能会在运行过程中超过 2^31 bytes,导致程序失败。

RocksDBStateBackend 适用于以下场景:

  • 超大状态、超长窗口(天)、大键值状态的作业
  • 适合高可用模式

使用 RocksDBStateBackend 时,能够限制状态大小的是 taskmanager 磁盘空间(相对于 FsStateBackend 状态大小限制于 taskmanager 内存 )。这也导致 RocksDBStateBackend 的吞吐比其他两个要低一些。因为 RocksDB 的状态数据的读写都要经过反序列化/序列化。

RocksDBStateBackend 是目前三者中唯一支持增量 checkpoint 的。

img

二. Keyed State & Operator State

2.1 state 分类
  • Operator State (或者non-keyed state ) 每个 Operator state 绑定一个并行 Operator 实例。Kafka Connector 是使用 Operator state 的典型示例:每个并行的 kafka consumer 实例维护了每个 kafka topic 分区和该分区 offset 的映射关系,并将这个映射关系保存为 Operator state。 在算子并行度改变时,Operator State 也会重新分配。

  • Keyed State 这种 State 只存在于 KeyedStream 上的函数和操作中,比如 Keyed UDF(KeyedProcessFunction…) window state 。可以把 Keyed State 想象成被分区的 Operator State。每个 Keyed State 在逻辑上可以看成与一个 <parallel-Operator-instance, key> 绑定,由于一个 key 肯定只存在于一个 Operator 实例,所以我们可以简单的认为一个 <operaor, key> 对应一个 Keyed State。 每个 Keyed State 在逻辑上还会被分配到一个 Key Group。分配方法如下:

// maxParallelism 为最大并行度
MathUtils.murmurHash(key.hashCode()) % maxParallelism;

其中 maxParallelism 是 flink 程序的最大并行度,这个值一般我们不会去手动设置,使用默认的值(128)就好,这里注意下,maxParallelism 和我们运行程序时指定的算子并行度(parallelism)不同,parallelism 不能大于 maxParallelism ,parallelism 最多只能设置为 maxParallelism 。 为什么会有 Key Group 这个概念呢?举个栗子,我们通常写程序,会给算子指定一个并行度,运行一段时间后,积累了一些 state ,这时候数据量大了,需要增大并行度;我们修改并行度后重新提交,那这些已经存在的 state 该如何分配到各个 Operator 呢?这就有了最大并行度(maxParallelism ) 和 Key Group 的概念。上面计算 Key Group 的公式也说明了 Key Group 的个数最多是 maxParallelism 个。当并行度更改后,我们再计算这个 key 被分配到的 Operator:

keyGroupId * parallelism / maxParallelism;

可以看到, 一个 keyGroupId 会对应到一个 Operator,当并行度更改时,新的 Operator 会去拉取对应 Key Group 的 Keyed State,这样就把 KeyedState 尽量均匀地分配给所有的 Operator 啦!

根据 state 数据是否被 flink 托管,flink 又将 state 分类为 managed state 和 raw state:

  • managed state: 被 flink 托管,保存为内部的哈希表或者 RocksDB; checkpoint 时,flink 将 state 进行序列化编码。例如 ValueState ListState…
  • raw state: Operator 自行管理的数据结构,checkpoint 时,它们只能以 byte 数组写入 checkpoint。

当然建议使用 managed state 啦!使用 managed state 时, flink 会帮我们在更改并行度时重新分发 state,并且优化内存。

2.2 使用 managed keyed state
如何创建

上面提到,Keyed state 只能在 keyedStream 上使用,可以通过 stream.keyBy(…) 创建 keyedStream。我们可以创建以下几种 keyed state:

  • ValueState <T>
  • ListState<T>
  • ReducingState<T>
  • AggregatingState<IN, OUT>
  • MapState<UK, UV>
  • FoldingState<T, ACC>

每种 state 都对应各自的描述符,通过描述符从 RuntimeContext 中获取对应的 State,而 RuntimeContext 只有 RichFunction 才能获取,所以要想使用 keyed state,用户编写的类必须继承 RichFunction 或者其子类。

  • ValueState<T> getState(ValueStateDescriptor<T>)
  • ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
  • ListState<T> getListState(ListStateDescriptor<T>)
  • AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)
  • FoldingState<T, ACC> getFoldingState(FoldingStateDescriptor<T, ACC>)
  • MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)

示例:


给 keyed state 设置过期时间

flink-1.6.0 以后,我们还可以给 Keyed state 设置 TTL(Time-To-Live),当某一个 key 的 state 数据过期时,会被 statebackend 尽力删除。 官方给出了使用示例:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // 状态存活时间
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) // TTL 何时被更新,这里配置的 state 创建和写入时
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();// 设置过期的 state 不被读取
    
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

简单来说就是在创建状态描述符时,添加 StateTtlConfig 配置,

state 的 TTL 何时被更新?

可以进行以下配置,默认只在 key 的 state 被 modify(创建或更新) 的时候才更新 TTL:

  • StateTtlConfig.UpdateType.OnCreateAndWrite: 只在一个 key 的 state 创建和写入时更新 TTL(默认)
  • StateTtlConfig.UpdateType.OnReadAndWrite: 读取 state 时仍然更新 TTL
当 state 过期但是还未删除时,这个状态是否还可见?

可以进行以下配置,默认是不可见的:

  • StateTtlConfig.StateVisibility.NeverReturnExpired: 不可见(默认)
  • StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp: 可见

注意:

  • 状态的最新访问时间会和状态数据保存在一起,所以开启 TTL 特性会增大 state 的大小。Heap state backend 会额外存储一个包括用户状态以及时间戳的 Java 对象,RocksDB state backend 会在每个状态值(list 或者 map 的每个元素)序列化后增加 8 个字节。
  • 暂时只支持基于 processing time 的 TTL。
  • 尝试从 checkpoint/savepoint 进行恢复时,TTL 的状态(是否开启)必须和之前保持一致,否则会遇到 “StateMigrationException”。
  • TTL 的配置并不会保存在 checkpoint/savepoint 中,仅对当前 Job 有效。
  • 当前开启 TTL 的 map state 仅在用户值序列化器支持 null 的情况下,才支持用户值为 null。如果用户值序列化器不支持 null, 可以用 NullableSerializer 包装一层。
过期的 state 何时被删除?

默认情况下,过期的 state 数据只有被显示读取的时候才会被删除,例如,调用 ValueState.value() 时。 注意:如果过期的数据如果之后不被读取,那么这个过期数据就不会被删除,可能导致状态不断增大。目前有两种方式解决这个问题:

1. 从全量快照恢复时删除

可以配置从全量快照恢复时删除过期数据:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // state 存活时间,这里设置的 1 秒过期
    .cleanupFullSnapshot()
    .build();

局限是正常运行的程序的过期状态还是无法删除,全量快照时,过期状态还是被备份了,只是在从上一个快照恢复时会过滤掉过期数据。

  • 注意:使用 RocksDB 增量快照时,该配置无效。
  • 这种清理方式可以在任何时候通过 StateTtlConfig 启用或者关闭,比如在从 savepoint 恢复时。

2. 后台程序删除(flink-1.8 之后的版本支持)

flink-1.8 引入了后台清理过期 state 的特性,通过 StateTtlConfig 开启,显式调用 cleanupInBackground(),使用示例如下:

import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // state 存活时间,这里设置的 1 秒过期
    .cleanupInBackground()
    .build();

官方介绍,使用 cleanupInBackground() 时,可以让不同 statebackend 自动选择 cleanupIncrementally(heap state backend) 或者 cleanupInRocksdbCompactFilter(rocksdb state backend) 策略进行后台清理。也就是说,不同的 statebackend 的具体清理过期 state 原理也是不一样的。而且,配置为 cleanupInBackground() 时,只能使用默认配置的参数。想要更改参数时,需要显式配置上面提到的两种清理方式,并且要和 statebackend 对应:

  • heap state backend 支持的增量清理 在状态访问或处理时进行。如果某个状态开启了该清理策略,则会在存储后端保留一个所有状态的惰性全局迭代器。 每次触发增量清理时,从迭代器中选择已经过期的进行清理。通过 StateTtlConfig 配置,显式调用 cleanupIncrementally():
import org.apache.flink.api.common.state.StateTtlConfig;
 StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupIncrementally(10, true)
    .build();
12345

使用 cleanupIncrementally() 策略时,当 state 被访问时会触发清理逻辑。 cleanupIncrementally() 包含两个参数:第一个参数表示每次清理被触发时,要检查的 state 条目个数;第二个参数表示是否在每条数据被处理时都触发清理逻辑。如果使用 cleanupInBackground() 的话,这里的默认值是(5, false)。 还有以下几点需要注意: a. 如果没有 state 访问,也没有处理数据,则不会清理过期数据。 b. 增量清理会增加数据处理的耗时。 c. 现在仅 Heap state backend 支持增量清除机制。在 RocksDB state backend 上启用该特性无效。 d. 如果 Heap state backend 使用同步快照方式,则会保存一份所有 key 的拷贝,从而防止并发修改问题,因此会增加内存的使用。但异步快照则没有这个问题。 e. 对已有的作业,这个清理方式可以在任何时候通过 StateTtlConfig 启用或禁用该特性,比如从 savepoint 重启后。

  • RocksDB 进行 compaction(压缩合并) 时清理 如果使用 RocksDB state backend,可以使用 Flink 为 RocksDB 定制的 compaction filter。RocksDB 会周期性的对数据进行异步合并压缩从而减少存储空间。 Flink 压缩过滤器会在压缩时过滤掉已经过期的状态数据。 该特性默认是关闭的,可以通过 Flink 的配置项 state.backend.rocksdb.ttl.compaction.filter.enabled 或者调用 RocksDBStateBackend::enableTtlCompactionFilter 启用该特性。然后通过如下方式让任何具有 TTL 配置的状态使用过滤器:
import org.apache.flink.api.common.state.StateTtlConfig;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupInRocksdbCompactFilter(1000)
    .build();
123456

使用这种策略需要注意: a. 压缩时调用 TTL 过滤器会降低速度。TTL 过滤器需要解析上次访问的时间戳,并对每个将参与压缩的状态进行是否过期检查。 对于集合型状态类型(比如 list 和 map),会对集合中每个元素进行检查。 b. 对于元素序列化后长度不固定的列表状态,TTL 过滤器需要在每次 JNI 调用过程中,额外调用 Flink 的 java 序列化器, 从而确定下一个未过期数据的位置。 c. 对已有的作业,这个清理方式可以在任何时候通过 StateTtlConfig 启用或禁用该特性,比如从 savepoint 重启后。

2.3 使用 managed operator state

我们可以通过实现 CheckpointedFunctionListCheckpointed<T extends Serializable> 接口来使用 managed operator state。

CheckpointedFunction

CheckpointedFunction 接口提供了访问 non-keyed state 的方法,需要实现如下两个方法:

void snapshotState(FunctionSnapshotContext context) throws Exception;

void initializeState(FunctionInitializationContext context) throws Exception;

进行 checkpoint 时会调用 snapshotState()。 用户自定义函数初始化时会调用 initializeState(),初始化包括第一次自定义函数初始化和从之前的 checkpoint 恢复。 因此 initializeState() 不仅是定义不同状态类型初始化的地方,也需要包括状态恢复的逻辑。

当前,managed operator state 以 list 的形式存在。这些状态是一个 可序列化 对象的集合 List,彼此独立,方便在改变并发后进行状态的重新分派。 换句话说,这些对象是重新分配 non-keyed state 的最细粒度。根据状态的不同访问方式,有如下几种重新分配的模式:

  • Even-split redistribution: 每个算子都保存一个列表形式的状态集合,整个状态由所有的列表拼接而成。当作业恢复或重新分配的时候,整个状态会按照算子的并发度进行均匀分配。 比如说,算子 A 的并发读为 1,包含两个元素 element1 和 element2,当并发读增加为 2 时,element1 会被分到并发 0 上,element2 则会被分到并发 1 上。
  • Union redistribution: 每个算子保存一个列表形式的状态集合。整个状态由所有的列表拼接而成。当作业恢复或重新分配时,每个算子都将获得所有的状态数据。
ListCheckpointed

ListCheckpointed 接口是 CheckpointedFunction 的精简版,仅支持 even-split redistributuion 的 list state。同样需要实现两个方法:

List<T> snapshotState(long checkpointId, long timestamp) throws Exception;

void restoreState(List<T> state) throws Exception;

snapshotState() 需要返回一个将写入到 checkpoint 的对象列表,restoreState 则需要处理恢复回来的对象列表。如果状态不可切分, 则可以在 snapshotState() 中返回 Collections.singletonList(MY_STATE)。

OperatorState 示例:实现带状态的 Sink Function

下面的例子中的 SinkFunction 在 CheckpointedFunction 中进行数据缓存,然后统一发送到下游,这个例子演示了列表状态数据的 event-split redistribution。

package com.lagou.bak;

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeutils.base.LongSerializer;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.checkpoint.ListCheckpointed;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.util.Collector;

import javax.swing.plaf.IconUIResource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 求平均值
 * (1,3)(1,5)(1,7)(1,4)(1,2)
 */
public class StateTest1 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();


        DataStreamSource<Tuple2<Long, Long>> data = env.fromElements(Tuple2.of(1l, 3l), Tuple2.of(1l, 5l), Tuple2.of(1l, 7l), Tuple2.of(1l, 4l), Tuple2.of(1l, 2l));
        KeyedStream<Tuple2<Long, Long>, Long> keyed = data.keyBy(value -> value.f0);
//        keyed.
//        keyed.print();
        /*
        * 为什么用RichFlatMapFunction?  首先需求是调用flatMap方法,所以应该用FlatMapFunction,但是FlatMapFunction跟源码发现只有flatMap方法。此处需要初始化一些东西,RichFlatMapFunction
        继承自AbstractRichFunction,有open方法.并且实现了FlatMapFunction接口。是FlatMapFunction的功能丰富的变体(比如多了open方法)
        在说说AbstractRichFunction,继承自RichFunction,又继承自Function。
         Function是用户自定义函数UDF的基础接口
        RichFunction提供了两个功能:1、Function的生命周期方法 2、提供了访问Function运行时上下文
        AbstractRichFunction顾名思义Abstract即为RichFunction接口的抽象实现类,功能为实现类提供基类功能
        两个待深入点:1、UDf 2、运行时上下文
        UDF:开发人员实现业务逻辑就是UDF
        RuntimeContext:对于每个Task而言,有更细节的配置信息,所以Flink又抽象出了RuntimeContext,每一个Task实例有自己的RuntimeContext,StreamExecutionEnvironment中配置信息和算子级别信息的综合。
        */

        SingleOutputStreamOperator<Tuple2<Long, Long>> flatMaped = keyed.flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
            private transient ValueState<Tuple2<Long, Long>> sum;

            @Override
            public void open(Configuration parameters) throws Exception {

//                ValueStateDescriptor<Long> count = new ValueStateDescriptor<>("count", LongSerializer.INSTANCE, 0L);
                System.out.println("...open");
                ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        })
                        , Tuple2.of(0L, 0L)
                );
//                ValueStateDescriptor<Tuple2<Long, Long>> descriptor1 = new ValueStateDescriptor<>("average", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
//                }));
                //RuntimeContext是Function运行时的上下文,包含了Function在运行时需要的所有信息,如并行度相关信息、Task名称、执行配置信息ExecutionConfig、State等
                sum = getRuntimeContext().getState(descriptor);
//                sum.update(new Tuple2<>(0L,0L));

            }

            @Override
            public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {

                //获取当前状态值
                Tuple2<Long, Long> currentSum = sum.value();

                //更新
                currentSum.f0 += 1;
                currentSum.f1 += value.f1;

                //更新状态值
                sum.update(currentSum);

                //如果count>=2 清空状态值,重新计算
                if(currentSum.f0 == 2) {
                    out.collect(new Tuple2<>(value.f0,currentSum.f1 / currentSum.f0));
                    sum.clear();
                }
            }


        });

        flatMaped.print();

//        flatMaped.addSink(new BufferingSink(1));

        env.execute();
    }
}

class BufferingSink implements SinkFunction<Tuple2<Long,Long>>, CheckpointedFunction{
    ListState<Tuple2<Long, Long>> checkpointedState;
    private List<Tuple2<Long,Long>> bufferedElements;
    private final int threshold;

    public BufferingSink(int threshold) {
        this.threshold = threshold;
        this.bufferedElements = new ArrayList<Tuple2<Long,Long>>();
    }

    // checkpoint 时会调用 snapshotState() 函数
    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        System.out.println("...snapshotState");
        // 清空 ListState,我们要放入最新的数据啦
        checkpointedState.clear();
        // 把当前局部变量中的所有元素写入到 checkpoint 中
        for (Tuple2<Long,Long> element : bufferedElements) {
            checkpointedState.add(element);
        }
    }

    // 需要处理第一次自定义函数初始化和从之前的 checkpoint 恢复两种情况
    // initializeState 方法接收一个 FunctionInitializationContext 参数,会用来初始化 non-keyed state 的 “容器”。这些容器是一个 ListState, 用于在 checkpoint 时保存 non-keyed state 对象。
    // 就是说我们可以通过 FunctionInitializationContext 获取 ListState 状态
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        System.out.println("...initializeState");
        // StateDescriptor 会包括状态名字、以及状态类型相关信息
        ListStateDescriptor<Tuple2<Long, Long>> descriptor = new ListStateDescriptor<>("buffered-elements", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
        }));
        // context.getOperatorStateStore().getListState(descriptor) 使用 even-split redistribution 算法
        // 我们还可以通过 context.getKeyedStateStore() 获取 keyed state,当然要在 keyedStream 上使用啦!
        checkpointedState = context.getOperatorStateStore().getListState(descriptor);
        // 需要处理从 checkpoint/savepoint 恢复的情况
        // 通过 isRestored() 方法判断是否从之前的故障中恢复回来,如果该方法返回 true 则表示从故障中进行恢复,会执行接下来的恢复逻辑
        if(context.isRestored()) {
            for(Tuple2<Long,Long> element : checkpointedState.get()) {
                bufferedElements.add(element);
            }
            System.out.println("....initializeState.bufferedElements:" + bufferedElements);
        }
    }

    @Override
    public void invoke(Tuple2<Long, Long> value, Context context) throws Exception {
        System.out.println("...invoke...value:" + value);
        // 把数据加入局部变量中
        bufferedElements.add(value);
        // 达到阈值啦!快发送
        if(bufferedElements.size() == threshold) {
            for (Tuple2<Long,Long> element : bufferedElements) {
                //// 这里实现发送逻辑
                System.out.println("...out:" + element);
            }
            // 发送完注意清空缓存
            bufferedElements.clear();
        }
    }
}

class CounterSource extends RichParallelSourceFunction<Long> implements ListCheckpointed<Long> {

    /**  current offset for exactly once semantics */
    private Long offset = 0L;

    /** flag for job cancellation */
    private volatile boolean isRunning = true;

    @Override
    public void run(SourceContext<Long> ctx) {
        final Object lock = ctx.getCheckpointLock();

        while (isRunning) {
            // output and state update are atomic
            synchronized (lock) {
                ctx.collect(offset);
                offset += 1;
            }
        }
    }

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

    @Override
    public List<Long> snapshotState(long checkpointId, long checkpointTimestamp) {
        return Collections.singletonList(offset);
    }

    @Override
    public void restoreState(List<Long> state) {
        for (Long s : state)
            offset = s;
    }
}



希望订阅 checkpoint 成功消息的算子,可以参考 org.apache.flink.runtime.state.CheckpointListener 接口。

2.4 statebackend 如何保存 managed keyed/operator state

上面我们详细介绍了三种 statebackend,那么这三种 statebackend 是如何托管 keyed state 和 Operator state 的呢? 参考很多资料并查阅源码后,感觉下面的图能简单明了的表示当前 flink state 的存储方式。

img

在 flink 的实际实现中,对于同一种 statebackend,不同的 state 在运行时会有细分的 statebackend 托管,例如 MemeoryStateBackend,就有 DefaultOperatorStateBackend 管理 Operator state,HeapKeydStateBackend 管理 Keyed state。我们看到 MemoryStateBackend 和 FsStateBackend 对于 keyed state 和 Operator state 的存储都符合我们之前的理解,运行时 state 数据保存于内存,checkpoint 时分别将数据备份在 jobmanager 内存和磁盘; RocksDBStateBackend 运行时 Operator state 的保存位置需要注意下,并不是保存在 RocksDB 中,而是通过 DefaultOperatorStateBackend 保存在 taskmanager 内存,创建源码如下:

// RocksDBStateBackend.java
// 创建 keyed statebackend
public <K> AbstractKeyedStateBackend<K> createKeyedStateBackend(...){
...
return new RocksDBKeyedStateBackend<>(
				...);
}
// 创建 Operator statebackend
public OperatorStateBackend createOperatorStateBackend(
			Environment env,
			String operatorIdentifier) throws Exception {

		//the default for RocksDB; eventually there can be a operator state backend based on RocksDB, too.
		final boolean asyncSnapshots = true;
		return new DefaultOperatorStateBackend(
				...);
	}

源码中也标注了,未来会提供基于 RocksDB 存储的 Operator state。所以当前即使使用 RocksDBStateBackend, Operator state 也不能超过内存限制。

Operator State 在内存中对应两种数据结构:

  • ListState: 对应的实际实现类为 PartitionableListState,创建并注册的代码如下
// DefaultOperatorStateBackend.java
private <S> ListState<S> getListState(...){
    partitionableListState = new PartitionableListState<>(
				new RegisteredOperatorStateBackendMetaInfo<>(
					name,
					partitionStateSerializer,
					mode));
	registeredOperatorStates.put(name, partitionableListState);
}
123456789

PartitionableListState 中通过 ArrayList 来保存 state 数据:

// PartitionableListState.java
/**
	 * The internal list the holds the elements of the state
	 */
	private final ArrayList<S> internalList;
12345
  • BroadcastState:对应的实际实现类为 HeapBroadcastState,创建并注册的代码如下
public <K, V> BroadcastState<K, V> getBroadcastState(...) {
    broadcastState = new HeapBroadcastState<>(
					new RegisteredBroadcastStateBackendMetaInfo<>(
							name,
							OperatorStateHandle.Mode.BROADCAST,
							broadcastStateKeySerializer,
							broadcastStateValueSerializer));
	registeredBroadcastStates.put(name, broadcastState);
}
123456789

HeapBroadcastState 中通过 HashMap 来保存 state 数据:

/**
	 * The internal map the holds the elements of the state.
	 */
	private final Map<K, V> backingMap;
	HeapBroadcastState(RegisteredBroadcastStateBackendMetaInfo<K, V> stateMetaInfo) {
		this(stateMetaInfo, new HashMap<>());
	}
1234567

我们对比下 HeapKeydStateBackend 和 RocksDBKeyedStateBackend 是如何保存 keyed state 的: @HeapKeydStateBackend | center

对于 HeapKeydStateBackend , state 数据被保存在一个由多层 java Map 嵌套而成的数据结构中。这个图表示的是 window 中的 keyed state 保存方式,而 window-contents 是 flink 中 window 数据的 state 描述符的名称,当然描述符类型是根据实际情况变化的。比如我们经常在 window 后执行聚合操作 (aggregate),flink 就有可能创建一个名字为 window-contents 的 AggregatingStateDescriptor:

// WindowedStream.java
AggregatingStateDescriptor<T, ACC, V> stateDesc = new AggregatingStateDescriptor<>("window-contents", aggregateFunction, accumulatorType.createSerializer(getExecutionEnvironment().getConfig()));
12

HeadKeyedStateBackend 会通过一个叫 StateTable 的数据结构,查找 key 对应的 StateMap:

// StateTable.java
/**
 * Map for holding the actual state objects. The outer array represents the key-groups.
 * All array positions will be initialized with an empty state map.
 */
protected final StateMap<K, N, S>[] keyGroupedStateMaps;
123456

根据是否开启异步 checkpoint,StateMap 会分别对应两个实现类:CopyOnWriteStateMap<K, N, S> 和 NestedStateMap<K, N, S>。 对于 NestedStateMap,实际存储数据如下:

// NestedStateMap.java
private final Map<N, Map<K, S>> namespaceMap;
12

CopyOnWriteStateMap 是一个支持 Copy-On-Write 的 StateMap 子类,实际上参考了 HashMap 的实现,它支持渐进式哈希(incremental rehashing) 和异步快照特性。

对于 RocksDBKeyedStateBackend,每个 state 存储在一个单独的 column family 内,KeyGroup、key、namespace 进行序列化存储在 DB 作为 key,状态数据作为 value。

三. 配置 state backend

我们知道 flink 提供了三个 state backend,那么如何配置使用某个 state backend 呢? 默认的配置在 conf/flink-conf.yaml 文件中 state.backend 指定,如果没有配置该值,就会使用 MemoryStateBackend。默认的 state backend 可以被代码中的配置覆盖。

3.1 Per-job 设置

我们可以通过 StreamExecutionEnvironment 设置:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints"));
12

如果想使用 RocksDBStateBackend,你需要将相关依赖加入你的 flink 程序中:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_2.11</artifactId>
    <version>${flink.version}</version>
    <scope>provided</scope>
</dependency>
123456
3.2 默认设置

如果没有在程序中指定,flink 将使用 conf/flink-conf.yaml 文件中的 state.backend 指定的 state backend,这个值有三种配置:

  • jobmanager (代表 MemoryStateBackend)
  • filesystem (代表 FsStateBackend)
  • rocksdb (代表 RocksDBStateBackend)

state.checkpoints.dir 定义了 checkpoint 时,state backend 将快照数据备份的目录

四. 开启 checkpoint

开启 checkpoint 后,state backend 管理的 taskmanager 上的状态数据才会被定期备份到 jobmanager 或 外部存储,这些状态数据在作业失败恢复时会用到。我们可以通过以下代码开启和配置 checkpoint:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//env.getConfig().disableSysoutLogging();
//每 30 秒触发一次 checkpoint,checkpoint 时间应该远小于(该值 + MinPauseBetweenCheckpoints),否则程序会一直做checkpoint,影响数据处理速度
env.enableCheckpointing(30000); // create a checkpoint every 30 seconds

// set mode to exactly-once (this is the default)
// flink 框架内保证 EXACTLY_ONCE 
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// make sure 30 s of progress happen between checkpoints
// 两个 checkpoints之间最少有 30s 间隔(上一个checkpoint完成到下一个checkpoint开始,默认为0,这里建议设置为非0值)
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);

// checkpoints have to complete within one minute, or are discarded
// checkpoint 超时时间(默认 600 s)
env.getCheckpointConfig().setCheckpointTimeout(600000);

// allow only one checkpoint to be in progress at the same time
// 同时只有一个checkpoint运行(默认)
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);

// enable externalized checkpoints which are retained after job cancellation
// 取消作业时是否保留 checkpoint (默认不保留)
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

// checkpoint失败时 task 是否失败( 默认 true, checkpoint失败时,task会失败)
env.getCheckpointConfig().setFailOnCheckpointingErrors(true);

// 对 FsStateBackend 刷出去的文件进行文件压缩,减小 checkpoint 体积
env.getConfig().setUseSnapshotCompression(true);
123456789101112131415161718192021222324252627282930

FsStateBackend 和 RocksDBStateBackend checkpoint 完成后最终保存到下面的目录:

 hdfs:///your/checkpoint/path/{JOB_ID}/chk-{CHECKPOINT_ID}/
1

JOB_ID 是应用的唯一 ID,CHECKPOINT_ID 是每次 checkpoint 时自增的数字 ID 我们可以从备份的 checkpoint 数据恢复当时的作业状态:

flink-1x.x/bin/flink run -s  hdfs:///your/checkpoint/path/{JOB_ID}/chk-{CHECKPOINT_ID}/ path/to//your/jar
1

我们可以实现 CheckpointedFunction 方法,在程序初始化或者 checkpoint 时修改状态:

public class StatefulProcess extends KeyedProcessFunction<String, KeyValue, KeyValue> implements CheckpointedFunction {
    ValueState<Integer> processedInt;


    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
    }

    @Override
    public void processElement(KeyValue keyValue, Context context, Collector<KeyValue> collector) throws Exception {
        try{
            Integer a =  Integer.parseInt(keyValue.getValue());
            processedInt.update(a);
            collector.collect(keyValue);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void initializeState(FunctionInitializationContext functionInitializationContext) throws Exception {
        processedInt = functionInitializationContext.getKeyedStateStore().getState(new ValueStateDescriptor<>("processedInt", Integer.class));
        if(functionInitializationContext.isRestored()){
            //Apply logic to restore the data
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
        processedInt.clear();
    }
}
123456789101112131415161718192021222324252627282930313233

五. state 文件格式

当我们创建 state 时,数据是如何保存的呢? 对于不同的 statebackend,有不同的存储格式。但是都是使用 flink 序列化器,将键值转化为字节数组保存起来。这里使用 RocksDBStateBackend 示例。 每个 taskmanager 会创建多个 RocksDB 目录,每个目录保存一个 RocksDB 数据库;每个数据库包含多个 column famiilies,这些 column families 由 state descriptors 定义。 每个 column family 包含多个 key-value 对,key 是 Operator 的 key, value 是对应的状态数据。 让我们看个例子程序:

// TestFlink.java
public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
    ParameterTool configuration = ParameterTool.fromArgs(args);
    
    FlinkKafkaConsumer010<String> kafkaConsumer010 = new FlinkKafkaConsumer010<String>("test", new SimpleStringSchema(), getKafkaConsumerProperties("testing123"));
    
    DataStream<String> srcStream = env.addSource(kafkaConsumer010);
    
    Random random = new Random();
    
    DataStream<String> outStream =  srcStream
            .map(row -> new KeyValue("testing" + random.nextInt(100000), row))
            .keyBy(row -> row.getKey())
            .process(new StatefulProcess()).name("stateful_process").uid("stateful_process")
            .keyBy(row -> row.getKey())
            .flatMap(new StatefulMapTest()).name("stateful_map_test").uid("stateful_map_test");
    
    outStream.print();
    env.execute("Test Job");
}

public static Properties getKafkaConsumerProperties(String groupId){
    Properties props = new Properties();
    props.setProperty("bootstrap.servers", "localhost:9092"
    );
    props.setProperty("group.id", groupId);

    return props;
}
12345678910111213141516171819202122232425262728293031

这个程序包含两个有状态的算子:

//StatefulMapTest.java
public class StatefulMapTest extends RichFlatMapFunction<KeyValue, String> {
    ValueState<Integer> previousInt;
    ValueState<Integer> nextInt;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        previousInt = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("previousInt", Integer.class));
        nextInt = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("nextInt", Integer.class));
    }

    @Override
    public void flatMap(KeyValue s, Collector<String> collector) throws Exception {
        try{
            Integer oldInt = Integer.parseInt(s.getValue());
            Integer newInt;
            if(previousInt.value() == null){
                newInt = oldInt;
                collector.collect("OLD INT: " + oldInt.toString());
            }else{
                newInt = oldInt - previousInt.value();
                collector.collect("NEW INT: " + newInt.toString());
            }
            nextInt.update(newInt);
            previousInt.update(oldInt);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
// StatefulProcess.java
public class StatefulProcess extends KeyedProcessFunction<String, KeyValue, KeyValue> {
    ValueState<Integer> processedInt;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        processedInt = getRuntimeContext().getState(new ValueStateDescriptor<>("processedInt", Integer.class));
    }

    @Override
    public void processElement(KeyValue keyValue, Context context, Collector<KeyValue> collector) throws Exception {
        try{
            Integer a =  Integer.parseInt(keyValue.getValue());
            processedInt.update(a);
            collector.collect(keyValue);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在 flink-conf.yaml 文件中设置 rocksdb 作为 state backend。每个 taskmanager 将在指定的 tmp 目录下(对于 onyarn 模式,tmp 目录由 yarn 指定,一般为 /path/to/nm-local-dir/usercache/user/appcache/application_xxx/flink-io-xxx),生成下面的目录:

drwxr-xr-x   4 abc  74715970   128B Sep 23 03:19 job_127b2b84f80b368b8edfe02b2762d10d_op_KeyedProcessOperator_0d49016af99997646695a030f69aa7ee__1_1__uuid_65b50444-5857-4940-9f8c-77326cc79279/db
drwxr-xr-x   4 abc  74715970   128B Sep 23 03:20 job_127b2b84f80b368b8edfe02b2762d10d_op_StreamFlatMap_11f49afc24b1cce91c7169b1e5140284__1_1__uuid_19b333d3-3278-4e51-93c8-ac6c3608507c/db

目录名含义如下: @rocksdb-dir | center 大致分为 3 部分:

  1. JOB_ID: JobGraph 创建时分配的随机 id
  2. OPERATOR_ID: 由 4 部分组成, 算子基类_Murmur3(算子 uid)_task索引_task总并行度。对于 StatefulMapTest 这个算子,4 个 部分分别为:
    • StreamFlatMap
    • Murmur3_128(“stateful_map_test”) -> 11f49afc24b1cce91c7169b1e5140284
    • 1,因为总并行度指定了1,所以只有这一个 task
    • 1,因为总并行度指定了1
  3. UUID: 随机的 UUID 值 每个目录都包含一个 RocksDB 实例,其文件结构如下:
-rw-r--r--  1 abc  74715970    21K Sep 23 03:20 000011.sst
-rw-r--r--  1 abc  74715970    21K Sep 23 03:20 000012.sst
-rw-r--r--  1 abc  74715970     0B Sep 23 03:36 000015.log
-rw-r--r--  1 abc  74715970    16B Sep 23 03:36 CURRENT
-rw-r--r--  1 abc  74715970    33B Sep 23 03:18 IDENTITY
-rw-r--r--  1 abc  74715970     0B Sep 23 03:33 LOCK
-rw-r--r--  1 abc  74715970    34K Sep 23 03:36 LOG
-rw-r--r--  1 abc  74715970   339B Sep 23 03:36 MANIFEST-000014
-rw-r--r--  1 abc  74715970    10K Sep 23 03:36 OPTIONS-000017
  • .sst 文件是 RocksDB 生成的 SSTable,包含真实的状态数据。
  • LOG 文件包含 commit log
  • MANIFEST 文件包含元数据信息,例如 column families
  • OPTIONS 文件包含创建 RocksDB 实例时使用的配置

我们通过 RocksDB java API 打开这些文件:

//FlinkRocksDb.java
public class FlinkRocksDb {
    public static void main(String[] args) throws Exception {
        RocksDB.loadLibrary();
        String previousIntColumnFamily = "previousInt";
        byte[] previousIntColumnFamilyBA = previousIntColumnFamily.getBytes(StandardCharsets.UTF_8);

        String nextIntcolumnFamily = "nextInt";
        byte[] nextIntcolumnFamilyBA = nextIntcolumnFamily.getBytes(StandardCharsets.UTF_8);
         try (final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) {

            // list of column family descriptors, first entry must always be default column family
            final List<ColumnFamilyDescriptor> cfDescriptors = Arrays.asList(
                    new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts),
                    new ColumnFamilyDescriptor(previousIntColumnFamilyBA, cfOpts),
                    new ColumnFamilyDescriptor(nextIntcolumnFamilyBA, cfOpts)
            );

            // a list which will hold the handles for the column families once the db is opened
            final List<ColumnFamilyHandle> columnFamilyHandleList = new ArrayList<>();

            String dbPath = "/Users/abc/job_127b2b84f80b368b8edfe02b2762d10d_op"+
            "_StreamFlatMap_11f49afc24b1cce91c7169b1e5140284__1_1__uuid_19b333d3-3278-4e51-93c8-ac6c3608507c/db/";
            try (final DBOptions options = new DBOptions()
                    .setCreateIfMissing(true)
                    .setCreateMissingColumnFamilies(true);

                 final RocksDB db = RocksDB.open(options, dbPath, cfDescriptors, columnFamilyHandleList)) {

                try {
                    for(ColumnFamilyHandle columnFamilyHandle : columnFamilyHandleList){
                    // 有些 rocksdb 版本去除了 getName 这个方法
                        byte[] name = columnFamilyHandle.getName();
                        System.out.write(name);
                    }
                }finally {
                    // NOTE frees the column family handles before freeing the db
                    for (final ColumnFamilyHandle columnFamilyHandle :
                            columnFamilyHandleList) {
                        columnFamilyHandle.close();
                    }
                }
            }
    } catch (Exception e) {
          e.printStackTrace();
    }
}

上面的程序将会输出:

default
previousInt
nextInt

我们可以打印出每个 column family 中的键值对:

// RocksdbKVIterator.java
TypeInformation<Integer> resultType = TypeExtractor.createTypeInfo(Integer.class);
TypeSerializer<Integer> serializer = resultType.createSerializer(new ExecutionConfig());

RocksIterator iterator =  db.newIterator(columnFamilyHandle);
iterator.seekToFirst();
iterator.status();

while (iterator.isValid()) {
    byte[] key = iterator.key();
    System.out.write(key);
    System.out.println(serializer.deserialize(new TestInputView(iterator.value())));
    iterator.next();
}

上面的程序将会输出键值对,如 (testing123, 1423), (testing456, 1212) …

第九部分:关于并行度的设置

一个Flink程序由多个Operator组成(source、transformation和 sink)。

一个Operator由多个并行的Task(线程)来执行, 一个Operator的并行Task(线程)数目就被称为该Operator(任务)的并行度(Parallel)

并行度可以有如下几种指定方式

1.Operator Level(算子级别)(可以使用)

一个算子、数据源和sink的并行度可以通过调用 setParallelism()方法来指定

actions.filter(new FilterFunction<UserAction>() {
            @Override
            public boolean filter(UserAction value) throws Exception {
                return false;
            }
        }).setParallelism(4);

2.Execution Environment Level(Env级别)(可以使用)

执行环境(任务)的默认并行度可以通过调用setParallelism()方法指定。为了以并行度3来执行所有的算子、数据源和data sink, 可以通过如下的方式设置执行环境的并行度:

执行环境的并行度可以通过显式设置算子的并行度而被重写

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);

3.Client Level(客户端级别,推荐使用)(可以使用)

并行度可以在客户端将job提交到Flink时设定。

对于CLI客户端,可以通过-p参数指定并行度

./bin/flink run -p 10 WordCount-java.jar

4.System Level(系统默认级别,尽量不使用)

在系统级可以通过设置flink-conf.yaml文件中的parallelism.default属性来指定所有执行环境的默认并行度

示例

image-20200921112124816

image-20200921112136826

Example1

在fink-conf.yaml中 taskmanager.numberOfTaskSlots 默认值为1,即每个Task Manager上只有一个Slot ,此处是3

Example1中,WordCount程序设置了并行度为1,意味着程序 Source、Reduce、Sink在一个Slot中,占用一个Slot

Example2

通过设置并行度为2后,将占用2个Slot

Example3

通过设置并行度为9,将占用9个Slot

Example4

通过设置并行度为9,并且设置sink的并行度为1,则Source、Reduce将占用9个Slot,但是Sink只占用1个Slot

注意

1.并行度的优先级:算子级别 > env级别 > Client级别 > 系统默认级别 (越靠前具体的代码并行度的优先级越高)

2.如果source不可以被并行执行,即使指定了并行度为多个,也不会生效

3.尽可能的规避算子的并行度的设置,因为并行度的改变会造成task的重新划分,带来shuffle问题,

4.推荐使用任务提交的时候动态的指定并行度

5.slot是静态的概念,是指taskmanager具有的并发执行能力; parallelism是动态的概念,是指程序运行时实际使用的并发能力

第十部分:Flink-Connector (Kafka)

第一节:源码理解

Funtion:UDF---处理数据的逻辑

RichFunction: open/close 管理函数的生命周期的方法 ...RunTimeContext函数的运行时上下文

SourceFunction: 提供了自定义数据源的功能,run方法是获取数据的方法

ParallelSourceFunction:

image-20201012113629573

创建一个新的流数据源消费者

Flink Kafka Consumer是一个流数据源,它从Apache Kafka提取并行数据流。使用者可以在多个并行实例中运行,每个实例将从一个或多个Kafka分区提取数据。

Flink Kafka消费者参与检查点并保证没有数据丢失

当出现故障时,计算过程只处理一次元素。

(注:这些保证自然假设Kafka本身不会丢失任何数据。)

请注意,Flink在内部快照偏移量,将其作为分布式检查点的一部分。提交到kafka上的offset只是为了使外部的outside view of progress与Flink的view of progress同步。通过这种方式,监视和其他工作可以了解Flink Kafka消费者在某个主题上消费了多少数据。

FlinkKafkaConsumerBase:

所有Flink Kafka Consumer数据源的基类。这个类实现了所有Kafka版本的公共行为

回顾自定义数据源---

open方法和run方法

image-20201012131740281

Flink-Kafka-Consumer:

package com.lagou.source;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;

import java.util.Properties;

public class FromKafka {
    public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            Properties properties = new Properties();
            properties.setProperty("bootstrap.servers", "teacher2:9092");

            FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>("mytopic", new SimpleStringSchema(), properties);
            //从最早开始消费
            consumer.setStartFromEarliest();
            DataStream<String> stream = env.addSource(consumer);
            stream.print();
            //stream.map();
            env.execute();


    }
}

flink-kafka 是如何消费的?以及如何分区分配等

open方法源码:

(1)指定offset提交模式

OffsetCommitMode:

OffsetCommitMode:表示偏移量如何从外部提交回Kafka brokers/ Zookeeper的行为

它的确切值是在运行时在使用者子任务中确定的。

image-20201012132333714

  • DISABLED:完全禁用offset提交。
  • ON_CHECKPOINTS:只有当检查点完成时,才将偏移量提交回Kafka。
  • KAFKA_PERIODIC:使用内部Kafka客户端的自动提交功能,定期将偏移量提交回Kafka。

image-20201012133036183

使用多个配置值确定偏移量提交模式

如果启用了checkpoint,并且启用了checkpoint完成时提交offset,返回ON_CHECKPOINTS。

如果未启用checkpoint,但是启用了自动提交,返回KAFKA_PERIODIC。

其他情况都返回DISABLED。

(2)

接下来创建和启动分区发现工具

image-20201012133633643

创建用于为此子任务查找新分区的分区发现程序。

参数1:topicsDescriptor : 描述我们是为固定主题还是主题模式发现分区,也就是fixedTopics和topicPattern的封装。其中fixedTopics明确指定了topic的名称,称为固定topic。topicPattern为匹配topic名称的正则表达式,用于分区发现。

image-20201012133917211

参数2:indexOfThisSubtask :此consumer子任务的索引。

参数3:numParallelSubtasks : 并行consumer子任务的总数

方法返回一个分区发现器的实例

(3)

打开分区发现程序,初始化所有需要的Kafka连接。

image-20201012134652814

注意是线程不安全的

初始化所有需要的Kafka链接源码:

image-20201012134925865

KafkaPartitionDiscoverer:

image-20201012134941777

创建出KafkaConsumer对象。

(4)

subscribedPartitionsToStartOffsets = new HashMap<>();

已订阅的分区列表,这里将它初始化

private Map<KafkaTopicPartition, Long> subscribedPartitionsToStartOffsets;

用来保存将读取的一组主题分区,以及要开始读取的初始偏移量。

(5)

用户获取所有fixedTopics和匹配topicPattern的Topic包含的所有分区信息

image-20201012141209322

(6)

如果consumer从检查点恢复状态,restoredState用来保存要恢复的偏移量

选择TreeMap数据类型,目的是有序

image-20201012150718485

在initializeState实例化方法中填充:

image-20201012151532274

回顾:context.isRestored的机制:

当程序发生故障的时候值为true

image-20201012151426149

if (restoredState != null) {
// 从快照恢复逻辑...
} else {
// 直接启动逻辑...
}

如果restoredState没有存储某一分区的状态, 需要重头消费该分区

image-20201012141822620

过滤掉不归该subtask负责的partition分区

image-20201012142445184

assign方法:

返回应该分配给特定Kafka分区的目标子任务的索引

image-20201012142846614

subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());

将restoredState中保存的一组topic的partition和要开始读取的起始偏移量保存到subscribedPartitionsToStartOffsets

其中restoredStateEntry.getKey为某个Topic的摸个partition,restoredStateEntry.getValue为该partition的要开始读取的起始偏移量

过滤掉topic名称不符合topicsDescriptor的topicPattern的分区

image-20201012150056860

(7) 直接启动consumer

image-20201012152143992

该枚举类型有5个值:

  • GROUP_OFFSETS:从保存在zookeeper或者是Kafka broker的对应消费者组提交的offset开始消费,这个是默认的配置
  • EARLIEST:尽可能从最早的offset开始消费
  • LATEST:从最近的offset开始消费
  • TIMESTAMP:从用户提供的timestamp处开始消费
  • SPECIFIC_OFFSETS:从用户提供的offset处开始消费

根据startup mode,获取从哪个地方开始消费。然后,partition discoverer就会拉取初始分区的数据

image-20201012152514274

如果startup模式为SPECIFIC_OFFSETS:

异常情况:如果没有配置具体从哪个offset开始消费

正常情况:获取每个分区指定的消费起始offset

Long specificOffset = specificStartupOffsets.get(seedPartition);

image-20201012153043847

image-20201012164509269

Run方法:

(1) 判断保存分区和读取起始偏移量的集合是否为空:

image-20201012211020853

(2)

记录Kafka offset成功提交和失败提交的数量

image-20201012210848272

(3)

获取当前自任务的索引

image-20201012211250222

image-20201012211151422

(4)

注册一个提交时的回调函数,提交成功时,提交成功计数器加一;提交失败时,提交失败计数器加一

image-20201012211416696

(5)

接下来判断subscribedPartitionsToStartOffsets集合是否为空。如果为空,标记数据源的状态为暂时空闲。

image-20201012211656663

(6)创建一个KafkaFetcher,借助KafkaConsumer API从Kafka的broker拉取数据

image-20201012212051017

(7)

根据分区发现间隔时间,来确定是否启动分区定时发现任务

如果没有配置分区定时发现时间间隔,则直接启动获取数据任务;否则,启动定期分区发现任务和数据获取任务

image-20201012212354702

循环拉取数据源码:

image-20201012212916899

createAndStartDiscoveryLoop:启动分区发现任务的方法:

image-20201012213547827

尝试发现新的分区:

image-20201012213627808

将发现的新分区添加到kafkaFetcher中

image-20201012213721209

启动分区发现定时任务

image-20201012213919489

partitionDiscoverer.discoverPartitions()的调用,即发现分区的执行过程。

image-20201012214319780

image-20201012214636507

image-20201012214844411

kafkaFetcher的runFetchLoop方法

此方法为FlinkKafkaConsumer获取数据的主入口,通过一个循环来不断获取kafka broker的数据。

image-20201012215703725

KafkaConsumerThread线程的run方法实例化handover

image-20201012215942755

回到KafkaFecher类中的runFetchLoop方法

image-20201012220346826

image-20201012221406546

partitionConsumerRecordsHandler方法

image-20201012222153359

@Override
	public void open(Configuration configuration) throws Exception {
		// determine the offset commit mode 
		// 指定offset的提交模式:   DISABLED、 ON_CHECKPOINTS 、KAFKA_PERIODIC
		this.offsetCommitMode = OffsetCommitModes.fromConfiguration(
				getIsAutoCommitEnabled(),
				enableCommitOnCheckpoints,
				((StreamingRuntimeContext) getRuntimeContext()).isCheckpointingEnabled());

		// create the partition discoverer
		//  创建一个分区发现器
		this.partitionDiscoverer = createPartitionDiscoverer(
				topicsDescriptor,
				getRuntimeContext().getIndexOfThisSubtask(),
				getRuntimeContext().getNumberOfParallelSubtasks());
		// 实例化出 consumer对象
		this.partitionDiscoverer.open();

		// 已经订阅的分区列表
		subscribedPartitionsToStartOffsets = new HashMap<>();
		// 获取kafka中的所有分区
		final List<KafkaTopicPartition> allPartitions = partitionDiscoverer.discoverPartitions();
		if (restoredState != null) {
			//restoredState: 快照  consumer是从快照中恢复的方式创建
			for (KafkaTopicPartition partition : allPartitions) {
				if (!restoredState.containsKey(partition)) {
					restoredState.put(partition, KafkaTopicPartitionStateSentinel.EARLIEST_OFFSET);
				}
			}

			for (Map.Entry<KafkaTopicPartition, Long> restoredStateEntry : restoredState.entrySet()) {
				if (!restoredFromOldState) {
					// seed the partition discoverer with the union state while filtering out
					// restored partitions that should not be subscribed by this subtask
					// 过滤一下和当前的subTask没有关系的分区数据
					if (KafkaTopicPartitionAssigner.assign(
						restoredStateEntry.getKey(), getRuntimeContext().getNumberOfParallelSubtasks())
						== getRuntimeContext().getIndexOfThisSubtask()){
						subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());
					}
				} else {
					// when restoring from older 1.1 / 1.2 state, the restored state would not be the union state;
					// in this case, just use the restored state as the subscribed partitions
					subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());
				}
			}

			LOG.info("Consumer subtask {} will start reading {} partitions with offsets in restored state: {}",
				getRuntimeContext().getIndexOfThisSubtask(), subscribedPartitionsToStartOffsets.size(), subscribedPartitionsToStartOffsets);
		} else {
			//重新创建一个新的consumer
			// use the partition discoverer to fetch the initial seed partitions,
			// and set their initial offsets depending on the startup mode.
			// for SPECIFIC_OFFSETS and TIMESTAMP modes, we set the specific offsets now;
			// for other modes (EARLIEST, LATEST, and GROUP_OFFSETS), the offset is lazily determined
			// when the partition is actually read.
			switch (startupMode) {
			//startupMode : consumer的消费策略
				case SPECIFIC_OFFSETS:
					if (specificStartupOffsets == null) {
						throw new IllegalStateException(
							"Startup mode for the consumer set to " + StartupMode.SPECIFIC_OFFSETS +
								", but no specific offsets were specified.");
					}

					for (KafkaTopicPartition seedPartition : allPartitions) {
						Long specificOffset = specificStartupOffsets.get(seedPartition);
						if (specificOffset != null) {
							// since the specified offsets represent the next record to read, we subtract
							// it by one so that the initial state of the consumer will be correct
							subscribedPartitionsToStartOffsets.put(seedPartition, specificOffset - 1);
						} else {
							// default to group offset behaviour if the user-provided specific offsets
							// do not contain a value for this partition
							subscribedPartitionsToStartOffsets.put(seedPartition, KafkaTopicPartitionStateSentinel.GROUP_OFFSET);
						}
					}

					break;
				case TIMESTAMP:
					if (startupOffsetsTimestamp == null) {
						throw new IllegalStateException(
							"Startup mode for the consumer set to " + StartupMode.TIMESTAMP +
								", but no startup timestamp was specified.");
					}

					for (Map.Entry<KafkaTopicPartition, Long> partitionToOffset
						: fetchOffsetsWithTimestamp(allPartitions, startupOffsetsTimestamp).entrySet()) {
						subscribedPartitionsToStartOffsets.put(
							partitionToOffset.getKey(),
							(partitionToOffset.getValue() == null)
								// if an offset cannot be retrieved for a partition with the given timestamp,
								// we default to using the latest offset for the partition
								? KafkaTopicPartitionStateSentinel.LATEST_OFFSET
								// since the specified offsets represent the next record to read, we subtract
								// it by one so that the initial state of the consumer will be correct
								: partitionToOffset.getValue() - 1);
					}

					break;
				default:
					for (KafkaTopicPartition seedPartition : allPartitions) {
						subscribedPartitionsToStartOffsets.put(seedPartition, startupMode.getStateSentinel());
					}
			}

			if (!subscribedPartitionsToStartOffsets.isEmpty()) {
				switch (startupMode) {
					case EARLIEST:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the earliest offsets: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case LATEST:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the latest offsets: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case TIMESTAMP:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from timestamp {}: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							startupOffsetsTimestamp,
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case SPECIFIC_OFFSETS:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the specified startup offsets {}: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							specificStartupOffsets,
							subscribedPartitionsToStartOffsets.keySet());

						List<KafkaTopicPartition> partitionsDefaultedToGroupOffsets = new ArrayList<>(subscribedPartitionsToStartOffsets.size());
						for (Map.Entry<KafkaTopicPartition, Long> subscribedPartition : subscribedPartitionsToStartOffsets.entrySet()) {
							if (subscribedPartition.getValue() == KafkaTopicPartitionStateSentinel.GROUP_OFFSET) {
								partitionsDefaultedToGroupOffsets.add(subscribedPartition.getKey());
							}
						}

						if (partitionsDefaultedToGroupOffsets.size() > 0) {
							LOG.warn("Consumer subtask {} cannot find offsets for the following {} partitions in the specified startup offsets: {}" +
									"; their startup offsets will be defaulted to their committed group offsets in Kafka.",
								getRuntimeContext().getIndexOfThisSubtask(),
								partitionsDefaultedToGroupOffsets.size(),
								partitionsDefaultedToGroupOffsets);
						}
						break;
					case GROUP_OFFSETS:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the committed group offsets in Kafka: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
				}
			} else {
				LOG.info("Consumer subtask {} initially has no partitions to read from.",
					getRuntimeContext().getIndexOfThisSubtask());
			}
		}
	}

该方法包含的内容为FlinkKafkaConsumer的初始化逻辑。

首先设置提交offset的模式。

接下来创建和启动分区发现工具。

subscribedPartitionsToStartOffsets 为已订阅的分区列表,这里将它初始化。

run:

kafka-console-producer.sh --broker-list teacher2:9092 --topic mytopic

1.1 消费策略

  • setStartFromGroupOffsets()【默认消费策略】

    默认读取上次保存的offset信息 如果是应用第一次启动,读取不到上次的offset信息,则会根据这个参数auto.offset.reset的值来进行消费数据

  • setStartFromEarliest() 从最早的数据开始进行消费,忽略存储的offset信息

  • setStartFromLatest() 从最新的数据进行消费,忽略存储的offset信息

  • setStartFromSpecificOffsets(Map<KafkaTopicPartition, Long>) 从指定位置进行消费

  • 当checkpoint机制开启的时候,KafkaConsumer会定期把kafka的offset信息还有其他operator的状态信息一块保存起来。当job失败重启的时候,Flink会从最近一次的checkpoint中进行恢复数据,重新消费kafka中的数据。

  • 为了能够使用支持容错的kafka Consumer,需要开启checkpoint env.enableCheckpointing(5000); // 每5s checkpoint一次

1.2 Kafka consumer offset自动提交:

kafka consumer offset自动提交的配置需要根据job是否开启checkpoint来区分

checkpoint关闭时:

checkpoint开启时:

如果启用了checkpoint,并且启用了checkpoint完成时提交offset,返回ON_CHECKPOINTS。

如果未启用checkpoint,但是启用了自动提交,返回KAFKA_PERIODIC。

其他情况都返回DISABLED。

OffsetCommitMode是一个枚举类型,具有如下三个值:

  • DISABLED:完全禁用offset提交。
  • ON_CHECKPOINTS:当checkpoint完成的时候再提交offset。
  • KAFKA_PERIODIC:周期性提交offset。

Flink kafka Producer

nc

代码接受nc

把接收到的nc的数据,给到kafka flink kafka producer

代码:

package com.lagou.sink;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;

import java.util.Properties;

public class SinkToKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers","teacher2:9092");
        FlinkKafkaProducer producer = new FlinkKafkaProducer("teacher2:9092", "mytopic2", new SimpleStringSchema());
        data.addSink(producer);
        env.execute();
    }
}

十一部分 Flink CEP

CEP 即Complex Event Processing - 复杂事件处理,Flink CEP 是在 Flink 中实现的复杂时间处理(CEP)库。处理事件的规则,被叫做“模式”(Pattern),Flink CEP 提供了 Pattern API,用于对输入流数据进行复杂事件规则定义,用来提取符合规则的事件序列。  Pattern API 大致分为三种:个体模式,组合模式,模式组。

Flink CEP 应用场景:

CEP 在互联网各个行业都有应用,例如金融、物流、电商、智能交通、物联网行业等行业:

实时监控:

在网站的访问日志中寻找那些使用脚本或者工具“爆破”登录的用户;

我们需要在大量的订单交易中发现那些虚假交易(超时未支付)或发现交易活跃用户;

或者在快递运输中发现那些滞留很久没有签收的包裹等。

风险控制:

比如金融行业可以用来进行风险控制和欺诈识别,从交易信息中寻找那些可能存在的危险交易和非法交易。

营销广告:

跟踪用户的实时行为,指定对应的推广策略进行推送,提高广告的转化率。

1、基础

(1)定义 复合事件处理(Complex Event Processing,CEP)是一种基于动态环境中事件流的分析技术,事件在这里通常是有意义的状态变化,通过分析事件间的关系,利用过滤、关联、聚合等技术,根据事件间的时序关系和聚合关系制定检测规则,持续地从事件流中查询出符合要求的事件序列,最终分析得到更复杂的复合事件。 (2)特征 CEP的特征如下: 目标:从有序的简单事件流中发现一些高阶特征; 输入:一个或多个简单事件构成的事件流; 处理:识别简单事件之间的内在联系,多个符合一定规则的简单事件构成复杂事件; 输出:满足规则的复杂事件。 在这里插入图片描述

(3)功能 CEP用于分析低延迟、频繁产生的不同来源的事件流。CEP可以帮助在复杂的、不相关的时间流中找出有意义的模式和复杂的关系,以接近实时或准实时的获得通知或组织一些行为。 CEP支持在流上进行模式匹配,根据模式的条件不同,分为连续的条件或不连续的条件;模式的条件允许有时间的限制,当条件范围内没有达到满足的条件时,会导致模式匹配超时。 看起来很简单,但是它有很多不同的功能: ① 输入的流数据,尽快产生结果; ② 在2个事件流上,基于时间进行聚合类的计算; ③ 提供实时/准实时的警告和通知; ④ 在多样的数据源中产生关联分析模式; ⑤ 高吞吐、低延迟的处理 市场上有多种CEP的解决方案,例如Spark、Samza、Beam等,但他们都没有提供专门的库支持。然而,Flink提供了专门的CEP库。 (4)主要组件 Flink为CEP提供了专门的Flink CEP library,它包含如下组件:Event Stream、Pattern定义、Pattern检测和生成Alert。 首先,开发人员要在DataStream流上定义出模式条件,之后Flink CEP引擎进行模式检测,必要时生成警告。 在这里插入图片描述

2、 Pattern API

处理事件的规则,被叫作模式(Pattern)。 Flink CEP提供了Pattern API用于对输入流数据进行复杂事件规则定义,用来提取符合规则的事件序列。 模式大致分为三类: ① 个体模式(Individual Patterns) 组成复杂规则的每一个单独的模式定义,就是个体模式。

start.times(3).where(_.behavior.startsWith(‘fav’))

② 组合模式(Combining Patterns,也叫模式序列) 很多个体模式组合起来,就形成了整个的模式序列。 模式序列必须以一个初始模式开始:

val start = Pattern.begin(‘start’)

③ 模式组(Group of Pattern) 将一个模式序列作为条件嵌套在个体模式里,成为一组模式。

2.1 个体模式

个体模式包括单例模式和循环模式。单例模式只接收一个事件,而循环模式可以接收多个事件。

(1)量词 可以在一个个体模式后追加量词,也就是指定循环次数。

// 匹配出现4次
start.time(4)
// 匹配出现0次或4次
start.time(4).optional
// 匹配出现2、3或4次
start.time(2,4)
// 匹配出现2、3或4次,并且尽可能多地重复匹配
start.time(2,4).greedy
// 匹配出现1次或多次
start.oneOrMore
// 匹配出现0、2或多次,并且尽可能多地重复匹配
start.timesOrMore(2).optional.greedy

(2)条件 每个模式都需要指定触发条件,作为模式是否接受事件进入的判断依据。CEP中的个体模式主要通过调用.where()、.or()和.until()来指定条件。按不同的调用方式,可以分成以下几类: ① 简单条件 通过.where()方法对事件中的字段进行判断筛选,决定是否接收该事件

start.where(event=>event.getName.startsWith(“foo”))

② 组合条件 将简单的条件进行合并;or()方法表示或逻辑相连,where的直接组合就相当于与and。

Pattern.where(event => …/*some condition*/).or(event => /*or condition*/)

③ 终止条件 如果使用了oneOrMore或者oneOrMore.optional,建议使用.until()作为终止条件,以便清理状态。 ④ 迭代条件 能够对模式之前所有接收的事件进行处理;调用.where((value,ctx) => {…}),可以调用ctx.getEventForPattern(“name”)

2.2 模式序列

不同的近邻模式如下图: 在这里插入图片描述

(1)严格近邻 所有事件按照严格的顺序出现,中间没有任何不匹配的事件,由.next()指定。例如对于模式“a next b”,事件序列“a,c,b1,b2”没有匹配。 (2)宽松近邻 允许中间出现不匹配的事件,由.followedBy()指定。例如对于模式“a followedBy b”,事件序列“a,c,b1,b2”匹配为{a,b1}。 (3)非确定性宽松近邻 进一步放宽条件,之前已经匹配过的事件也可以再次使用,由.followedByAny()指定。例如对于模式“a followedByAny b”,事件序列“a,c,b1,b2”匹配为{ab1},{a,b2}。 除了以上模式序列外,还可以定义“不希望出现某种近邻关系”: .notNext():不想让某个事件严格紧邻前一个事件发生。 .notFollowedBy():不想让某个事件在两个事件之间发生。 需要注意

①所有模式序列必须以.begin()开始;

②模式序列不能以.notFollowedBy()结束;

③“not”类型的模式不能被optional所修饰;

④可以为模式指定时间约束,用来要求在多长时间内匹配有效。

next.within(Time.seconds(10))

2.3 模式的检测

指定要查找的模式序列后,就可以将其应用于输入流以检测潜在匹配。调用CEP.pattern(),给定输入流和模式,就能得到一个PatternStream。

val input:DataStream[Event] = …
val pattern:Pattern[Event,_] = …
val patternStream:PatternStream[Event]=CEP.pattern(input,pattern)

2.4 匹配事件的提取

创建PatternStream之后,就可以应用select或者flatSelect方法,从检测到的事件序列中提取事件了。 select()方法需要输入一个select function作为参数,每个成功匹配的事件序列都会调用它。 select()以一个Map[String,Iterable[IN]]来接收匹配到的事件序列,其中key就是每个模式的名称,而value就是所有接收到的事件的Iterable类型。

def selectFn(pattern : Map[String,Iterable[IN]]):OUT={
  val startEvent = pattern.get(“start”).get.next
  val endEvent = pattern.get(“end”).get.next
  OUT(startEvent, endEvent)
}

flatSelect通过实现PatternFlatSelectFunction实现与select相似的功能。唯一的区别就是flatSelect方法可以返回多条记录,它通过一个Collector[OUT]类型的参数来将要输出的数据传递到下游。

process

select

2.5超时事件的提取

当一个模式通过within关键字定义了检测窗口时间时,部分事件序列可能因为超过窗口长度而被丢弃;为了能够处理这些超时的部分匹配,select和flatSelect API调用允许指定超时处理程序。

Flink CEP 开发流程:

  1. DataSource 中的数据转换为 DataStream;
  2. 定义 Pattern,并将 DataStream 和 Pattern 组合转换为 PatternStream;
  3. PatternStream 经过 select、process 等算子转换为 DataStraem;
  4. 再次转换的 DataStream 经过处理后,sink 到目标库。  

select方法:

SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
    @Override
    public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
        return map.get("begin").get(0);
    }
}, new PatternSelectFunction<PayEvent, PayEvent>() {
    @Override
    public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
        return map.get("pay").get(0);
    }
});

对检测到的模式序列应用选择函数。对于每个模式序列,调用提供的{@link PatternSelectFunction}。模式选择函数只能产生一个结果元素。

对超时的部分模式序列应用超时函数。对于每个部分模式序列,调用提供的{@link PatternTimeoutFunction}。模式超时函数只能产生一个结果元素。

您可以在使用相同的{@link OutputTag}进行select操作的{@link SingleOutputStreamOperator}上获得由{@link SingleOutputStreamOperator}生成的{@link SingleOutputStreamOperator}生成的超时数据流。

@param timedOutPartialMatchesTag 标识端输出超时模式的@link OutputTag}

@param patternTimeoutFunction 为超时的每个部分模式序列调用的模式超时函数。

@param patternSelectFunction 为每个检测到的模式序列调用的模式选择函数。

@param <L> 产生的超时元素的类型

@param <R>结果元素的类型

return {@link DataStream},其中包含产生的元素和在边输出中产生的超时元素。

DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);

获取{@link DataStream},该{@link DataStream}包含由操作发出到指定{@link OutputTag}的边输出的元素。

3、NFA:非确定有限自动机

FlinkCEP在运行时会将用户的逻辑转化成这样的一个NFA Graph (nfa对象)

所以有限状态机的工作过程,就是从开始状态,根据不同的输入,自动进行状态转换的过程。

img

上图中的状态机的功能,是检测二进制数是否含有偶数个 0。从图上可以看出,输入只有 1 和 0 两种。从 S1 状态开始,只有输入 0 才会转换到 S2 状态,同样 S2 状态下只有输入 0 才会转换到 S1。所以,二进制数输入完毕,如果满足最终状态,也就是最后停在 S1 状态,那么输入的二进制数就含有偶数个 0。

4、案例

Flink CEP 开发流程:

  1. DataSource 中的数据转换为 DataStream;watermark、keyby
  2. 定义 Pattern,并将 DataStream 和 Pattern 组合转换为 PatternStream;
  3. PatternStream 经过 select、process 等算子转换为 DataStream;
  4. 再次转换的 DataStream 经过处理后,sink 到目标库。

案例1:恶意登录检测

需求:找出5秒内,连续登录失败的账号

思路:

1、数据源

​ new CEPLoginBean(1L, "fail", 1597905234000L), ​ new CEPLoginBean(1L, "success", 1597905235000L), ​ new CEPLoginBean(2L, "fail", 1597905236000L), ​ new CEPLoginBean(2L, "fail", 1597905237000L), ​ new CEPLoginBean(2L, "fail", 1597905238000L), ​ new CEPLoginBean(3L, "fail", 1597905239000L), ​ new CEPLoginBean(3L, "success", 1597905240000L)

2、在数据源上做出watermark

3、在watermark上根据id分组keyby

4、做出模式pattern

 Pattern<CEPLoginBean, CEPLoginBean> pattern = Pattern.<CEPLoginBean>begin("start").where(new IterativeCondition<CEPLoginBean>() {
            @Override
            public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                return value.getLogresult().equals("fail");
            }
        })
                .next("next").where(new IterativeCondition<CEPLoginBean>() {
                    @Override
                    public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                        return value.getLogresult().equals("fail");
                    }
                })
                .within(Time.seconds(5));

5、在数据流上进行模式匹配

6、提取匹配成功的数据

代码:

依赖:

 <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep_2.12</artifactId>
            <version>1.11.1</version>
        </dependency>

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
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;

import java.util.List;
import java.util.Map;

public class CEPLoginTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);
        DataStreamSource<CEPLoginBean> data = env.fromElements(
                new CEPLoginBean(1L, "fail", 1597905234000L),
                new CEPLoginBean(1L, "success", 1597905235000L),
                new CEPLoginBean(2L, "fail", 1597905236000L),
                new CEPLoginBean(2L, "fail", 1597905237000L),
                new CEPLoginBean(2L, "fail", 1597905238000L),
                new CEPLoginBean(3L, "fail", 1597905239000L),
                new CEPLoginBean(3L, "success", 1597905240000L)
        );

        SingleOutputStreamOperator<CEPLoginBean> watermarks = data.assignTimestampsAndWatermarks(new WatermarkStrategy<CEPLoginBean>() {
            @Override
            public WatermarkGenerator<CEPLoginBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                return new WatermarkGenerator<CEPLoginBean>() {
                    long maxTimeStamp = Long.MIN_VALUE;

                    @Override
                    public void onEvent(CEPLoginBean event, long eventTimestamp, WatermarkOutput output) {
                        maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
                    }

                    long maxOutOfOrderness = 500L;

                    @Override
                    public void onPeriodicEmit(WatermarkOutput output) {
                        output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
                    }
                };
            }
        }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())));

        KeyedStream<CEPLoginBean, Long> keyed = watermarks.keyBy(value -> value.getId());

        Pattern<CEPLoginBean, CEPLoginBean> pattern = Pattern.<CEPLoginBean>begin("start").where(new IterativeCondition<CEPLoginBean>() {
            @Override
            public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                return value.getLogresult().equals("fail");
            }
        })
                .next("next").where(new IterativeCondition<CEPLoginBean>() {
                    @Override
                    public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                        return value.getLogresult().equals("fail");
                    }
                })
                .within(Time.seconds(5));

        PatternStream<CEPLoginBean> patternStream = CEP.pattern(keyed, pattern);
        SingleOutputStreamOperator<String> process = patternStream.process(new PatternProcessFunction<CEPLoginBean, String>() {
            @Override
            public void processMatch(Map<String, List<CEPLoginBean>> match, Context ctx, Collector<String> out) throws Exception {
                System.out.println(match);
                List<CEPLoginBean> start = match.get("start");
                List<CEPLoginBean> next = match.get("next");
                String res = "start:" + start + "...next:" + next;
                out.collect(res + start.get(0).getId());
            }
        });

        process.print();

        env.execute();
    }
}



案例2:检测交易活跃用户

需求:找出24小时内,至少5次有效交易的用户:

思路:

1、数据源:

				new ActiveUserBean("100XX", 0.0D, 1597905234000L),
                new ActiveUserBean("100XX", 100.0D, 1597905235000L),
                new ActiveUserBean("100XX", 200.0D, 1597905236000L),
                new ActiveUserBean("100XX", 300.0D, 1597905237000L),
                new ActiveUserBean("100XX", 400.0D, 1597905238000L),
                new ActiveUserBean("100XX", 500.0D, 1597905239000L),
                new ActiveUserBean("101XX", 0.0D, 1597905240000L),
                new ActiveUserBean("101XX", 100.0D, 1597905241000L)

2、watermark转化

3、keyby转化

4、做出pattern

至少5次:timesOrMore(5)

24小时之内:within(Time.hours(24))

 Pattern<ActiveUserBean, ActiveUserBean> pattern = Pattern.<ActiveUserBean>begin("start").where(new SimpleCondition<ActiveUserBean>() {
            @Override
            public boolean filter(ActiveUserBean value) throws Exception {
                return value.getMoney() > 0;
            }
        }).timesOrMore(5).within(Time.hours(24));;

5、模式匹配

6、提取匹配成功数据

代码:

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.util.List;
import java.util.Map;

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

        DataStreamSource<ActiveUserBean> data = env.fromElements(
                new ActiveUserBean("100XX", 0.0D, 1597905234000L),
                new ActiveUserBean("100XX", 100.0D, 1597905235000L),
                new ActiveUserBean("100XX", 200.0D, 1597905236000L),
                new ActiveUserBean("100XX", 300.0D, 1597905237000L),
                new ActiveUserBean("100XX", 400.0D, 1597905238000L),
                new ActiveUserBean("100XX", 500.0D, 1597905239000L),
                new ActiveUserBean("101XX", 0.0D, 1597905240000L),
                new ActiveUserBean("101XX", 100.0D, 1597905241000L)
        );
        SingleOutputStreamOperator<ActiveUserBean> watermark = data.assignTimestampsAndWatermarks(new WatermarkStrategy<ActiveUserBean>() {
            @Override
            public WatermarkGenerator<ActiveUserBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                return new WatermarkGenerator<ActiveUserBean>() {
                    long maxTimeStamp = Long.MIN_VALUE;

                    @Override
                    public void onEvent(ActiveUserBean event, long eventTimestamp, WatermarkOutput output) {
                        maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
                    }

                    long maxOutOfOrderness = 500l;

                    @Override
                    public void onPeriodicEmit(WatermarkOutput output) {
                        output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
                    }
                };
            }
        }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())));
        KeyedStream<ActiveUserBean, String> keyed = watermark.keyBy(value -> value.getUid());
        Pattern<ActiveUserBean, ActiveUserBean> pattern = Pattern.<ActiveUserBean>begin("start").where(new SimpleCondition<ActiveUserBean>() {
            @Override
            public boolean filter(ActiveUserBean value) throws Exception {
                return value.getMoney() > 0;
            }
        }).timesOrMore(5).within(Time.hours(24));

        PatternStream<ActiveUserBean> patternStream = CEP.pattern(keyed, pattern);
        SingleOutputStreamOperator<ActiveUserBean> process = patternStream.process(new PatternProcessFunction<ActiveUserBean, ActiveUserBean>() {
            @Override
            public void processMatch(Map<String, List<ActiveUserBean>> match, Context ctx, Collector<ActiveUserBean> out) throws Exception {
                System.out.println(match);
            }
        });
        process.print();
        env.execute();

    }
}

案例3:超时未支付

需求:找出下单后10分钟没有支付的订单

思路:

1、数据源:

				new PayEvent(1L, "create", 1597905234000L),
                new PayEvent(1L, "pay", 1597905235000L),
                new PayEvent(2L, "create", 1597905236000L),
                new PayEvent(2L, "pay", 1597905237000L),
                new PayEvent(3L, "create", 1597905239000L)

2、转化watermark

3、keyby转化

4、做出Pattern(下单以后10分钟内未支付)

注意:下单为create 支付为pay ,create和pay之间不需要是严格临近,所以选择followedBy

Pattern<PayEvent, PayEvent> pattern = Pattern.<PayEvent>
                begin("begin")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("create");
                    }
                })
                .followedBy("pay")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("pay");
                    }
                })
                .within(Time.seconds(600));

5、模式匹配

6、取出匹配成功的数据

(1)采用测输出的方式

OutputTag<PayEvent> orderTimeoutOutput = new OutputTag<PayEvent>("orderTimeout") {};

(2)采用select方法

SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
                return map.get("begin").get(0);
            }
        }, new PatternSelectFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
                return map.get("pay").get(0);
            }
        });

        //result.print();
        DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);
        sideOutput.print();

代码:

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.PatternTimeoutFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.OutputTag;

import java.util.List;
import java.util.Map;

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

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        DataStream<PayEvent> source = env.fromElements(
                new PayEvent(1L, "create", 1597905234000L),
                new PayEvent(1L, "pay", 1597905235000L),
                new PayEvent(2L, "create", 1597905236000L),
                new PayEvent(2L, "pay", 1597905237000L),
                new PayEvent(3L, "create", 1597905239000L)

        )
//                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<PayEvent>(Time.milliseconds(500L)) {
//            @Override
//            public long extractTimestamp(PayEvent payEvent) {
//                return payEvent.getTs();
//            }
//        })

                .assignTimestampsAndWatermarks(new WatermarkStrategy<PayEvent>() {
                    @Override
                    public WatermarkGenerator<PayEvent> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                        return new WatermarkGenerator<PayEvent>() {
                            long maxTimestamp = Long.MIN_VALUE;

                            @Override
                            public void onEvent(PayEvent event, long eventTimestamp, WatermarkOutput output) {
                                maxTimestamp = Math.max(maxTimestamp, event.getTs());
                            }

                            @Override
                            public void onPeriodicEmit(WatermarkOutput output) {
                                long maxOutofOrderness = 500l;
                                output.emitWatermark(new Watermark(maxTimestamp - maxOutofOrderness));
                            }
                        };
                    }
                }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())))

                /*.keyBy(new KeySelector<PayEvent, Object>() {
                    @Override
                    public Object getKey(PayEvent value) throws Exception {
                        return value.getId();
                    }
                }*/
                .keyBy(value -> value.getId()
                );

        // 逻辑处理代码
        OutputTag<PayEvent> orderTimeoutOutput = new OutputTag<PayEvent>("orderTimeout") {
        };
        Pattern<PayEvent, PayEvent> pattern = Pattern.<PayEvent>
                begin("begin")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("create");
                    }
                })
                .followedBy("pay")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("pay");
                    }
                })
                .within(Time.seconds(600));

        PatternStream<PayEvent> patternStream = CEP.pattern(source, pattern);
        SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
                return map.get("begin").get(0);
            }
        }, new PatternSelectFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
                return map.get("pay").get(0);
            }
        });

        //result.print();
        DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);
        sideOutput.print();


        env.execute("execute cep");
    }

}

十二部分 FlinkSQL

1、什么是 Table API 和 Flink SQL

Flink 本身是批流统一的处理框架,所以 Table API 和 SQL,就是批流统一的上层处理 API。

Table API 是一套内嵌在 Java 和 Scala 语言中的查询 API,它允许我们以非常直观的方式,

组合来自一些关系运算符的查询(比如 select、filter 和 join)。而对于 Flink SQL,就是直接可

以在代码中写 SQL,来实现一些查询(Query)操作。Flink 的 SQL 支持,基于实现了 SQL 标

准的 Apache Calcite(Apache 开源 SQL 解析工具)。

无论输入是批输入还是流式输入,在这两套 API 中,指定的查询都具有相同的语义,得到相同的结果

2、入门代码:

依赖:

 		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table</artifactId>
            <version>1.11.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>

        <!-- Either... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- or... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>

依赖说明:

flink-table-api-java-bridge_2.1:桥接器,主要负责 table API 和 DataStream/DataSet API

的连接支持,按照语言分 java 和 scala。

flink-table-planner-blink_2.12:计划器,是 table API 最主要的部分,提供了运行时环境和生

成程序执行计划的 planner;

如果是生产环境,lib 目录下默认已 经有了 planner,就只需要有 bridge 就可以了

flink-table:flinktable的基础依赖

代码:

1、Flink执行环境env

2、用env,做出Table环境tEnv

StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

基于 blink 版本的流处理环境(Blink-Streaming-Query)或者,基于 blink 版本的批处理环境(Blink-Batch-Query):

		EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .useBlinkPlanner()
//                .inBatchMode()
                .inStreamingMode()
                .build();

3、获取流式数据源

DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });

4、将流式数据源做成Table

(1)table方式:

Table table = tEnv.fromDataStream(data, $("name"), $("age"));

(2)sql方式:

		tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);

5、对Table中的数据做查询

(1)table方式:

Table name = table.select($("name"));

(2)sql方式:

tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);

6、将Table转成数据流:

DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);
package com.lagou.table;

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.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import org.apache.flink.util.CloseableIterator;

import static org.apache.flink.table.api.Expressions.$;

public class TableApiDemo {
    public static void main(String[] args) throws Exception {
        //Flink执行环境env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //用env,做出Table环境tEnv
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
        //获取流式数据源
        DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });
        //Table方式
        //将流式数据源做成Table
        Table table = tEnv.fromDataStream(data, $("name"), $("age"));
        
        //对Table中的数据做查询
        Table name = table.select($("name"));
        //将处理结果输出到控制台
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);

        //SQL方式:
        /*tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(table, Row.class);*/

        result.print();
        env.execute();

    }
}

3、外部链接

Connectors

Name Version Maven dependency SQL Client JAR
Filesystem Built-in Built-in
Elasticsearch 6 flink-connector-elasticsearch6 Download
Elasticsearch 7 flink-connector-elasticsearch7 Download
Apache Kafka 0.10 flink-connector-kafka-0.10 Download
Apache Kafka 0.11 flink-connector-kafka-0.11 Download
Apache Kafka 0.11+ (universal) flink-connector-kafka Download
Apache HBase 1.4.3 flink-connector-hbase Download
JDBC flink-connector-jdbc Download

Formats

Name Maven dependency SQL Client JAR
Old CSV (for files) Built-in Built-in
CSV (for Kafka) flink-csv Built-in
JSON flink-json Built-in
Apache Avro flink-avro Download

1. 数据查询语言DQL 数据查询语言DQL基本结构是由SELECT子句,FROM子句,WHERE 子句组成的查询块: SELECT <字段名表> FROM <表或视图名> WHERE <查询条件>

2 .数据操纵语言DML 数据操纵语言DML主要有三种形式:

  1. 插入:INSERT
  2. 更新:UPDATE
  3. 删除:DELETE

3. 数据定义语言DDL 数据定义语言DDL用来创建数据库中的各种对象-----表、视图、 索引、同义词、聚簇等如: CREATE TABLE/VIEW/INDEX/SYN/CLUSTER 表 视图 索引 同义词 簇

DDL操作是隐性提交的!不能rollback

4. 数据控制语言DCL 数据控制语言DCL用来授予或回收访问数据库的某种特权,并控制 数据库操纵事务发生的时间及效果,对数据库实行监视等。如:

连接外部系统在 Catalog 中注册表,直接调用 tableEnv.connect()就可以,里面参数要传

入一个 ConnectorDescriptor,也就是 connector 描述器。对于文件系统的 connector 而言,

flink 内部已经提供了,就叫做 FileSystem()。

<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-csv</artifactId>
            <version>1.11.1</version>
        </dependency>
        tEnv.connect(new FileSystem().path("sensor.txt"))// 定义表数据来源,外部连接
                .withFormat(new Csv()) // 定义从外部系统读取数据之后的格式化方法
                .withSchema(new Schema()
                        .field("id", DataTypes.STRING())
                        .field("timestamp", DataTypes.BIGINT())
                        .field("temperature", DataTypes.DOUBLE())) // 定义表结构
                .createTemporaryTable("inputTable"); // 创建临时表

连接Kafka:

ConnectTableDescriptor descriptor = tEnv.connect(
                // declare the external system to connect to
                new Kafka()
                        .version("universal")
                        .topic("animal")
                        .startFromEarliest()
                        .property("bootstrap.servers", "hdp-2:9092")
        )

                // declare a format for this system
                .withFormat(
//                        new Json()
                        new Csv()
                )

                // declare the schema of the table
                .withSchema(
                        new Schema()
//                                .field("rowtime", DataTypes.TIMESTAMP(3))
//                                .rowtime(new Rowtime()
//                                        .timestampsFromField("timestamp")
//                                        .watermarksPeriodicBounded(60000)
//                                )
//                                .field("user", DataTypes.BIGINT())
                                .field("message", DataTypes.STRING())
                );
        // create a table with given name
        descriptor.createTemporaryTable("MyUserTable");

        Table table1 = tEnv.sqlQuery("select * from MyUserTable");
        DataStream<Tuple2<Boolean, Row>> tuple2DataStream = tEnv.toRetractStream(table1, Row.class);
        tuple2DataStream.print();

4、查询数据

4.1 Table API

官网:https://ci.apache.org/projects/flink/flink-docs-release-1.11/dev/table/tableApi.html

select/filter/as

Table filtered = table.select($("name"), $("age")).filter($("age").mod(2).isEqual(0));
        //将处理结果输出到控制台
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(filtered, Row.class);
 Table mingzi = table.select($("name").as("mingzi"));
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(mingzi, Row.class);

4.2 SQL

 tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name,age from userss where mod(age,2)=0";
        Table table = tEnv.sqlQuery(s);
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(table, Row.class);

5、输出表

5.1 输出到文件:

代码:

tEnv.connect(new FileSystem().path("D:\\data\\out.txt"))
                .withFormat(new Csv())
                .withSchema(new Schema().field("name", DataTypes.STRING()).field("age",DataTypes.INT()))
                .createTemporaryTable("outputTable");
        filtered.executeInsert("outputTable");

hive支持的输出到orc

package com.lagou.bak;

import org.apache.flink.core.fs.Path;
import org.apache.flink.orc.OrcSplitReaderUtil;
import org.apache.flink.orc.vector.RowDataVectorizer;
import org.apache.flink.orc.writer.OrcBulkWriterFactory;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.types.logical.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.orc.TypeDescription;

import java.util.Properties;

public class StreamingWriteFileOrc {
    public static void main(String[] args) throws Exception{
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10000);
        env.setParallelism(1);
        DataStream<RowData> dataStream = env.addSource(
                new MySource());

        //写入orc格式的属性
        final Properties writerProps = new Properties();
        writerProps.setProperty("orc.compress", "LZ4");

        //定义类型和字段名
        LogicalType[] orcTypes = new LogicalType[]{
                new IntType(), new DoubleType(), new VarCharType()};
        String[] fields = new String[]{"a1", "b2", "c3"};
        TypeDescription typeDescription = OrcSplitReaderUtil.logicalTypeToOrcType(RowType.of(
                orcTypes,
                fields));

        //构造工厂类OrcBulkWriterFactory
        final OrcBulkWriterFactory<RowData> factory = new OrcBulkWriterFactory<RowData>(
                new RowDataVectorizer(typeDescription.toString(), orcTypes),
                writerProps,
                new Configuration());

        StreamingFileSink orcSink = StreamingFileSink
                .forBulkFormat(new Path("d:\\data\\out"), factory)//  file:///tmp/aaaa
                .build();

        dataStream.addSink(orcSink);

        env.execute();
    }

    public static class MySource implements SourceFunction<RowData> {
        @Override
        public void run(SourceContext<RowData> sourceContext) throws Exception{
            while (true){
                GenericRowData rowData = new GenericRowData(3);
                rowData.setField(0, (int) (Math.random() * 100));
                rowData.setField(1, Math.random() * 100);
                rowData.setField(2, org.apache.flink.table.data.StringData.fromString(String.valueOf(Math.random() * 100)));
                sourceContext.collect(rowData);
                Thread.sleep(10);
            }
        }

        @Override
        public void cancel(){

        }
    }
}

5.2 输出到Kafka

定义

//往kafka上输出表
        DataStreamSource<String> data = env.addSource(new SourceFunction<String>() {
            @Override
            public void run(SourceContext<String> ctx) throws Exception {
                int num = 0;
                while (true) {
                    num++;
                    ctx.collect("name"+num);
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });

        Table name = tEnv.fromDataStream(data, $("name"));

        ConnectTableDescriptor descriptor = tEnv.connect(
                // declare the external system to connect to
                new Kafka()
                        .version("universal")
                        .topic("animal")
                        .startFromEarliest()
                        .property("bootstrap.servers", "hdp-2:9092")
        )

                // declare a format for this system
                .withFormat(
//                        new Json()
                        new Csv()
                )

                // declare the schema of the table
                .withSchema(
                        new Schema()
//                                .field("rowtime", DataTypes.TIMESTAMP(3))
//                                .rowtime(new Rowtime()
//                                        .timestampsFromField("timestamp")
//                                        .watermarksPeriodicBounded(60000)
//                                )
//                                .field("user", DataTypes.BIGINT())
                                .field("message", DataTypes.STRING())
                );
        // create a table with given name
        descriptor.createTemporaryTable("MyUserTable");

        name.executeInsert("MyUserTable");

5.3 输出到mysql (了解)

CREATE TABLE MyUserTable (
  ...
) WITH (
  'connector.type' = 'jdbc', -- required: specify this table type is jdbc
  
  'connector.url' = 'jdbc:mysql://localhost:3306/flink-test', -- required: JDBC DB url
  
  'connector.table' = 'jdbc_table_name',  -- required: jdbc table name

  -- optional: the class name of the JDBC driver to use to connect to this URL.
  -- If not set, it will automatically be derived from the URL.
  'connector.driver' = 'com.mysql.jdbc.Driver',

  -- optional: jdbc user name and password
  'connector.username' = 'name',
  'connector.password' = 'password',
  
  -- **followings are scan options, optional, used when reading from a table**

  -- optional: SQL query / prepared statement.
  -- If set, this will take precedence over the 'connector.table' setting
  'connector.read.query' = 'SELECT * FROM sometable',

  -- These options must all be specified if any of them is specified. In addition,
  -- partition.num must be specified. They describe how to partition the table when
  -- reading in parallel from multiple tasks. partition.column must be a numeric,
  -- date, or timestamp column from the table in question. Notice that lowerBound and
  -- upperBound are just used to decide the partition stride, not for filtering the
  -- rows in table. So all rows in the table will be partitioned and returned.

  'connector.read.partition.column' = 'column_name', -- optional: the column name used for partitioning the input.
  'connector.read.partition.num' = '50', -- optional: the number of partitions.
  'connector.read.partition.lower-bound' = '500', -- optional: the smallest value of the first partition.
  'connector.read.partition.upper-bound' = '1000', -- optional: the largest value of the last partition.

  -- optional, Gives the reader a hint as to the number of rows that should be fetched
  -- from the database when reading per round trip. If the value specified is zero, then
  -- the hint is ignored. The default value is zero.
  'connector.read.fetch-size' = '100',

  -- **followings are lookup options, optional, used in temporary join**

  -- optional, max number of rows of lookup cache, over this value, the oldest rows will
  -- be eliminated. "cache.max-rows" and "cache.ttl" options must all be specified if any
  -- of them is specified. Cache is not enabled as default.
  'connector.lookup.cache.max-rows' = '5000',

  -- optional, the max time to live for each rows in lookup cache, over this time, the oldest rows
  -- will be expired. "cache.max-rows" and "cache.ttl" options must all be specified if any of
  -- them is specified. Cache is not enabled as default.
  'connector.lookup.cache.ttl' = '10s',

  'connector.lookup.max-retries' = '3', -- optional, max retry times if lookup database failed

  -- **followings are sink options, optional, used when writing into table**

  -- optional, flush max size (includes all append, upsert and delete records),
  -- over this number of records, will flush data. The default value is "5000".
  'connector.write.flush.max-rows' = '5000',

  -- optional, flush interval mills, over this time, asynchronous threads will flush data.
  -- The default value is "0s", which means no asynchronous flush thread will be scheduled.
  'connector.write.flush.interval' = '2s',

  -- optional, max retry times if writing records to database failed
  'connector.write.max-retries' = '3'
)

代码:


第十三分 作业提交

Flink的jar文件并不是Flink集群的可执行文件,需要经过转换之后提交给集群

转换过程:

1、在Flink Client中,通过反射启动jar中的main函数,生成Flink StreamGraph和JobGraph。将JobGraph提交给Flink集群。

2、Flink集群收到JobGraph后,将JobGraph翻译成ExecutionGraph,然后开始调度执行,启动成功之后开始消费数据

总结:

Flink的核心执行流程就是,把用户的一系列API调用,转化为StreamGraph -- JobGraph -- ExecutionGraph -- 物理执行拓扑(Task DAG)

image-20201017174411777

Flink提交作业的核心过程图

PipelineExecutor:流水线执行器:

是Flink Client生成JobGraph之后,将作业提交给集群运行的重要环节

image-20201017174842456

Session模式:AbstractSessionClusterExecutor

Per-Job模式:AbstractJobClusterExecutor

IDE调试:LocalExecutor

Session模式:

作业提交通过: yarn-session.sh脚本

在启动脚本的时候检查是否已经存在已经启动好的Flink-Session模式的集群,

然后在PipelineExecutor中,通过Dispatcher提供的Rest接口提交Flink JobGraph

Dispatcher为每一个作业提供一个JobMaser,进入到作业执行阶段

Per-Job模式:一个作业一个集群,作业之间相互隔离。

在PipelineExecutor执行作业提交的时候,可以创建集群并将JobGraph以及所有需要的文件一起提交给Yarn集群,在Yarn集群的容器中启动Flink Master(JobManager进程),进行初始化后,从文件系统中获取JobGraph,交给Dispatcher,之后和Session流程相同。

流图:

image-20201017191746914

img

<img src="Flink大数据讲义.assets/1275415-20201012210627328-264459047.png" alt="img" style="zoom:150%;" />

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部