1. Flink特点

  • 事件驱动
    • 事件驱动型应用是一类具有状态的应用, 它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。比较典型的就是以 kafka为代表的消息队列几乎都是事件驱动型应用。
  • 基于流处理
    • 一切皆由流组成,离线数据是有界的流;实时数据是一个没有界限的流。
  • 分层API

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

      1.1 数据流

  • 无界数据流

    • 无界数据流有开始但是没有结束,它们不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须在获取后立即处理 event。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取 event,以便能够推断结果完整性。
  • 有界数据流

    • 有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流,处理有界流不需要有序获取,因为可以始终对有界数据集进行排序,有界流的处理也称为批处理。

      1.2 Flink vs Spark Streaming

  • 数据模型

    • Spark采用RDD模型,spark streaming的DStream实际上也就是一组组小批数据RDD的集合
    • flink基本数据模型是数据流,以及事件(Event)序列
  • 运行时架构

    • spark是批计算,将DAG划分为不同的stage,一个完成后才可以计算下一个
    • flink是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点处理

      2. Flink架构

      Flink 运行时由两种类型的进程组成:一个 JobManager 和一个或者多个 TaskManager。Flink架构图:
      1643093586(1).png
      Client:为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client 可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
      JobManager: 负责调度 job、管理task、协调checkpoint、并且协调从失败中恢复等等。这个进程由三个不同的组件组成:

    • ResourceManager:负责 Flink 集群中的资源提供、回收、分配 ,它管理 task slots,这是 Flink 集群中资源调度的单位。

    • Dispatcher:提供了一个 REST 接口,用来提交 Flink 应用程序执行,并为每个提交的作业启动一个新的 JobMaster。
    • JobMaster:负责管理单个JobGraph的执行。Flink 集群中可以同时运行多个作业,每个作业都有自己的 JobMaster。

始终至少有一个 JobManager。高可用(HA)设置中可能有多个 JobManager,其中一个始终是 leader,其他的则是 standby。
TaskManagers:Flink中的工作进程。通常在 Flink中会有多个 TaskManager运行,每一个 TaskManager
都包含了一定数量的插槽(slots)。插槽的数量限制了 TaskManager能够执行的任务数量。启动之后, TaskManager会向资源管理器注册它的插槽;收到资源管理器的指令后,TaskManager就会将一个或者多个插槽提供给 JobManager调用。JobManager就可以向插槽分配任务(tasks)来执行了。在执行过程中,一个 TaskManager可以跟其它运行同一应用程序的 TaskManager交换数据。一个 task slot 中可以执行多个算子。

3. 任务提交流程

任务提交流程:
image.png
Yarn模式任务提交流程:
image.png
Flink任务提交后, Client向 HDFS上传 Flink的 Jar包和配置,之后向 Yarn ResourceManager提交任务, ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster,ApplicationMaster启动后加载 Flink的 Jar包和配置构建环境,然后启动 JobManager,之后ApplicationMaster向ResourceManager申请资源启动 TaskManager,ResourceManager分配 Container资 源 后 ,由ApplicationMaster通知资源所在节点的NodeManager启动 TaskManager,NodeManager加载 Flink的 Jar包和配置构建环境并启动TaskManager,TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。

4. 调度原理

4.1 TaskManger与 Slots

Flink中每一个worker(TaskManager)都是一个JVM进程,它可能会在独立的线程上执行一个或多个subtask。为了控制一个worker能接收多少个task,worker通过taskslot来进行控制(一个worker至少有一个taskslot)。
每个taskslot表示TaskManager拥有资源的一个固定大小的子集。
假如一个TaskManager有三个slot,那么它会将其管理的内存分成三份给各个slot。资源slot化意味着一个subtask将不需要跟来自其他job的subtask竞争被管理的内存,取而代之的是它将拥有一定数量的内存储备。需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的受管理的内存。
通过调整taskslot的数量,允许用户定义subtask之间如何互相隔离。如果一个TaskManager一个slot,那将意味着每个taskgroup运行在独立的JVM中(该JVM可能是通过一个特定的容器启动的),而一个TaskManager多个slot意味着更多的subtask可以共享同一个JVM。而在同一个JVM进程中的task将共享TCP连接(基于多路复用)和心跳消息。它们也可能共享数据集和数据结构,因此这减少了每个task的负载。
image.png

4.2 执行图

由Flink程序直接映射成的数据流图是StreamGraph,也被称为逻辑流图,因为它们表示的是计算逻辑的高级视图。为了执行一个流处理程序,Flink需要将逻辑流图转换为物理数据流图(也叫执行图),详细说明程序的执行方式。
Flink中的执行图可以分成四层:StreamGraph->JobGraph->ExecutionGraph->物理执行图。
StreamGraph:是根据用户通过StreamAPI编写的代码生成的最初的图。用来表示程序的拓扑结构。
JobGraph:StreamGraph经过优化后生成了JobGraph,提交给JobManager的数据结构。主要的优化为,将多个符合条件的节点chain在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
ExecutionGraph:JobManager根据JobGraph生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
物理执行图:JobManager根据ExecutionGraph对Job进行调度后,在各个TaskManager上部署Task后形成的“图”,并不是一个具体的数据结构。
image.png

4.3 并行度

Flink程序的执行具有并行、分布式的特性。
并行度:默认是计算机的CPU逻辑核数。
在执行过程中,一个流(stream)包含一个或多个分区(streampartition),而每一个算子(operator)可以包含一个或多个子任务(operatorsubtask),这些子任务在不同的线程、不同的物理机或不同的容器中彼此互不依赖地执行。
一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。
image.png
Stream在算子之间传输数据的形式可以是one-to-one(forwarding)的模式也可以是redistributing的模式,具体是哪一种形式,取决于算子的种类。
One-to-one:map算子的子任务看到的元素的个数以及顺序跟source算子的子任务生产的元素的个数、顺序相同,map、fliter、flatMap等算子都是one-to-one的对应关系。类似于spark中的窄依赖
Redistributing:stream(map()跟keyBy/window之间或者keyBy/window跟sink之间)的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如,keyBy()基于hashCode重分区、broadcast和rebalance会随机重新分区,这些算子都会引起redistribute过程,而redistribute过程就类似于Spark中的shuffle过程。类似于spark中的宽依赖

4.4 任务链

相同并行度的one-to-one操作,Flink这样相连的算子链接在一起形成一个task,原来的算子成为里面的一部分。将算子链接成task是非常有效的优化:它能减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。链接的行为可以在编程API中进行指定。
image.png