Flink简介
- 2010年,Flink由柏林工业大学、柏林洪堡大学和哈索·普拉特纳研究所共同研究;2014年4月,Flink被捐赠给Apache软件基金会,成为Apache软件基金会孵化器项目;2014年12月,Flink成为Apache软件基金会的顶级项目。
- ApacheFlink是为分布式、高性能的流处理应用程序打造的开源流处理框架。Flink不仅能提供同时支持高吞吐和exactly-once语义的实时计算,还能提供批量数据处理。
- 相较于市面上的其他数据处理引擎,Flink和Spark都可以同时支持流处理和批处理。但是,Spark的技术理念是基于批处理来模拟流的计算;而Flink则完全相反,它采用的是基于流计算来模拟批处理。
Flink关键概念
四个关键概念:
- 流数据的连续处理
- 事件时间
- 有状态流处理
- 状态快照
Flink核心理念
- Flink与其他流计算引擎的最大区别,就是状态管理。
- Flink提供了内置的状态管理,可以把这些状态存储在Flink内部,而不需要把它存储在外部系统。这样做的好处:
- 降低了计算引擎对外部系统的依赖,使得部署、运维更加简单;
- 对性能带来了极大的提升。
FlinkRuntime整体架构

Flink核心概念-DataStream
- DataStream:Flink用类DataStream来表示程序中的流式数据。
- 用户可以认为它们是含有重复数据的不可修改的集合(collection),DataStream中元素的数量是无限的。
Flink核心概念-DataSet
- DataSet:可对数据集进行转换(例如,过滤,映射,联接,分组),或通过读取文件或从本地集合创建数据集。结果通过接收器返回,接收器可以将数据写入(分布式)文件或标准输出(例如命令行终端)。
TFlink程序
- Flink程序由Source、Transformation和Sink三部分组成,其中Source主要负责数据的读取,支持HDFS、Kafka和文本等;Transformation主要负责对数据的转换操作;Sink负责最终数据的输出,支持HDFS、Kafka和文本输出等。在各部分之间流转的数据称为流(stream)。

Flink数据源

Flink程序运行图

Flink作业运行流程
- 用户首先提交Flink程序到JobClient,经过JobClient的处理、解析、优化提交到JobManager,最后由TaskManager运行task。

- JobClient是Flink程序和JobManager交互的桥梁。主要负责接收程序、解析程序的执行计划、优化程序的执行计划,然后提交执行计划到JobManager。在Flink中主要有三类Operator。
- Source Operator:数据源操作,比如文件、socket、Kafka等。
- Transformation Operator:数据转换操作,比如map、flatMap、reduce等算子。
- SinkOperator:数据存储操作。比如数据存储到HDFS、Mysql、Kafka等等。
Flink的数据处理
- ApacheFlink它同时支持批处理和流处理,也能用来做一些基于事件的应用。
- Flink是一个纯流式的计算引擎,它的基本数据模型是数据流。流可以是无边界的无限流,即一般意义上的流处理。也可以是有边界的有限流,就是批处理。因此Flink用一套架构同时支持了流处理和批处理。
- Flink的一个优势是支持有状态的计算。如果处理一个事件(或一条数据)的结果只跟事件本身的内容有关,称为无状态处理;反之结果还和之前处理过的事件有关,称为有状态处理。
有界流与无界流
- 无界流:有定义流的开始,但没有定义流的结束。数据源会无休止地产生数据。无界流的数据必须持续处理,即数据被读取后需要立刻处理。不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。处理无界数据通常要求以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。
- 有界流:有定义流的开始,也有定义流的结束。有界流可以在读取所有数据后再进行计算。有界流所有数据可以被排序,所以并不需要有序摄取。有界流处理通常被称为批处理。
批处理示例
- 批处理是流处理的一种非常特殊的情况。在流处理中,我们为数据定义滑动窗口或滚动窗口,并且在每次窗口滑动或滚动时生成结果。批处理则不同,我们定义一个全局窗口,所有的记录都属于同一个窗口。举例来说,以下代码表示一个简单的Flink程序,它负责每小时对某网站的访问者计数,并按照地区分组。

- 如果知道输入数据是有限的,则可以通过以下代码实现批处理。

- 如果输入数据是有限的,那么下面代码与上面代码的运行结果相同。

Flink批处理模型
- Flink通过一个底层引擎同时支持流处理和批处理

流与批处理机制
- Flink的两套机制分别对应各自的API(DataStreamAPI和DataSetAPI),在创建Flink作业时,并不能通过将两者混合在一起来同时利用Flink的所有功能。
- Flink支持两种关系型的API,TableAPI和SQL。这两个API都是批处理和流处理统一的API,这意味着在无边界的实时数据流和有边界的历史记录数据流上,关系型API会以相同的语义执行查询,并产生相同的结果。
- TableAPI/SQL正在以流批统一的方式成为分析型用例的主要API。
- DataStreamAPI是数据驱动应用程序和数据管道的主要API。
时间背景
- 在流处理器编程中,对于时间的处理是非常关键的。比如计数的例子,事件流数据(例如服务器日志数据、网页点击数据和交易数据)不断产生,我们需要用key将事件分组,并且每隔一段时间就针对每一个key对应的事件计数。这就是我们熟知的"大数据"应用。
流处理中的时间分类
- 在数据流处理过程中,我们经常使用系统时间即:processingtime作为某个事件的时间,而实际上系统时间processingtime是我们强加给事件的时间,由于网络延迟等原因并不能较好的反应事件之间发生的先后顺序。
- 在实际场景中,每个事件的时间可以分为三种:
- event time,即事件发生时的时间;
- ingestiontime,即事件到达流处理系统的时间;
- processing time,即事件被系统处理的时间。
三种时间示例
- 例如,一条日志进入Flink的时间为2019-11-1210:00:00.123,到达Window的系统时间为2019-11-1210:00:01.234,日志的内容如下:
- 2019-11-02 18:37:15.624 INFO Fail over to rm2
- 2019-11-02 18:37:15.624是Event Time;
- 2019-11-12 10:00:00.123是Ingestion Time;
- 2019-11-12 10:00:01.234是Processing Time;
- 2019-11-02 18:37:15.624 INFO Fail over to rm2
三种时间的区别
- 实际情况中事件真正发生的先后顺序与系统时间存在一定的差异,这些差异主要由网络延迟、处理时间的长短等造成。如图所示:

- 横坐标代表Eventtime,纵坐标代表processing time。理想情况下,eventtime和processingtime构成的坐标应该形成一条倾斜角为45度的线。但实际应用过程中,processing time要落后与eventtime,造成事件到来的先后顺序不一致。
Flink支持的时间语义

Window概述
- 流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而Window是一种切割无限数据为有限块进行处理的手段。
- Window是无限数据流处理的核心,它将一个无限的stream拆分成有限大小的"buckets"桶,我们可以在这些桶上做计算操作。
Window类型
- Window可以分成两类:
- CountWindow:数据驱动,按照指定的数据条数生成一个Window,与时间无关。
- TimeWindow:时间驱动,按照时间生成Window。
- ApacheFlink是一个天然支持无限流数据处理的分布式计算框架,在Flink中Window可以将无限流切分成有限流。Flink中Window可以是TimeWindow,也可以是CountWindow。
TimeWindow分类
- TimeWindow可以根据窗口实现原理的不同分成三类:
- 滚动窗口(Tumbling Window)
- 滑动窗口(Sliding Window)
- 会话窗口(Session Window)。
滚动窗口
- 将数据依据固定的窗口长度对数据进行切片。
- 特点:时间对齐,窗口长度固定,没有重叠。
- 适用场景:适合做BI统计等(做每个时间段的聚合计算)。

滑动窗口
- 滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。
- 特点:时间对齐,窗口长度固定,有重叠。
- 适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率来决定是否要报警)。

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

代码定义
- 在Flink中,一分钟滚动窗口的定义如下:
- stream.timeWindow(Time.minutes(1))
- 每半分钟(即30秒)滑动一次的一分钟滑动窗口,如下所示:
- stream.timeWindow(Time.minutes(1),Time.seconds(30))
乱序问题
- 流处理从事件产生到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的EventTime顺序排列的。

乱序示例
- 例子:某App会记录用户的所有点击行为,并回传日志(在网络不好的情况下,先保存在本地,延后回传)。A用户在11:02对App进行操作,B用户在11:03对App进行操作,但是A用户的网络不太稳定,回传日志延迟了,导致我们在服务端先接受到B用户11:03的消息,然后再接受到A用户11:02的消息,消息乱序了。
为什么需要Watermark
- 对于无穷数据集,我们缺乏一种有效的方式来判断数据完整性,因此就有了Watermark,它是建立在事件时间上的一个概念,用来刻画数据流的完整性。如果按照处理时间来衡量事件,一切都是有序的、完美的,自然而然也就不需要Watermark了。换句话说事件时间带来了乱序的问题,而Watermark就是用来解决乱序问题。所谓的乱序,其实就是有事件延迟了,对于延迟的元素,我们不可能无限期的等下去,必须要有一种机制来保证一个特定的时间后,必须触发Window进行计算。这个特别的机制,就是Watermark,它告诉了算子延迟到达的消息不应该再被接收。
Watermark的原理
- Flink怎么保证基于event-time的窗口在销毁的时候,已经处理完了所有的数据呢?这就是watermark的功能所在。watermark会携带一个单调递增的时间戳t,Watermark(t)表示所有时间戳不大于t的数据都已经到来了,未来小于等于t的数据不会再来,因此可以放心地触发和销毁窗口了。
- 有序流的Watermarker如下图所示:(Watermark设置为o)

- 乱序流的Watermarker如下图所示:(Watermark设置为2)

延迟的数据
- Watermark能够应对乱序的数据,但是真实世界中没法得到一个完美的Watermark数值。要么没法获取到,要么耗费太大,因此实际工作中会近似Watermark(t)之后,还有较小的概率接受到时间戳t之前的数据,在Flink中将这些数据定义为"lateelements",同样可以在Window中指定允许延迟的最大时间(默认为0),可以使用下面的代码进行设置:

延迟数据处理机制
- 延迟事件是乱序事件的特例,和一般乱序事件不同的是它们的乱序程度超出了水位线(Watermark)的预计,导致窗口在它们到达之前已经关闭。
- 延迟事件出现时窗口已经关闭并产出了计算结果,对于此种情况处理的方法有3种:
- 重新激活已经关闭的窗口并重新计算以修正结果。
- 将延迟事件收集起来另外处理。
- 将延迟事件视为错误消息并丢弃。
- Flink默认的处理方式是第3种直接丢弃,其他两种方式分别使用SideOutput和Allowed Latenesso
Side Output机制
- SideOutput机制可以将延迟事件单独放入一个数据流分支,这会作为Window计算结果的副产品,以便用户获取并对其进行特殊处理。
- 设置allowedLateness之后,迟来的数据同样可以触发窗口,进行输出,利用Flink的sideoutput机制,可以获取到这些延迟的数据,使用方式如下:

Allowed Lateness机制
- AllowedLateness机制允许用户设置一个允许的最大延迟时长。Flink会在窗口关闭后一直保存窗口的状态直至超过允许延迟时长,这期间的延迟事件不会被丢弃,而是默认会触发窗口重新计算。因为保存窗口状态需要额外内存,并且如果窗口计算使用了ProcessWindowFunctionAPI还可能使得每个延迟事件触发一次窗口的全量计算,代价比较大,所以允许延迟时长不宜设得太长,延迟事件也不宜过多。
Checkpoint
- Flink如何保证exactly-once呢?
- Flink使用一种被称为"检查点(Checkpoint)"的特性,在出现故障时将系统重置回正确状态。
- Flink容错机制基于异步轻量级的分布式快照技术,连续处理分布式流数据的快照,可以将同一时间点任务/操作的State数据全局统一地进行快照处理,即对程序中的状态进行备份。
Checkpoint检查点机制
- Flink提供Checkpoints容错机制。分布式快照可以将同一时间点Task/Operator的状态数据全局统一快照处理。Flink会在输入的数据集上间隔性地生成checkpointbarrier,通过栅栏(barrier)将间隔时间段内的数据划分到相应的Checkpoint中。当应用出现异常时,Operator就能够从上一次快照中恢复所有算子之前的状态,从而保证数据的一致性。
- 对流处理应用程序来说,这些快照非常轻巧,可以在不影响性能的情况下频繁操作。
- 对于状态占用空间比较小的应用,快照产生过程非常轻量,高频率创建且对Flink任务性能影响相对较小。Checkpoint过程中状态数据一般被保存在一个可配置的环境中,通常是在JobManager节点或HDFS上。
Checkpoint原理
- 分布式快照的核心要素是barriers(栅栏)。这些barriers将注入数据流中,并与记录一起作为数据流的一部分一起流动。barriers严格按照顺序排列。一个barriers将数据流中的记录分为两部分,一部分记录进入当前快照,另一部分记录进入下一个快照。barriers不会中断数据流的流动,因此是一种轻量级的快照。来自不同快照的多个barriers可以同时出现在数据流中,这意味着可以同时进行各种快照。

Checkpoint配置
- 默认情况下Flink不开启检查点,用户需要在程序中通过调用enableCheckpointing(n)方法配置和开启检查点,其中n为检查点执行的时间间隔,单位为毫秒。

- exactly-once和at-least-once语义选择
- exactly-once:保证端到端数据一致性,数据要求高,不允许出现数据丢失和数据重复,Flink的性能也相对较弱;
- at-least-once:时延和吞吐量要求非常高但对数据的一致性要求不高的场景。
- Flink默认使用exactly-once模式,可以通过setCheckpointingMode()方法来设定语义模式。

- Checkpoint超时时间
- 指定每次Checkpoint执行过程中的上限时间范围,一旦Checkpoint执行时间超过该阈值,Flink将会中断Checkpoint过程,并按照超时处理。
- 该指标可以通过setCheckpointTimeout方法设定,默认10分钟。

- 检查点之间最小时间间隔
- 设定两个Checkpoint之间的最小时间间隔,防止出现状态数据过大而导致Checkpoint执行时间过长,从而导致Checkpoint积压过多,最终Flink应用密集地触发Checkpoint操作,会占用大量计算资源而影响到整个应用的性能。

- 最大并行执行的检查点数量
- 设定能够同时执行的Checkpoint数量。在默认情况下只有一个检查点可以运行,根据用户指定的数量可以同时触发多个Checkpoint,从而提升Checkpoint整体的效率。

- 外部检查点
- 设定周期性的外部检查点,然后将状态数据持久化到外部系统中,使用这种方式不会在任务停止的过程中清理掉检查点数据,而是一直保存在外部系统介质中,也可以通过从外部检查点中对任务就行恢复。

作业如何恢复数据?
- Flink在Cancel时允许在外部介质保留Checkpoint;另一方面,Flink还有另外一个机制是SavePoint。
- Savepoints是检查点的一种特殊实现,底层其实是使用Checkpoints的机制。Savepoints是用户以手工命令的方式触发,并将结果持久化到指定的存储路径中,目的是帮助用户在升级和维护集群过程中保存系统中的状态数据,避免因为停机运维或者升级应用等正常终止应用的操作而导致系统无法恢复到原有的计算状态的情况,从而无法实现端到端的Exactly-Once语义保证。
Savepoint与Checkpoint

State Backend
- 用DataStreamAPI编写的程序通常以各种形式保存状态,在启动CheckPoint机制时,状态会随着CheckPoint而持久化,以防止数据丢失,保障数据恢复时的一致性。状态内部的存储格式、状态在CheckPoint时如何持久化及其持久化在哪里,均取决于所选择的State Backend。
状态的存储方式-MemoryStateBackend
- 构造方式
- MemoryStateBackend(int maxStateSize, boolean asynchronousSnapshots)
- 存储方式
- State:TaskManager内存;
- Checkpoint:JobManager内存。
- 容量限制
- 单个State maxStateSize默认5 MB;
- maxStateSize <= akka.framesize,默认10 MB。
- 总大小不超过JobManager的内存。
- 推荐使用的场景:本地测试;几乎无状态的作业,比如ETL。
状态的存储方式-FsStateBackend
- 构造方式
- FsStateBackend(URI checkpointDataUri, boolean asynchronousSnapshots)
- 存储方式
- State:TaskManager内存;
- CHeckpoint:外部文件存储系统(本地或HDFS)。
- 容量限制
- 单TaskManager上State总量不超过它的内存;
- 总大小不超过配置的文件系统容量。
- 推荐使用的场景:常规使用状态的作业,例如分钟级别窗口聚合、Join;需要开启HA的作业;可以在生产场景使用。
状态的存储方式-RocksDBStateBackend
- 构造方式
- RocksDBStateBackend(URI checkpointDataUri, boolean enablelncrementalCheckpointing)
- 存储方式
- State:TaskManager上的Kv数据库(实际使用内存+磁盘);
- CHeckpoint:外部文件存储系统(本地或HDFS)。
- 容量限制
- 单TaskManager上State总量不超过它的内存+磁盘;
- 单Key最大2 GB;
- 总大小不超过配置的文件系统容量。
- 推荐使用的场景:超大状态的作业,例如天级别窗口聚合;需要开启HA的作业;对状态读写性能要求不高的作业;可以在生产场景使用。
缩略语
