Flink 流处理

Flink 流处理

Dataflow编程

顾名思义,Dataflow程序描述了数据如何在不同操作之间流动。Dataflow程序通常表现为有向无环图(DAG),图中顶点称为算子(Operator),表示计算。而边表示数据依赖关系

算子是Dataflow程序的基本功能单元,他们从输入获取数据,对其进行计算,然后产生数据并发往输出以供后续处理。而所有Flink程序都由三部分算子组成。

  • Source(数据源):负责获取输入数据。
  • Transformation(数据处理):对数据进行处理加工,通常对应着多个算子。
  • Sink(数据汇):负责输出数据。

执行图

类似上图的Dataflow图被称为逻辑图,因为它们表达了高层视角下的计算逻辑。为了执行Dataflow程序,需要将逻辑图转化为物理Dataflow图(执行图),后者会指定程序的执行细节。

在Flink中,执行图按层级顺序分为以下四层

  1. StreamingGraph
    • 是根据用户通过Stream API编写的代码生成的初始流程图,用于表示程序的拓扑结构
  2. JobGraph
    • StreamGraph经过优化后生成了JobGraph,提交给JobManager的数据结构。主要的优化为将多个符合条件的节点链接在一起作为一个节点(任务链Operator Chains)后放在一个作业中执行,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
  3. ExecutionGraph
    • JobManager根据JobGraph生成ExecutionGraph,ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
  4. 物理执行图
    • JobManager根据ExecutionGraph对任务进行调度后,在各个TaskManager上部署作业后形成的“图”,并不是一个具体的数据结构。

并行度

Flink程序的执行具有并行、分布式的特性。

在执行过程中,一个Stream包含一个或多个分区(partition),而每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务中不同的线程、不同的物理机或不同的容器中彼此互不依赖地执行。

一个特定算子的子任务的个数被称之为并行度(paralelism)。一般情况下一个流程序的并行度可以认为就是其所有算子中最大的并行度,一个程序中不同的算子可以具有不同的并行度。

数据传输策略

Stream在算子之间传输数据的形式可以是one-to-one(forwarding)的模式也可以是Redistributing的模式,具体是哪一种需要取决于算子的种类。

  • One-to-one
    • Stream维护着分区以及元素的顺序(比如在Source和map operator之间),那意味着map算子的子任务看到的元素的个数以及顺序跟Source算子的子任务生产的元素的个数、顺序相同,map、filter、flatmap等算子都是one-to-one的对应关系。
    • 类似于Spark中的窄依赖
  • Redistributing
    • Stream的分区会发生改变(map()跟keyBy/window之间或者keyBy/windows跟Sink之间)。每一个算子的子任务依据所选择的Transformation发送数据到不同的目标任务。
    • 例如keyBy()基于hashCode重分区、broadcast和rebalance会随机重新分区,这些算子都会引起redistribute过程,而redistribute过程就类似于Spark中的shuffle过程。
    • 类似于Spark中的宽依赖

任务链

Flink 采用了一种称为任务链的优化技术,可以在特定条件下减少本地通信的开销。为了满足任务链的要求,必须将两个或多个算子设为相同的并行度,并通过本地转发(local forward) 的方式进行连接。

相同并行度one-to-one 操作 (两个条件缺一不可),Flink 这样相连的算子链接在一起形成一个 task,原来的算子成为里面的 subtask。每个 task 由一个线程执行。将算子链接成 task 是个有用的优化:它减少线程间切换、缓冲的开销,并且减少延迟的同时增加整体吞吐量。

时间语义

对于流式数据处理,最大的特点就是数据上具有时间的属性特征,Flink根据时间产生的位置不同,将时间区分为如下三种时间概念

  • 事件时间(Event Time):数据流事件实际发生的时间。
  • 接入时间(Ingestion Time):数据进入Flink系统的时间。
  • 处理时间(Processing Time):当前流处理算子所在机器上的本地时钟时间。

Flink中默认使用的是处理时间,但是在大多数情况下都会使用事件时间(即实际事件的发生点,也符合事件发生进而分析的逻辑),一般只有在Event Time无法使用的情况下才会使用接入时间和处理时间,因此我们可以通过调用执行环境的setStreamTimeCharacteristic方法来指定时间语义

1
2
3
4
5
//创建执行环境				
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

//设置指定的时间语义,如下面的设置为EventTime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

处理时间与事件时间的选择

在大部分场景由于我们需要依据事件发生的顺序来进行逻辑处理,因此都会使用事件时间。但是在一些特殊场景下,考虑到事件数据数据乱序到达以及延迟到达等问题,为了保证实时性和低延迟,处理时间就会派上用场。

例如下面几种场景:

  1. 更重视处理速度而非准确性的应用。
  2. 需要周期性实时报告结果而无论其准确性(如实时监控仪表盘)。

因此,对比处理时间和事件时间得出结论:

  • 处理时间提供了低延迟,但是它的结果依赖处理速度,因此具有不确定性。
  • 事件时间则与之相反,能够保证结果的准确性,并允许你处理延迟甚至无序的事件。

水位线(Watermarks)

在理想状态下,事件数据都是按照事件产生的时间顺序传输至Flink系统中。但事实上,由于网络或者分布式系统等外部因素的影响下,事件数据往往不能及时传输,导致系统的不稳定而造成数据乱序到达或者延迟到达等情况。

一旦出现这种问题,如果我们严格按照Event Time来决定窗口的运行,我们既不能保证属于该窗口的数据已经全部到达,也不能无休止的等待延迟到达的数据,因此我们需要一种机制来控制数据处理的进度,这就是水位线(Watermarks)机制

水位线是一个全局的进度指标,它能够衡量数据处理进度==(表达数据到达的完整性)**,保证事件数据全部到达Flink系统,即使数据乱序或者延迟到达,也能够像预期一样计算出正确和连续的结果。

那么它是如何做到的呢?

  • Flink会使用最新的事件时间减去固定时间间隔作为水位线,该时间时间为用户外部配置的支持最大延迟到达的时间长度。
  • 当一个算子接收到一个时间为T的水位线,就可以认为不会再收到任何时间戳小于或等于T的事件了(迟到事件或异常事件)
  • 水位线其实就相当于一个提示算子的信号,当水位线时间戳大于时间窗口的结束时间,且窗口中含有事件数据时,此时算子就会认为某个特定时间区间的时间戳已经全部到齐,立即开始触发窗口计算或对接收的数据进行排序。

从上面我们可以看出,水位线其实就是在结果的准确性和延迟之间做出取舍,它虽然保证了低延迟,但是伴随而来的却是低可信度。倘若我们要保证后续的延迟事件不丢失,就必须额外增加一些代码来处理他们,但是如果采用这种保守的机制,虽然可信度低高了,但是延迟又会继续增加,因此延迟和可信无法做到两全其美,需要我们依据具体场景来自己平衡。

Built with Hugo
主题 StackJimmy 设计