Flink、Flink SQL学习笔记

文章目录

碎碎念

  1. Flink 中每一个 TaskManager 都是一个JVM进程。
  2. slot控制一个 TaskManager 能接收多少个 task。
  3. 每个task slot表示TaskManager拥有资源的一个固定大小的子集。slot仅仅用来隔离task的受管理内存。
  4. 可以通过调整task slot的数量去自定义subtask之间的隔离方式。如一个TaskManager一个slot时,那么每个task group运行在独立的JVM中。而当一个TaskManager多个slot时,多个subtask可以共同享有一个JVM,而在同一个JVM进程中的task将共享TCP连接和心跳消息,也可能共享数据集和数据结构,从而减少每个task的负载
  5. 默认情况下,Flink 允许同一个Job的子任务共享 slot。
  6. Task Slot 是静态的概念,是指 TaskManager 具有的并发执行能力;而并行度parallelism是动态概念,即TaskManager运行程序时实际使用的并发能力。

1. Flink的特点

  • 事件驱动(Event-driven)

    跟随当前时间点上出现的事件,调动可用资源,执行相关任务,使不断出现的问题得以解决

  • 基于流处理

    一切皆由流组成,离线数据是有界的流;实时数据是一个没有界限的流。(有界流、无界流)

  • 分层API

    • 越顶层越抽象,表达含义越简明,使用越方便
    • 越底层越具体,表达能力越丰富,使用越灵活

在这里插入图片描述

其他特点:

  • 支持事件时间(event-time)、处理时间(process-time)语义
  • 支持精确一次(exactly-once)语义
  • 低延迟,每秒处理百万时间,毫秒级延迟
  • 与众多存储系统的连接
  • 高可用,动态扩展,实现7*24小时全天候运行

1.1 Flink vs Spark Streaming

  • 数据模型
    • Spark采用RDD模型,spark streaming的DStream实际上也就是一组组小批数据RDD的集合
    • flink基本数据模型是数据流,以及事件(Event)序列
  • 运行时架构
    • spark是批计算,将DAG划分为不同的stage,一个完成后才可以计算下一个
    • flink是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点处理

2. Java快速使用

pom依赖

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-java</artifactId>
    <version>1.12.1</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
    <version>1.12.1</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-clients_${scala.binary.version}</artifactId>
    <version>1.12.1</version>
</dependency>

代码实现

// 创建执行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 从数据源中读取数据
String inputPath = "/tmp/Flink_Tutorial/src/main/resources/hello.txt";0

    
//流处理
DataStream<String> inputDataStream = env.readTextFile(inputPath);
// 基于数据流进行转换计算
DataStream<Tuple2<String,Integer>> resultStream = inputDataStream.flatMap(new WordCount.MyFlatMapper())
    .keyBy(item->item.f0)
    .sum(1);     
// 输出处理结果
resultSet.print();
// 执行流处理任务
env.execute();   


//批处理
DataSet<String> inputDataSet = env.readTextFile(inputPath);
// 对数据集进行处理
DataSet<Tuple2<String, Integer>> resultSet = inputDataSet.flatMap(new MyFlatMapper())
    .groupBy(0)//批处理
    .sum(1);

// 自定义类,实现FlatMapFunction接口
public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

    @Override
    public void flatMap(String s, Collector<Tuple2<String, Integer>> out) throws Exception {
        // 按空格分词
        String[] words = s.split(" ");
        // 遍历所有word,包成二元组输出
        for (String str : words) {
            out.collect(new Tuple2<>(str, 1));
        }
    }
}

3. Flink部署模式

3.1 Standalone模式

独立模式。

启动命令:bin/flink run -c <入口类> -p <并行度> <jar包路径> <启动参数>

访问 http://localhost:8081 可以对 flink 集群和任务进行监控管理。

3.2 yarn模式

以 Yarn 模式部署 Flink 任务时,要求 Flink 是有 Hadoop 支持的版本,Hadoop环境需要保证版本在 2.2 以上,并且集群中安装有 HDFS 服务。

Flink提供了两种在yarn上运行的模式,分别为Session-Cluster和Per-Job-Cluster模式。

1. Sesstion Cluster模式

在 yarn 中初始化一个 flink 集群,这个 flink 集群会常驻在 yarn 集群中,除非手工停止;并开辟指定的资源,用于任务的提交执行,申请的资源永远保持不变,如果资源满了,下一个作业就无法提交,得等其他作业执行完成后,释放了资源,下个作业才会正常提交。适合规模小执行时间短的作业。

2. Per Job Cluster 模式

**每提交一个Job都会创建一个新的 flink 集群,任务之间互相独立,互不影响,任务执行完成之后创建的集群也会消失。**适合规模大长时间运行的作业。

4. Flink运行架构

4.1 Flink运行时的组件

Flink运行时架构主要包括四个不同的组件,它们会在运行流处理应用程序时协同工作:

  • 作业管理器(JobManager)
  • 资源管理器(ResourceManager)
  • 任务管理器(TaskManager)
  • 分发器(Dispatcher)

作业管理器(JobManager)

也叫Master,接收Job作业,协调任务调度、资源分配、分布式计算,协调checkpoints,错误调度等。

  1. JobManager会先接收到要执行的应用程序,这个应用程序会包括:
  • 作业图(JobGraph)
  • 逻辑数据流图(logical dataflow graph)
  • 打包了所有的类、库和其它资源的JAR包。
  1. JobManager会把JobGraph转换成一个物理层面的数据流图,即“执行图”(ExecutionGraph,包含了所有可以并发执行的任务)

  2. JobManager会向资源管理器(ResourceManager)请求执行任务必要的资源(TaskManager的slot)。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上,并在运行过程中,负责所有需要中央协调的操作。

Flink集群至少需要一个 JobManager,部署多个JobManager时,只有一个leader,其他都是standby模式

资源管理器(ResourceManager)

主要负责管理TaskManager的slot资源,包括接收JobManager的资源申请、分配空闲的slot给JobManager、终止空闲的TaskManger等。

如果ResourceManager没有足够的插槽来满足JobManager的请求,它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器。

Flink为不同的环境和资源管理工具提供了不同资源管理器,比如YARN、Mesos、K8s,以及standalone部署。

任务管理器(TaskManager)

也叫Worker,Flink中的工作进程。每一个TaskManager都有一定数量的插槽(slots)。插槽平分TaskManager的内存,其数量限制了TaskManager能够执行的任务数量

启动之后,TaskManager会向资源管理器注册它的插槽;收到资源管理器的指令后,TaskManager就会将一个或者多个插槽提供给JobManager调用。

在执行过程中,同一个Job的不同TaskManager间可以交换数据。

分发器(Dispatcher)

可以跨作业运行,它为应用提交提供了REST接口。

当一个应用被提交执行时,分发器就会启动并将应用移交给一个JobManager。由于是REST接口,所以Dispatcher可以作为集群的一个HTTP接入点,这样就能够不受防火墙阻挡。Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。

Dispatcher在架构中可能并不是必需的,这取决于应用提交运行的方式。

4.2 作业提交流程

在这里插入图片描述

以下是yarn模式:

在这里插入图片描述

4.3 作业调度原理

在这里插入图片描述

一些原理

  1. Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。
  2. Client 为提交 Job 的客户端,提交 Job 后,Client 可以结束进程,也可以等待结果返回。
  3. JobManager 从 Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager 去执行。JobManager 是一个JVM进程。
  4. TaskManager 执行Task并将心跳和统计信息汇报给 JobManager,一个slot 只能同时启动一个Task,但可以有多个Task,TaskManager 之间以流的形式进行数据的传输。每个TaskManager都是一个JVM进程。

TaskSlot与并行度

算子共享slot的条件

同一个SlotGroup的算子能共享同一个slot。

同一个算子(并行度不为1)的子算子不能共享slot。

SlotGroup控制slot共享

在代码中通过算子的.slotSharingGroup("组名")可以指定算子所在的Slot组名。

默认的SlotGroup就是"default"。

每个算子的SlotGroup默认和上一个算子相同。

利用SlotGroup控制一个slot保存作业的整个管道的好处:

  • 省去跨slot、跨TaskManager的通信损耗(降低了并行度)
  • 执行健壮性更高,若某些slot执行出异常也能有其他slot补上。
  • 避免不同slot的CPU资源分配不均,有些slot分配到的子任务非CPU密集型,有些则CPU密集型,如果每个slot只完成自己的子任务,将出现某些slot太闲,某些slot过忙的现象。
运行Job所需的Slot总数

实际运行Job时所需的Slot总数 = 每个Slot组中的最大并行度。

并行度(parallelism)

一个特定算子的 子任务(subtask)的个数被称之为其并行度(parallelism)。

设置并行度

①对单独的每个算子进行设置并行度,

②用env设置全局的并行度

③在启动命令中设置

④在flink配置文件中设置

优先级①>②>③>④

开发环境的并行度默认就是计算机的CPU逻辑核数

数据流(DataFlow)

在运行时,Flink上运行的程序会被映射成“逻辑数据流”(dataflows),它包含了这三部分: Source 、Transformation 和 Sink

dataflow类似于任意的有向无环图(DAG),每一个dataflow以一个或多个sources开始以一个或多个sinks结束。

在大部分情况下,程序中的转换运算(transformations)跟dataflow中的算子(operator)是一一对应的关系

四层执行图

Flink 中的执行图可以分成四层:StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图。

  • StreamGraph:也被称为逻辑流图,由Flink程序直接映射而成,是根据用户通过Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。

  • JobGraph:是对StreamGraph优化后生成的,提交给JobManager 的数据结构。主要的优化为,将多个符合条件的节点chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。

  • ExecutionGraph:JobManager 根据JobGraph 生成的,是JobGraph的并行化版本。

  • 物理执行图:不是一个具体的数据结构,是JobManager 根据ExecutionGraph 对Job 进行调度后,在各个TaskManager 上部署Task 后形成的“图”。

算子间的数据传输形式

由于不同的算子可能具有不同的并行度,算子之间传输数据的形式可以是 one-to-one (forwarding) 的模式也可以是redistributing 的模式

  • One-to-one:stream维护着分区以及元素的顺序(比如source和map之间)。这意味着算子子任务看到的元素的个数以及顺序跟算子子任务生产的相同。map、fliter、flatMap等算子都是one-to-one的对应关系
  • Redistributing:stream的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如,keyBy 基于 hashCode 重分区、而 broadcast 和 rebalance 会随机重新分区,这些算子都会引起redistribute过程。

任务链(OperatorChains)

flink程序提交给JobManager前会将StreamGraph转换为JobGraph,涉及到任务链的生成,即将多个算子通过本地转发(local forward)的方式进行连接

任务链的前提条件

算子并行度相同、并且是 one-to-one 操作

任务链的生成控制
  • 算子.disableChaining(),强制当前算子的子任务不参与任务链的合并,即不和其他Slot资源合并,但是仍然可以保留“Slot共享”的特性。
  • env.disableOperatorChaining(),设置当前执行环境的算子不参与"任务链的合并"。
  • 算子.startNewChain(),表示不管前面任务链合并与否,从当前算子往后重新计算任务链的合并。通常用于前面强制不要任务链合并,而当前往后又需要任务链合并的特殊场景。

5. Flink流处理API

5.1 Environment执行环境

创建一个执行环境,表示当前执行程序的上下文。

有三种创建方式:

  • getExecutionEnvironment():如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境。StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  • createLocalEnvironment(int): 返回本地执行环境,需要在调用时指定默认的并行度LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(1);
  • createRemoteEnvironment(String,int,String):返回集群执行环境,将Jar提交到远程服务器。需要在调用时指定JobManager的IP和端口号,以及要在集群中运行的Jar包。StreamExecutionEnvironment env = StreamExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123,"YOURPATH//WordCount.jar");

5.2 Source数据源

env.fromCollection 从集合读取数据

DataStream<SensorReading> sensorDataStream = env.fromCollection(
    Arrays.asList(
        new SensorReading("sensor_1", 1547718199L, 35.8),
        new SensorReading("sensor_6", 1547718201L, 15.4),
        new SensorReading("sensor_7", 1547718202L, 6.7),
        new SensorReading("sensor_10", 1547718205L, 38.1)
    )
);

env.readTextFile 从文件读取数据

env.readTextFile("YOUR_FILE_PATH ");

从kafka读取数据

env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));

自定义数据源

需要实现SourceFunction 或者继承SourceFunction的富函数RichSourceFunction

env.addSource( new SourceFunction(){xxx});

DataStream<Tuple2<Integer, Integer>> inputStream= env.addSource(new RandomFibonacciSource());


private static class RandomFibonacciSource implements SourceFunction<Tuple2<Integer, Integer>> {
    private static final long serialVersionUID = 1L;
    private Random rnd = new Random();
    private int counter = 0;

    private volatile boolean isRunning = true;

    @Override
    public void run(SourceContext<Tuple2<Integer, Integer>> ctx) throws Exception {
        while (isRunning && counter < BOUND) {
            int first = rnd.nextInt(BOUND / 2 - 1) + 1;
            int second = rnd.nextInt(BOUND / 2 - 1) + 1;
            ctx.collect(new Tuple2<>(first, second));
            counter++;
            Thread.sleep(50L);
        }
    }

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

5.3 Transform转换算子

在这里插入图片描述

map

把数组流中的每一个值,使用所提供的函数执行一遍,得到元素个数相同的数组流。

DataStream<Integer> mapStram = dataStream.map(new MapFunction<String, Integer>() {
    public Integer map(String value) throws Exception {
    	return value.length();
    }
});

flatMap

把数组流中的每一个值,使用所提供的函数执行一遍,执行结果也是个数组流,并将这些数组拍平合并,得到元素不一定相同的数组流。

DataStream<String> flatMapStream = dataStream.flatMap(new FlatMapFunction<String,
String>() {
    public void flatMap(String value, Collector<String> out) throws Exception {
        String[] fields = value.split(",");
        for( String field: fields )
        out.collect(field);
    }
});

filter

DataStream<Interger> filterStream = dataStream.filter(new FilterFunction<String>(){
    public boolean filter(String value) throws Exception {
    	return value == 1;
    }
});

keyBy

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

1、KeyBy会重新分区; 2、不同的key有可能分到一起,因为是通过hash原理实现的;

滚动聚合算子

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

  • sum()
  • min()
  • max()
  • minBy()
  • maxBy()
max()和maxBy()的区别

都是对支流聚合后取最大值,区别在于,max()只取当前比较的字段的最大值,例如一条数据有a b c三个字段,max(a)会将当前数据的a字段更新为最大值,bc不变,然后返回;而maxBy(a),会将当前数据替换为a字段最大的那条数据,也就是bc也可能变化

reduce归并

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

需要实现ReduceFunction函数式接口。如:

keyedStream.reduce((curSensor,newSensor)->new SensorReading(curSensor.getId(),newSensor.getTimestamp(), Math.max(curSensor.getTemperature(), newSensor.getTemperature())));

其中curSensor表示上一条数据reduce的结果,newSensor当前数据

Split 和 Select分流

注:新版Flink已经不存在Split和Select这两个API了(至少Flink1.12.1没有!)

  • Split :
    • DataStream -> SplitStream:根据某些特征把DataStream拆分成多个SplitStream;
    • SplitStream虽然看起来像是两个Stream,但是其实它是一个特殊的Stream;
  • Select:
    • SplitStream -> DataStream:从一个SplitStream中获取一个或者多个DataStream;
// 1. 分流,按照温度值30度为界分为两条流
SplitStream<SensorReading> splitStream = dataStream.split(new OutputSelector<SensorReading>() {
    @Override
    public Iterable<String> select(SensorReading value) {
        return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
    }
});

DataStream<SensorReading> highTempStream = splitStream.select("high");
DataStream<SensorReading> allTempStream = splitStream.select("high", "low");

Connect 和 CoMap合流

  • Connect :
    • DataStream,DataStream → ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立
  • CoMap、CoFlatMap:
    • ConnectedStreams → DataStream:作用于 ConnectedStreams 上,功能与 map和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap处理。
// 2. 将高温流转换成二元组类型
DataStream<Tuple2<String, Double>> warningStream = highTempStream.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), value.getTemperature());
    }
});
// 2. 合流 connect
ConnectedStreams<Tuple2<String, Double>, SensorReading> connectedStreams = warningStream.connect(lowTempStream);

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

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

Union合流

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

和Connect的区别?
  1. Connect 只能合并两个流,流的数据类型可以不同;
  2. Union可以合并多条流,流的数据类型必须相同;

5.4 支持的数据类型

Flink使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

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

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

基础数据类型

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

元组Tuple0~Tuple25

java的元组类型由Flink的包提供,默认提供Tuple0~Tuple25

Java简单对象(POJO)

java的POJO这里要求必须提供无参构造函数

成员变量要求都是public(或者private但是提供get、set方法)

其他(Arrays, Lists, Maps, Enums,Scala 样例类,等等)

5.5 UDF 用户自定义函数

User Defined Function即用户自定义函数。

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

也就是说,用户可以自定义函数内容,以实现流操作。

自定义实现类

可以自定义参数传进去。示例:

DataStream<String> tweets = env.readTextFile("INPUT_FILE "); 
DataStream<String> flinkTweets = tweets.filter(new KeyWordFilter("flink")); 

public static class KeyWordFilter implements FilterFunction<String> { 
  private String keyWord; 

  KeyWordFilter(String keyWord) { 
    this.keyWord = keyWord; 
  } 

  @Override public boolean filter(String value) throws Exception { 
    return value.contains(this.keyWord); 
  } 
}

支持lambda匿名函数

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

!富函数(Rich Functions)

“富函数”是DataStream API提供的一个函数类的接口,所有Flink函数类都有其Rich版本。

它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

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

  • open()方法是rich function的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。
  • close()方法是生命周期中的最后一个调用的方法,做一些清理工作。
  • getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态
// 实现自定义富函数类(RichMapFunction是一个抽象类)
public static class MyMapper extends RichMapFunction<SensorReading, Tuple2<String, Integer>> {
    @Override
    public Tuple2<String, Integer> map(SensorReading value) throws Exception {
        //            RichFunction可以获取State状态
        //            getRuntimeContext().getState();
        return new Tuple2<>(value.getId(), getRuntimeContext().getIndexOfThisSubtask());
    }

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

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

5.6 数据重分区操作(shuffle、global)

keyBy():基于key的hash值进行分区

broadcase():广播,将消息广播给所有下游算子

shuffle():洗牌,将消息随机发送给下游算子

forward():直通,只在当前分区做计算

rebalance():以轮询的方式均匀分发给下游算子

rescale():类似rebalance,但是是分组的rebalance,如上游两个算子,下游四个算子,则将下游分为两组,上游的算子分别对一组进行轮询分发

global():固定将数据发送给下游的第一个分区

partitioncustom():自定义分区

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

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

    // 设置并行度 = 4
    env.setParallelism(4);

    // 从文件读取数据
    DataStream<String> inputStream = env.readTextFile("/tmp/Flink_Tutorial/src/main/resources/sensor.txt");

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

    // SingleOutputStreamOperator多并行度默认就rebalance,轮询方式分配
    dataStream.print("input");

    // 1. shuffle (并非批处理中的获取一批后才打乱,这里每次获取到直接打乱且分区)
    DataStream<String> shuffleStream = inputStream.shuffle();
    shuffleStream.print("shuffle");

    // 2. keyBy (Hash,然后取模)
    dataStream.keyBy(SensorReading::getId).print("keyBy");

    // 3. global (直接发送给第一个分区,少数特殊情况才用)
    dataStream.global().print("global");

    env.execute();
  }
}

5.7 sink

flink的所有对外的输出操作都要利用 Sink 完成:stream.addSink(new MySink(xxxx))
官方提供了一部分的框架的 sink,用户也可以自定义实现 sink。

在这里插入图片描述

自定义sink

实现RichSinkFunction接口

dataStream.addSink(new MyJdbcSink());

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

    @Override
    public void open(Configuration parameters) throws Exception {
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/flink_test?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false", "root", "example");
        insertStmt = connection.prepareStatement("insert into sensor_temp (id, temp) values (?, ?)");
        updateStmt = connection.prepareStatement("update sensor_temp set temp = ? where id = ?");
    }

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

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

kafka:FlinkKafkaProducer

dataStream.addSink( new FlinkKafkaProducer<String>("localhost:9092", "sinktest", new SimpleStringSchema()));

redis:RedisSink

// 定义jedis连接配置(我这里连接的是docker的redis)
FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
    .setHost("localhost")
    .setPort(6379)
    .setPassword("123456")
    .setDatabase(0)
    .build();

dataStream.addSink(new RedisSink<>(config, new MyRedisMapper()));

// 自定义RedisMapper
public static class MyRedisMapper implements RedisMapper<SensorReading> {
    // 定义保存数据到redis的命令,存成Hash表,hset sensor_temp id temperature
    @Override
    public RedisCommandDescription getCommandDescription() {
        return new RedisCommandDescription(RedisCommand.HSET, "sensor_temp");
    }

    @Override
    public String getKeyFromData(SensorReading data) {
        return data.getId();
    }

    @Override
    public String getValueFromData(SensorReading data) {
        return data.getTemperature().toString();
    }
}

ES:ElasticsearchSink

dataStream.addSink( new ElasticsearchSink.Builder<SensorReading>(httpHosts, new MyEsSinkFunction()).build());


// 实现自定义的ES写入操作
public static class MyEsSinkFunction implements ElasticsearchSinkFunction<SensorReading> {
    @Override
    public void process(SensorReading element, RuntimeContext ctx, RequestIndexer indexer) {
        // 定义写入的数据source
        HashMap<String, String> dataSource = new HashMap<>();
        dataSource.put("id", element.getId());
        dataSource.put("temp", element.getTemperature().toString());
        dataSource.put("ts", element.getTimestamp().toString());

        // 创建请求,作为向es发起的写入命令(ES7统一type就是_doc,不再允许指定type)
        IndexRequest indexRequest = Requests.indexRequest()
            .index("sensor")
            .source(dataSource);

        // 用index发送请求
        indexer.add(indexRequest);
    }
}

6. Flink 中的 Window

window是一种切割无限数据为有限块进行处理的手段,将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。

无限数据集是指一种不断增长的本质上无限的数据集。

Flink 默认的时间窗口根据 Processing Time 进行窗口的划分。

6.1 Window类型与特点

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

    • 滚动时间窗口(Tumbling Windows):按固定窗口长度对数据做切割,可以看做是滑动窗口的一种特殊情况(即窗口大小和滑动间隔相等)

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

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

    • 滑动时间窗口(Sliding Windows):每隔一个滑动间隔时间,对过去固定窗口长度的数据做切割

      • 特点:时间对齐,窗口长度固定, 可以有重叠。
      • 适用场景:对最近一个时间段内的统计(求某接口最近 5min 的失败率来决定是
        否要报警)。
    • 会话窗口(Session Windows):由一系列事件组合一个指定时间长度的timeout间隙组成,也就是一段时间没有接收到新数据就会生成新的窗口,配置 session 间隔以定义非活跃周期的长度,当收不到数据的时间达到session间隔,则关闭当前session,后续数据会分配到新session窗口

      • 特点:时间无对齐。
  • 计数窗口(Count Window):按数据数量生成窗口

    • 滚动计数窗口
    • 滑动计数窗口

6.2 Window API

  • 窗口分配器——window()方法
  • Flink提供了更加简单的.timeWindow().countWindow()方法,用于定义时间窗口和计数窗口。
  • 除了.windowAll(),其他窗口定义方法必须在keyBy之后才能使用
  • Flink 默认的时间窗口根据 Processing Time 进行窗口的划分

6.2.1 时间窗口.timeWindow( xxx )

滚动窗口:.timeWindow( Time.seconds(15) )

滑动窗口:.timeWindow( Time.seconds(15), Time.seconds(5) ) ,参数一个是window_size,一个是sliding_size。

DataStream<Tuple2<String, Double>> minTempPerWindowStream = dataStream 
  	.map(xxx) 
    .keyBy(data -> data.f0) 
    .timeWindow( Time.seconds(15) ) 
    .minBy(1);

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

6.2.2 计数窗口.countWindow( xxx )

滚动窗口:.countWindow( long)

滑动窗口:.countWindow( long, long ) ,参数一个是window_size,一个是sliding_size。

CountWindow 的 window_size 指的是相同 Key 的元素的个数,不是输入的所有元素的总数。

6.2.3 会话窗口(session window)

.window(EventTimeSessionWindows.withGap(Time.minutes(10)))

6.2.4 window function相关API

function例子:http://www.javashuo.com/article/p-ftiqugmc-eu.html

window function 定义了要对窗口中收集的数据做的计算操作,在window开窗操作后调用

主要可以分为两类:

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

    .window(EventTimeSessionWindows.withGap(Time.minutes(10))),保持一个简单的状态。典型的增量聚合函数有ReduceFunction, AggregateFunction。

    • .reduce(ReduceFunction); 合并当前的元素 和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果
    • .aggregate(AggregateFunction):比reduce通用,AggregateFunction需要传入三个数据类型,分别代表流、累加器、返回结果的类型
    • .fold(FoldFunction);——已经被弃用,建议使用aggregate函数
  • 全窗口函数:先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。WindowFunction和ProcessWindowFunction

    • .apply(WindowFunction)
    • .process(ProcessWindowFunction)
// 1. 增量聚合函数 (这里简单统计每个key组里传感器信息的总数)
    DataStream<Integer> resultStream = dataStream.keyBy("id")
      //                .timeWindow(Time.seconds(15)) // 已经不建议使用@Deprecated
      .window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
      .aggregate(new AggregateFunction<SensorReading, Integer, Integer>() {

        // 新建的累加器
        @Override
        public Integer createAccumulator() {
          return 0;
        }

        // 每个数据在上次的基础上累加
        @Override
        public Integer add(SensorReading value, Integer accumulator) {
          return accumulator + 1;
        }

        // 返回结果值
        @Override
        public Integer getResult(Integer accumulator) {
          return accumulator;
        }

        // 分区合并结果(TimeWindow一般用不到,SessionWindow可能需要考虑合并)
        @Override
        public Integer merge(Integer a, Integer b) {
          return a + b;
        }
      });

	  resultStream.print("result");
      env.execute();



new AggregateFunction<UserActionLog, Tuple2<Long,Long>, Double>() {

    // 一、初始值
    // 定义累加器初始值
    @Override
    public Tuple2<Long, Long> createAccumulator() {
        return new Tuple2<>(0L,0L);
    }

    // 二、累加
    // 定义累加器如何基于输入数据进行累加
    @Override
    public Tuple2<Long, Long> add(UserActionLog value, Tuple2<Long, Long> accumulator) {
        accumulator.f0 += 1;
        accumulator.f1 += value.getProductPrice();
        return accumulator;
    }

    // 三、合并
    // 定义累加器如何和State中的累加器进行合并
    @Override
    public Tuple2<Long, Long> merge(Tuple2<Long, Long> acc1, Tuple2<Long, Long> acc2) {
        acc1.f0+=acc2.f0;
        acc1.f1+=acc2.f1;
        return acc1;
    }

    // 四、输出
    // 定义如何输出数据
    @Override
    public Double getResult(Tuple2<Long, Long> accumulator) {
        return accumulator.f1 / (accumulator.f0 * 1.0);
    }

}

6.2.5 其它可选 API

.trigger() —— 触发器
定义 window 什么时候关闭,触发计算并输出结果
.evitor() —— 移除器
定义移除某些数据的逻辑
.allowedLateness() —— 允许处理迟到的数据
.sideOutputLateData() —— 将迟到的数据放入侧输出流
.getSideOutput() —— 获取侧输出流

7. 时间语义与 Wartermark

三种时间语义

  • **Event Time:事件创建时间;**通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳
  • Ingestion Time:数据进入Flink的时间;
  • Processing Time:执行操作算子的本地系统时间,与机器相关;

flink默认使用ProcessingTime

设置指定时间语义

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

7.3 Watermark

Flink对于迟到数据有三层保障,先来后到的保障顺序是:

  • WaterMark => 约等于放宽窗口标准
  • allowedLateness => 允许迟到(ProcessingTime超时,但是EventTime没超时)
  • sideOutputLateData => 超过迟到时间,另外捕获,之后可以自己批处理合并先前的数据

7.3.1 基本概念

前言:流处理从事件产生,到流经source,再到operator,有可能由于网络、分布式等原因,导致Flink接收到的事件的先后顺序与事件的Event Time顺序不同,此时若使用了Event Time语义,则不能明确数据是否全部到位,但又不能无限期的等下去,于是有了Watermark。

  1. Watermark可以理解成一个延迟触发机制,设置Watermark的延时时长t,每次对比当前数据的时间事件和已收到数据的最大事件时间,若eventTime<maxEventTime - t则认为小于该水印的所有数据都已经到达
  2. Watermark = maxEventTime-延迟时间t
  3. 如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。

7.3.2 watermark的引入

最常见的引用方式如下:

dataStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
    @Override
    public long extractTimestamp(SensorReading element) {
        return element.getTimestamp() * 1000L;
    }
});

BoundedOutOfOrdernessTimestampExtractor是AssignerWithPeriodicWatermarks的实现类,还有一个接口AssignerWithPunctuatedWatermarks,这两个接口都可以自定义如何从事件数据中抽取时间戳。

AssignerWithPeriodicWatermarks

  • 周期性的生成 watermark:系统会周期性的将 watermark 插入到流中
  • 默认周期是200毫秒,可以使用 ExecutionConfig.setAutoWatermarkInterval() 方法进行设置
  • 升序和前面乱序的处理 BoundedOutOfOrderness ,都是基于周期性 watermark 的

AssignerWithPunctuatedWatermarks

  • 没有时间周期规律,可打断的生成 watermark(即可实现每次获取数据都更新watermark)

一般认为Watermark的设置代码,在里Source步骤越近的地方越合适。

即使如此,依然可能会有些事件数据在 Watermark 之后到达,这时可以用Late Elements 处理。

8. Flink状态管理

  • 状态由一个任务维护,用来计算某个结果的所有数据都属于这个任务的状态
  • 可以认为任务状态就是一个本地变量,可以被任务的业务逻辑访问
  • Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问
  • 在Flink中,状态始终与特定算子相关联
  • 为了使运行时的Flink了解算子的状态,算子需要预先注册其状态

状态类型

  • 算子状态(Operator State)
    • 算子状态的作用范围限定为算子任务(也就是不能跨任务访问)
  • 键控状态(Keyed State)
    • 根据输入数据流中定义的键(key)来维护和访问

算子状态(用得少)

  • 算子状态的作用范围限定为算子任务,同一并行任务所处理的所有数据都可以访问到相同的状态。
  • 状态对于同一任务而言是共享的。(不能跨slot

算子状态数据结构

  • 列表状态(List state)
    • 将状态表示为一组数据的列表
  • 联合列表状态(Union list state)
    • 也将状态表示未数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复
  • 广播状态(Broadcast state)
    • 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态

键控状态

  • 键控状态是根据输入数据流中定义的键(key)来维护和访问的。
  • Flink 为每个key维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。
  • 当任务处理一条数据时,他会自动将状态的访问范围限定为当前数据的key

键控状态数据结构

  • 值状态(value state)
    • 将状态表示为单个的值
  • 列表状态(List state)
    • 将状态表示为一组数据的列表
  • 映射状态(Map state)
    • 将状态表示为一组key-value对
  • 聚合状态(Reducing state & Aggregating State)
    • 将状态表示为一个用于聚合操作的列表
dataStream
      .keyBy(SensorReading::getId)
      .map(new MyMapper());

// 自定义map富函数,测试 键控状态
  public static class MyMapper extends RichMapFunction<SensorReading,Integer>{
      
    private ValueState<Integer> valueState;

    // 其它类型状态的声明
    private ListState<String> myListState;
    private MapState<String, Double> myMapState;
    private ReducingState<SensorReading> myReducingState;

    @Override
    public void open(Configuration parameters) throws Exception {
      valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("my-int", Integer.class));

      myListState = getRuntimeContext().getListState(new ListStateDescriptor<String>("my-list", String.class));
      myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Double>("my-map", String.class, Double.class));
      //            myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<SensorReading>())

    }

    // 这里就简单的统计每个 传感器的 信息数量
    @Override
    public Integer map(SensorReading value) throws Exception {
      // 其它状态API调用
      // list state
      for(String str: myListState.get()){
        System.out.println(str);
      }
      myListState.add("hello");
      // map state
      myMapState.get("1");
      myMapState.put("2", 12.3);
      myMapState.remove("2");
      // reducing state
      //            myReducingState.add(value);

      myMapState.clear();


      Integer count = valueState.value();
      // 第一次获取是null,需要判断
      count = count==null?0:count;
      ++count;
      valueState.update(count);
      return count;
    }
  }

状态后端类型

  • MemoryStateBackend
    • 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上,而将checkpoint存储在JobManager的内存中
    • 特点:快速、低延迟,但不稳定
  • FsStateBackend(默认)
    • 将checkpoint存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上
    • 同时拥有内存级的本地访问速度,和更好的容错保证
  • RocksDBStateBackend
    • 将所有状态序列化后,存入本地的RocksDB中存储

使用

// 1. 状态后端配置
env.setStateBackend(new MemoryStateBackend());
env.setStateBackend(new FsStateBackend("checkpointDataUri"));
// 这个需要另外导入依赖
env.setStateBackend(new RocksDBStateBackend("checkpointDataUri"));

状态一致性

一致性实际上是"正确性级别"的另一种说法,即成功处理故障并恢复之后得到的结果,与没有发生故障时得到的结果相比,到底有多正确。

一致性级别

  • at-most-once: 最多一次,这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。

  • at-least-once: 至少一次,这表示计数程序在发生故障后可能多算,但是绝不会少算。

  • exactly-once: 精确一次,保证在发生故障后得到的计数结果与正确值一致。

最先保证 exactly-once 的系统(Storm Trident 和 Spark Streaming)在性能和表现力这两个方面付出了很大的代价。它们按批处理,保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此,用户经常不得不使用两个流处理框架(一个用来保证 exactly-once,另一个用来对每个元素做低延迟处理),结果使基础设施更加复杂

Flink 的一个重大价值在于, 它既保证了 exactly-once ,也具有低延迟和高吞吐力 的处理能力

端到端(end-to-end)状态一致性

端到端的保障指的是在整个数据处理管道上结果都是正确的。在每个组件都提供自身的保障情况下,整个处理管道上端到端的保障会受制于保障最弱的那个组件

端到端各部分的实现方式
  • 内部保证 —— 利用 checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
  • source 端 —— 需要外部源可重设数据的读取位置
  • sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统。
sink端实现exactly-once的两种方式
  • 幂等写入:所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
    • 中间有个恢复的过程,该过程可能会存在不正确的情况,但能保证最后结果正确。
  • 事务写入:需要构建事务来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint真正完成的时候,才把所有对应的结果写入 sink 系统中。
    • 会增加一定延迟
sink端事务写入的两种实现方式
  • 预写日志(WAL):可使用GenericWriteAheadSink 模板类实现
    • 把结果数据先当成状态保存,然后在收到checkpoint完成的通知时,一次性写入sink系统
    • 简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么sink系统,都能用这种方式一批搞定
    • 问题:写入数据时出现故障则会导致一部分数据成功一部分失败。
  • 两阶段提交(2PC):可使用TwoPhaseCommitSinkFunction 接口实现
    • sink任务对每个checkpoint启动一个事务,并将接下来所有接收到的数据添加到事务里
    • 然后将这些数据写入外部sink系统,但不提交它们——这时只是"预提交"
    • 当它收到Checkpoints完成的通知时,才正式提交事务,实现结果的真正写入。
    • 这种方式真正实现了exactly-once,它需要外部sink系统提供事务支持

2PC 对外部 sink 系统的要求
• 外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
• 在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
• 在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了),那么未提交的数据就会丢失
• sink 任务必须能够在进程失败后恢复事务
• 提交事务必须是幂等操作

检查点checkpoint

Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。

checkpoint使 Flink 可以保证 exactly-once,又不需要牺牲性能。

checkpoint其实就是**所有任务的状态在某个时间点的一份拷贝;**这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候

在JobManager中也有个Chechpoint的指针,指向了仓库的状态快照的一个拓扑图,为以后的数据故障恢复做准备

checkpoint算法与Barrier

Flink 检查点算法的正式名称是异步分界线快照(asynchronous barrier snapshotting),有个核心概念Barrier(屏障),由算子处理,但是不参与计算,而是会触发与检查点相关的行为:

  1. 由 JobManager 协调各个 TaskManager 进行 checkpoint 存储,checkpoint 保存在状态后端 StateBackend 中,默认 StateBackend 是内存级的
  2. 当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流;barrier 会在算子间传递下去。
  3. 当算子处理完记录并收到了屏障时,将状态异步保存到checkpoint ,这让 Flink 可以根据该位置重启
  4. 当状态备份和检查点位置备份都被确认后,则认为该检查点已完成。
  5. 当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成。
  6. 当 sink 任务收到确认通知,就会正式提交之前的事务
  7. 如果检查点操作失败,Flink 可以丢弃该检查点并继续正常执行,因为之后的某一个检查点可能会成功。只有在一系列连续的检查点操作失败之后,Flink 才会抛出错误

在这里插入图片描述

状态后端 State Backends

状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端( state backend)

状态后端主要负责两件事:本地状态管理,以及将检查点(checkPoint)状态写入远程存储

  • 每传入一条数据,有状态的算子任务都会读取和更新状态。
  • 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问。

保存点(Savepoints)

CheckPoint为自动保存,SavePoint为手动保存

  • Flink还提供了可以自定义的镜像保存功能,就是保存点(save points)
  • 原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
  • Flink不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作
  • 保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份、更新应用程序、版本迁移、暂停和重启程序,等等
public class StateTest4_FaultTolerance {
  public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);

    // 1. 状态后端配置
    env.setStateBackend(new MemoryStateBackend());
    env.setStateBackend(new FsStateBackend(""));
    // 这个需要另外导入依赖
    env.setStateBackend(new RocksDBStateBackend(""));

    // 2. 检查点配置 (每300ms让jobManager进行一次checkpoint检查)
    env.enableCheckpointing(300);

    // 高级选项
    env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
    //Checkpoint的处理超时时间
    env.getCheckpointConfig().setCheckpointTimeout(60000L);
    // 最大允许同时处理几个Checkpoint(比如上一个处理到一半,这里又收到一个待处理的Checkpoint事件)
    env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);
    // 与上面setMaxConcurrentCheckpoints(2) 冲突,这个时间间隔是 当前checkpoint的处理完成时间与接收最新一个checkpoint之间的时间间隔
    env.getCheckpointConfig().setMinPauseBetweenCheckpoints(100L);
    // 如果同时开启了savepoint且有更新的备份,是否倾向于使用更老的自动备份checkpoint来恢复,默认false
    env.getCheckpointConfig().setPreferCheckpointForRecovery(true);
    // 最多能容忍几次checkpoint处理失败(默认0,即checkpoint处理失败,就当作程序执行异常)
    env.getCheckpointConfig().setTolerableCheckpointFailureNumber(0);

    // 3. 重启策略配置
    // 固定延迟重启(最多尝试3次,每次间隔10s)
    env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 10000L));
    // 失败率重启(在10分钟内最多尝试3次,每次至少间隔1分钟)
    env.setRestartStrategy(RestartStrategies.failureRateRestart(3, Time.minutes(10), Time.minutes(1)));

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

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

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

9. ProcessFunction底层API

Process Function 用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window 函数和转换算子无法实现),可以访问到事件的时间戳信息和水位线信息

使用方法:.process( xxx )

所有的Process Function都有open()close()getRuntimeContext()等方法。

Flink提供了8个Process Function:

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • ProcessJoinFunction
  • BroadcastProcessFunction
  • KeyedBroadcastProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

9.1 KeyedProcessFunction

用于KeyedStream,会处理流的每一个元素,输出为0个、1个或者多个元素。

特有方法:

  • processElement(I value, Context ctx, Collector<O> out),流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context可以访问元素的时间戳,元素的 key ,以及TimerService 时间服务。 Context 还可以将结果输出到别的流(side outputs)。
  • onTimer(long timestamp, OnTimerContext ctx, Collector<O> out),是一个回调函数。当之前注册的定时器触发时调用。参数timestamp 为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext和processElement的Context 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
dataStream.keyBy("id").process( new MyProcess() ).print();

  // 实现自定义的处理函数
  public static class MyProcess extends KeyedProcessFunction<Tuple, SensorReading, Integer> {
    ValueState<Long> tsTimerState;

    @Override
    public void open(Configuration parameters) throws Exception {
      tsTimerState =  getRuntimeContext().getState(new ValueStateDescriptor<Long>("ts-timer", Long.class));
    }

    @Override
    public void processElement(SensorReading value, Context ctx, Collector<Integer> out) throws Exception {
      out.collect(value.getId().length());

      // context
      // Timestamp of the element currently being processed or timestamp of a firing timer.
      ctx.timestamp();
      // Get key of the element being processed.
      ctx.getCurrentKey();
      //            ctx.output();
      ctx.timerService().currentProcessingTime();
      ctx.timerService().currentWatermark();
      // 在5处理时间的5秒延迟后触发
      ctx.timerService().registerProcessingTimeTimer( ctx.timerService().currentProcessingTime() + 5000L);
      tsTimerState.update(ctx.timerService().currentProcessingTime() + 1000L);
      //            ctx.timerService().registerEventTimeTimer((value.getTimestamp() + 10) * 1000L);
      // 删除指定时间触发的定时器
      //            ctx.timerService().deleteProcessingTimeTimer(tsTimerState.value());
    }

      //定时器
    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<Integer> out) throws Exception {
      System.out.println(timestamp + " 定时器触发");
      ctx.getCurrentKey();
      //            ctx.output();
      ctx.timeDomain();
    }

    @Override
    public void close() throws Exception {
      tsTimerState.clear();
    }
  }

9.2 TimerService和定时器(Timers)

Context 和OnTimerContext 所持有的TimerService 对象拥有以下方法:

  • long currentProcessingTime() 返回当前处理时间
  • long currentWatermark() 返回当前watermark 的时间戳
  • void registerProcessingTimeTimer( long timestamp) 会注册当前key的processing time的定时器。当processing time 到达定时时间时,触发timer。
  • void registerEventTimeTimer(long timestamp) 会注册当前key 的event time 定时器。当Watermark水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
  • void deleteProcessingTimeTimer(long timestamp) 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
  • void deleteEventTimeTimer(long timestamp) 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。

当定时器timer 触发时,会执行回调函数onTimer()。注意定时器timer 只能在keyed streams 上面使用。

9.3 侧输出流(SideOutput)

  • 一个数据可以被多个window包含,只有其不被任何window包含的时候(包含该数据的所有window都关闭之后),才会被丢到侧输出流。
  • 简言之,如果一个数据被丢到侧输出流,那么所有包含该数据的window都由于已经超过了"允许的迟到时间"而关闭了,进而新来的迟到数据只能被丢到侧输出流!

  • 大部分的DataStream API 的算子的输出是单一输出,也就是某种数据类型的流。除了split 算子,可以将一条流分成多条流,这些流的数据类型也都相同。
  • processfunction 的side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。
  • 一个side output 可以定义为OutputTag[X]对象,X 是输出流的数据类型。
  • processfunction 可以通过Context 对象发射一个事件到一个或者多个side outputs。

场景:温度>=30放入高温流输出,反之放入低温流输出

  • java代码

    package processfunction;
    
    import apitest.beans.SensorReading;
    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.ProcessFunction;
    import org.apache.flink.util.Collector;
    import org.apache.flink.util.OutputTag;
    
    /**
     * @author : Ashiamd email: ashiamd@foxmail.com
     * @date : 2021/2/3 2:07 AM
     */
    public class ProcessTest3_SideOuptCase {
      public static void main(String[] args) throws Exception {
        // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置并行度 = 1
        env.setParallelism(1);
        // 从本地socket读取数据
        DataStream<String> inputStream = env.socketTextStream("localhost", 7777);
        // 转换成SensorReading类型
        DataStream<SensorReading> dataStream = inputStream.map(line -> {
          String[] fields = line.split(",");
          return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });
    
        // 定义一个OutputTag,用来表示侧输出流低温流
        // An OutputTag must always be an anonymous inner class
        // so that Flink can derive a TypeInformation for the generic type parameter.
        OutputTag<SensorReading> lowTempTag = new OutputTag<SensorReading>("lowTemp"){};
    
        // 测试ProcessFunction,自定义侧输出流实现分流操作
        SingleOutputStreamOperator<SensorReading> highTempStream = dataStream.process(new ProcessFunction<SensorReading, SensorReading>() {
          @Override
          public void processElement(SensorReading value, Context ctx, Collector<SensorReading> out) throws Exception {
            // 判断温度,大于30度,高温流输出到主流;小于低温流输出到侧输出流
            if (value.getTemperature() > 30) {
              out.collect(value);
            } else {
              ctx.output(lowTempTag, value);
            }
          }
        });
    
        highTempStream.print("high-temp");
        highTempStream.getSideOutput(lowTempTag).print("low-temp");
    
        env.execute();
      }
    }
    

9.4 CoProcessFunction

  • 对于两条输入流,DataStream API 提供了CoProcessFunction 这样的low-level操作。CoProcessFunction 提供了操作每一个输入流的方法: processElement1()processElement2()
  • 类似于ProcessFunction,这两种方法都通过Context 对象来调用。这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs。
  • CoProcessFunction 也提供了onTimer()回调函数
// 2. 合流 connect
ConnectedStreams<Tuple2<String, Double>, SensorReading> connectedStreams = warningStream.connect(lowTempStream);

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

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

10. Table API 与 SQL

  • Table API是集成在Scala和Java语言内的查询API
  • Table API基于代表"表"的Table类,并提供一整套操作处理的方法API;这些方法会返回一个新的Table对象,表示对输入表应用转换操作的结果
  • 有些关系型转换操作,可以由多个方法调用组成,构成链式调用结构

  • Flink 的 SQL 集成,基于实现 了SQL 标准的 Apache Calcite
  • 在 Flink 中,用常规字符串来定义 SQL 查询语句
  • SQL 查询的结果,也是一个新的 Table

示例:

package apitest.tableapi;

import apitest.beans.SensorReading;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

/**
   * @author : Ashiamd email: ashiamd@foxmail.com
   * @date : 2021/2/3 5:47 AM
   */
public class TableTest1_Example {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 1. 读取数据
        DataStream<String> inputStream = env.readTextFile("/tmp/Flink_Tutorial/src/main/resources/sensor.txt");

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

        // 3. 创建表环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // 4. 基于流创建一张表
        Table dataTable = tableEnv.fromDataStream(dataStream);

        // 5. 调用table API进行转换操作
        Table resultTable = dataTable.select("id, temperature")
            .where("id = 'sensor_1'");

        // 6. 执行SQL
        tableEnv.createTemporaryView("sensor", dataTable);
        String sql = "select id, temperature from sensor where id = 'sensor_1'";
        Table resultSqlTable = tableEnv.sqlQuery(sql);

        // 7. 将动态表转换为流进行输出
        tableEnv.toAppendStream(resultTable, Row.class).print("result");
        tableEnv.toAppendStream(resultSqlTable, Row.class).print("sql");

        env.execute();
    }
}

基本程序结构

Table API 和 SQL 的程序结构,与流式处理的程序结构十分类似

StreamTableEnvironment tableEnv = ... // 创建表的执行环境
// 创建一张表,用于读取数据
tableEnv.connect(...).createTemporaryTable("inputTable");
// 注册一张表,用于把计算结果输出
tableEnv.connect(...).createTemporaryTable("outputTable");
// 通过 Table API 查询算子,得到一张结果表
Table result = tableEnv.from("inputTable").select(...);
// 通过 SQL 查询语句,得到一张结果表
Table sqlResult = tableEnv.sqlQuery("SELECT ... FROM inputTable ...");
// 将结果表写入输出表中
result.insertInto("outputTable");

创建 TableEnvironment

  • 创建表的执行环境,需要将 flink 流处理的执行环境传入:StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
  • TableEnvironment 是 flink 中集成 Table API 和 SQL 的核心概念,所有对表的操作都基于 TableEnvironment
  • – 注册 Catalog
  • – 在 Catalog 中注册表
  • – 执行 SQL 查询
  • – 注册用户自定义函数(UDF)

配置 TableEnvironment

两种方式:基于老版本planner、基于Blink。

新版本blink,真正把批处理、流处理都以DataStream实现。


package apitest.tableapi;


import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.bridge.java.BatchTableEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.api.internal.TableEnvironmentImpl;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2021/2/3 3:56 PM
 */
public class TableTest2_CommonApi {
  public static void main(String[] args) {
    // 创建执行环境
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 设置并行度为1
    env.setParallelism(1);

    StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

    // 1.1 基于老版本planner的流处理
    EnvironmentSettings oldStreamSettings = EnvironmentSettings.newInstance()
      .useOldPlanner()
      .inStreamingMode()
      .build();
    StreamTableEnvironment oldStreamTableEnv = StreamTableEnvironment.create(env,oldStreamSettings);

    // 1.2 基于老版本planner的批处理
    ExecutionEnvironment batchEnv = ExecutionEnvironment.getExecutionEnvironment();
    BatchTableEnvironment oldBatchTableEnv = BatchTableEnvironment.create(batchEnv);

    // 1.3 基于Blink的流处理
    EnvironmentSettings blinkStreamSettings = EnvironmentSettings.newInstance()
      .useBlinkPlanner()
      .inStreamingMode()
      .build();
    StreamTableEnvironment blinkStreamTableEnv = StreamTableEnvironment.create(env,blinkStreamSettings);

    // 1.4 基于Blink的批处理
    EnvironmentSettings blinkBatchSettings = EnvironmentSettings.newInstance()
      .useBlinkPlanner()
      .inBatchMode()
      .build();
    TableEnvironment blinkBatchTableEnv = TableEnvironment.create(blinkBatchSettings);
  }
}

表Table

  • TableEnvironment可以注册目录Catalog,并可以基于Catalog注册表
  • 表(Table)是由一个"标示符"(identifier)来指定的,由3部分组成:Catalog名、数据库(database)名和对象名
  • 表可以是常规的,也可以是虚拟的(视图,View)
  • 常规表(Table)一般可以用来描述外部数据,比如文件、数据库表或消息队列的数据,也可以直接从DataStream转换而来
  • 视图(View)可以从现有的表中创建,通常是table API或者SQL查询的一个结果集

创建表

TableEnvironment可以调用connect()方法,连接外部系统,并调用.createTemporaryTable()方法,在Catalog中注册表

tableEnv
  .connect(...)    //    定义表的数据来源,和外部系统建立连接
  .withFormat(...)    //    定义数据格式化方法
  .withSchema(...)    //    定义表结构
  .createTemporaryTable("MyTable");    //    创建临时表
package apitest.tableapi;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.Csv;
import org.apache.flink.table.descriptors.FileSystem;
import org.apache.flink.table.descriptors.Schema;
import org.apache.flink.types.Row;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2021/2/3 3:56 PM
 */
public class TableTest2_CommonApi {
  public static void main(String[] args) throws Exception {
    // 创建执行环境
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 设置并行度为1
    env.setParallelism(1);

    StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

    // 2. 表的创建:连接外部系统,读取数据
    // 2.1 读取文件
    String filePath = "/tmp/Flink_Tutorial/src/main/resources/sensor.txt";

    tableEnv.connect(new FileSystem().path(filePath)) // 定义到文件系统的连接
      .withFormat(new Csv()) // 定义以csv格式进行数据格式化
      .withSchema(new Schema()
                  .field("id", DataTypes.STRING())
                  .field("timestamp", DataTypes.BIGINT())
                  .field("temp", DataTypes.DOUBLE())
                 ) // 定义表结构
      .createTemporaryTable("inputTable"); // 创建临时表

    Table inputTable = tableEnv.from("inputTable");
    inputTable.printSchema();
    tableEnv.toAppendStream(inputTable, Row.class).print();

    env.execute();
  }
}

表的查询 - Table API

  • Table API是集成在Scala和Java语言内的查询API

  • Table API基于代表"表"的Table类,并提供一整套操作处理的方法API;这些方法会返回一个新的Table对象,表示对输入表应用转换操作的结果

  • 有些关系型转换操作,可以由多个方法调用组成,构成链式调用结构

    Table sensorTable = tableEnv.from("inputTable");
    Table resultTable = sensorTable
      .select("id","temperature")
      .filter("id = 'sensor_1'");
    
// 3.1 Table API
// 简单转换
Table resultTable = inputTable.select("id, temp")
    .filter("id === 'sensor_6'");

// 聚合统计
Table aggTable = inputTable.groupBy("id")
    .select("id, id.count as count, temp.avg as avgTemp");

表的查询 – SQL

  • Flink 的 SQL 集成,基于实现 了SQL 标准的 Apache Calcite
  • 在 Flink 中,用常规字符串来定义 SQL 查询语句
  • SQL 查询的结果,也是一个新的 Table
    • Table resultSqlTable = tableEnv.sqlQuery("select id, temperature from sensorTable where id ='sensor_1'");
// 3.2 SQL
tableEnv.sqlQuery("select id, temp from inputTable where id = 'senosr_6'");
Table sqlAggTable = tableEnv.sqlQuery("select id, count(id) as cnt, avg(temp) as avgTemp from inputTable group by id");

更新模式

  • 对于流式查询,需要声明如何在表和外部连接器之间执行转换
  • 与外部系统交换的消息类型,由更新模式(Uadate Mode)指定
  • 追加(Append)模式
    • 表只做插入操作,和外部连接器只交换插入(Insert)消息
  • 撤回(Retract)模式
    • 表和外部连接器交换添加(Add)和撤回(Retract)消息
    • 插入操作(Insert)编码为Add消息;删除(Delete)编码为Retract消息;更新(Update)编码为上一条的Retract和下一条的Add消息
  • 更新插入(Upsert)模式
    • 更新和插入都被编码为Upsert消息;删除编码为Delete消息

输出表

  • 表的输出,是通过将数据写入 TableSink 来实现的

  • TableSink 是一个通用接口,可以支持不同的文件格式、存储数据库和消息队列

  • 输出表最直接的方法,就是通过 Table.insertInto() 方法将一个 Table 写入注册过的 TableSink 中

tableEnv.connect(...).createTemporaryTable("outputTable");
Table resultSqlTable = ...
    resultTable.insertInto("outputTable");

输出到文件

输出到文件有局限,只能是批处理,且只能是追加写,不能是更新式的随机写。

tableEnv.connect(
	new FileSystem().path("output.txt")
) // 定义到文件系统的连接
.withFormat(new Csv())
.withSchema(new Schema()
    .field("id", DataTypes.STRING())
    .field("temp", DataTypes.Double())
).createTemporaryTable("outputTable") ; // 创建临时表

resultTable.insertInto("outputTable"); // 输出表

读写kafka

package apitest.tableapi;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.Csv;
import org.apache.flink.table.descriptors.Kafka;
import org.apache.flink.table.descriptors.Schema;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2021/2/3 6:33 PM
 */
public class TableTest4_KafkaPipeLine {
  public static void main(String[] args) throws Exception {
    // 1. 创建环境
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);

    StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

    // 2. 连接Kafka,读取数据
    tableEnv.connect(new Kafka()
                     .version("universal")
                     .topic("sensor")
                     .property("zookeeper.connect", "localhost:2181")
                     .property("bootstrap.servers", "localhost:9092")
                    )
      .withFormat(new Csv())
      .withSchema(new Schema()
                  .field("id", DataTypes.STRING())
                  .field("timestamp", DataTypes.BIGINT())
                  .field("temp", DataTypes.DOUBLE())
                 )
      .createTemporaryTable("inputTable");

    // 3. 查询转换
    // 简单转换
    Table sensorTable = tableEnv.from("inputTable");
    Table resultTable = sensorTable.select("id, temp")
      .filter("id === 'sensor_6'");

    // 聚合统计
    Table aggTable = sensorTable.groupBy("id")
      .select("id, id.count as count, temp.avg as avgTemp");

    // 4. 建立kafka连接,输出到不同的topic下
    tableEnv.connect(new Kafka()
                     .version("universal")
                     .topic("sinktest")
                     .property("zookeeper.connect", "localhost:2181")
                     .property("bootstrap.servers", "localhost:9092")
                    )
      .withFormat(new Csv())
      .withSchema(new Schema()
                  .field("id", DataTypes.STRING())
                  //                        .field("timestamp", DataTypes.BIGINT())
                  .field("temp", DataTypes.DOUBLE())
                 )
      .createTemporaryTable("outputTable");

    resultTable.insertInto("outputTable");

    tableEnv.execute("");
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值