1. 什么是Flink?
Flink是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。能以内存速度和任意规模进行计算。
- 分布式:存储或计算交由多台服务器完成,最后汇总起来达到最终的效果;
- 实时:处理速度是毫秒级或者秒级的;
- 计算:对数据进行处理,比如清洗数据(对数据进行规整,取出有用的数据)。
Flink优于Storm的地方有哪些呢?
1.1 有边界和无边界?
像MQ这种没有做任何处理的消息(来一条,处理一条),默认就是无边界的;
无边界的基础上加上条件,那就是有边界的。
比如要做数据统计:每个小时的
pv
(page view)是多少,那就设置1小时的边界,攒着一小时的数据来处理一次。
在Flink
上,设置“边界”这种操作叫做开窗口(Windows
),窗口可简单分为两种类型:
- 时间窗口(
TimeWindows
):按照时间窗口进行聚合,累计一个小时的数据处理一次。- 计数窗口(
CountWindows
):按照指定的条数来进行聚合,如: 每来10条数据处理一次。
Flink
使用窗口聚合时,指定”时间语义“来保证数据的准确性。可以指定聚合的时间以Event Time(
事件发生的时间--
日志真正记录的时间)
来进行处理;不指定时默认是以Processing Time(
数据到Flink的时间)
来进行聚合处理。
虽然指定了聚合的时间为Event Time
,但还是没解决数据乱序的问题(比如06分产生了5条数据,实际上06分只收到了3条,而剩下的2条在07分才收到,那此时怎么办呢?在06分时该不该聚合,07分收到的两条06分数据怎么办?)
Flink
提出设置水位线(waterMarks
),即存在网络延迟等情况导致数据接收不是有序的,可根据实际情况,设置一个延迟时间(如1min),等延迟的时间到了,再统一聚合。
因为设置了
Event Time
,所以Flink
可以检测到每一条记录发生的时间,而waterMarks
设置延迟一分钟,等到Flink
发现07分:59秒
的数据来到了Flink
,那就确信06分
的数据都来了(因为设置了1分钟延迟),此时才聚合06分
的窗口数据。
1.2 有状态和无状态?
无状态:每次的执行都不依赖上一次或上N次的执行结果,每次的执行都是独立的。
有状态:某次的执行需要依赖前面事件的处理结果。
一个例子:要统计文章的阅读PV
(page view),现在只要有一个点击了文章,在Kafka
就会有一条消息。现在要在流式处理平台上进行统计,那此时是有状态的还是无状态的?
可以依赖Flink
的“存储”,将每次的处理结果交由Flink
管理,执行计算的逻辑。可以简单的认为:Flink本身提供了”存储“的功能,而每次执行是可以依赖Flink的”存储”的,所以它是有状态的。
1.3 Flink
有状态数据的存储
- MemoryStateBackend 内存
- FsStateBackend 文件系统(HDFS)
- RocksDBStateBackend 本地数据库(RocksDB数据库)
2. 精确一次性(exactly once)
Flink
遇到意外事件挂了以后,有什么机制来尽可能保证处理数据不重复和不丢失呢?
假设
Flink
挂了,可能内存的数据没了,磁盘可能存储了部分的数据,那再重启的时候(比如MQ会重新拉取),就不怕会丢了或多了数据吗?
流的语义性有三种:
- 精确一次性(exactly once):有且只有一条,不多不少
- 至少一次(at least once):最少会有一条,只多不少
- 最多一次(at most once):最多只有一条,可能会没有
Flink
有一个比较出名的特性:精确一次性,其指的是:状态只持久化一次到最终的存储介质中(本地数据库/HDFS...)。计算的数据可能会重复(无法避免),但状态在存储介质上只会存储一次!!!
- 数据源需要可回放,发证故障可以重新读取未确认的数据;
Flink
需要把数据存到磁盘介质(不能用内存),发生故障可以恢复;- 发送源需要支持事务(从读到写需要事务的支持保证中途不失败)。
2.1 一个例子
Source
数据流[21,13,8,5,3,2,1,1]
,需在Flink
做累加操作(求和)。
Step.1: 处理完2,1,1
,累加值是4
,现在Flink
把累积后的状态4
已经存储起来了(认为前面2,1,1
这几个数字已经完全处理过了)。
Step.2: 处理了[5,3]
,现在累加值是12
,但Flink
还没来得及把12
存储到最终的介质,系统就挂掉了。
Step.3: Flink重启后会重新把系统恢复到累加值是4
的状态,所以[5,3]
得继续计算一遍,程序继续往下走。
2.2 CheckPoint容错机制
CheckPoint
其实就是Flink
会在指定的时间段上保存状态的信息,假设Flink
挂了可以将上一次状态信息再捞出来,重放还没保存的数据来执行计算,最终实现exactly once
。其中Flink
多长时间存储一次是由自己手动配置的。
1. 在Kafka
在业务上实现“至少一次”是怎么做的?
从
Kafka
把数据拉下来,处理完业务之后,手动提交offset
(告诉Kafka
已经处理完了),即做完了业务规则才将offset
进行commit
的。
2. CheckPonit
是怎么办到的呢?
与Kafka机制一样,等拉下来该条数据所有的流程走完,才进行真正的checkponit
。
checkpoint
是怎么知道拉下来的数据已经走完了呢?
Flink
在流处理过程中插入了barrier
,每个环节处理到barrier
都会上报(给JobManager
),等到sink
都上报了barrier
,就说明这次checkpoint
已经走完了,即JobManager
就去完成一次checkpoint。
注意:
Flink
实现的精确一次性只是保证内部的状态是精确一次的,如果想要端到端精确一次,需要端的支持。
3. Flink 作业流程
Flink
根据提交代码,生成一个StreamGraph
图,来代表程序的拓扑结构;在提交前会
对StreamGraph进行
优化(可以合并的任务进行合并),变成JobGraph;
将
JobGraph
提交给JobManager;
JobManager
根据JobGraph
生成ExecutionGraph(JobGraph
的并行化版本);
TaskManager
接收到任务之后会将ExecutionGraph
生成为真正的物理执行图。
物理执行图
真正运行在TaskManager
上Transform
和Sink
之间都会有ResultPartition
(用来发送数据)和InputGate
(接收数据)两个组件。
屏蔽掉这些Graph
,发现Flink
的架构是:
checkpoint
是由JobManager
发出。
checkpoint
是Flink
实现容错机制的关键,在实际使用中往往要进行相关的配置,如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000);
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
4. 流程模块拆解
4.1 Checkpoint触发
JobManager会启动一个定时任务,触发 triggerCheckpoint()方法。
前置检查(是否可以触发
checkpoint
,距离上一次checkpoint的间隔时间是否符合...);检查是否所有需要做
checkpoint
的Task都处于running
状态;生成
checkpointId
和PendingCheckpoint
对象来代表待处理的检查点;注册一个定时任务,如果
checkpoint
超时后取消checkpoint。
注意:检查task
的任务状态时,只会把source
的task
封装给进Execution[]
数组,
JobManager
侧只会给source
的task
发送checkpoint。
4.2 JobManager发送
JobManager
收到Client
提交的JobGraph,然后根据该
JobGraph
生成ExecutionGraph,
这个过程中会触发checkpoint
的逻辑。
定时任务会进行前置检查(检查配置的各种参数是否符合);
判断
checkpoint
相关的task
是否都是running
状态,将source
的任务封装到Execution
数组中;创建
checkpointID、checkpointStorageLocation
(checkpoint保存的地方)、PendingCheckpoint
(待处理的checkpoint);创建定时任务(如果
checkpoint
超时,会将相关状态清除,重新触发);真正触发
checkPoint
给TaskManager
(只会发给source
的task
);找出所有
source
和需要ack
的Task;创建
checkpointCoordinator
协调器;创建
CheckpointCoordinatorDeActivator
监听器,监听Job
状态的变更;当
Job
启动时,会触发ScheduledTrigger
定时任务。
4.3 TaskManager接收
4.3.1 source Task接收
JobManager
在生成ExcutionGraph
时,会给所有的source
任务发送checkpoint
,而source
收到barrier
后会交由TaskExecutor
进行处理。Source
任务接收到Checkpoint
会广播到下游,进行快照处理。
4.3.2 非source Task接收
Flink
接收数据用的是InputGate
,其包括两个类BarrierTracker、BarrierBuffer
。
BarrierTracker
是at-least-once
模式,只要inputChannel
接收到barrier
,就直接通知完成处理checkpoint;
BarrierBuffer
是exactly-once
模式,当所有的inputChannel
接收到barrier
才通知完成处理checkpoint
,如果有的inputChannel
还没接收到barrier
,那已接收到barrier
的inputChannel
会读数据到缓存中,直到所有的inputChannel
都接收到barrier
,这有可能会造成反压。即BarrierBuffer
会有对齐barrier
的处理。
一个例子:
有一个包含有两个
partition的Topic
,现在要拉取Kafka
这两个分区的数据,由算子Map
进行消费转换,期间在转化的时候可能会存储信息到State
,最终输出到Sink
。
在Flink
做checkpoint
的时候JobManager
往每个Source
任务(图中两个paritiion
) 发送checkpointId
,然后做快照存储。其中Source
任务存储最主要的内容就是消费分区的offset
。比如现在source1
的offerset
=100
,source2
的offset
=105
。
假设source2
的数据会比source1
先到达Map
且使用的是BarrierBuffer(exactly-once
模式),那么source2
的barrier
到达Map
算子后,source2
之后的数据只能停下来,放到buffer
上,等source1
的barrier
到达之后,再真正处理source2
放在buffer
的数据。即barrier
对齐。
假设使用的是BarrierTracker
(at-least-once
模式),那么source2
的barrier
到达Map
算子后,source2
后面的数据会继续处理,而非停下来等待source1
。
无论是BarrierTracker
还是BarrierBuffer
,此时Checkpoint
都没做(source1
的barrier
还没到sink
端),这时如果Flink
挂了,重启之后会重新拉取数据(source1
的offerset
<100
,source2
的offset
<105
),State
的最终信息不会保存,对数据不会产生任何的影响。
但如果使用的是BarrierTracker
(at-least-once
)模式,没有任何问题,程序继续执行。等到source1
的barrier
也走到了slink
,最后完成了一次checkpoint
。
由于source2
的barrier
比source1
的barrier
要快,那么source1
所处理的State
的数据实际是包括offset>105
的数据的,自然Flink
保存的时候也会把这部分保存进去。程序继续运行,刚好保存完checkpoint
后,此时系统出了问题,挂了。因为checkpoint
已经做完了,所以Flink
会从source1
的offerset
=100
,而source2
的offset
=105
重新消费。
但是,由于使用的是 at-least-once
模式,所以State
里边的保存状态实际上有过source2
的offset
>105
的记录了。那source2
重新从offset
=105
开始消费,就会重复消费!
4.3.3 TaskManager总结
TaskExecutor
接收到JobManager
下发的checkpoint
,会触发triggerCheckpoint
()调用performCheckpoint
()对checkpoint
做前置处理,barrier
广播到下游,处理State
状态做快照,最后返回成功消息给JobManager。
Source和
普通算子最终处理checkpoint
的逻辑是一致的,只是会source
会直接通过TaskExecutor
处理,而普通算子会根据不同的配置交由不同的实例(BarrierTracker
、BarrierBuffer
)处理。
4.4 JobManager接收回应
无论是source
还是普通算子,都会调用performCheckpoint
()进行处理。
5. Flink 反压原理
5.1 一个例子
从各个数据源借助
Flink
清洗出数据,组装成一个宽模型,最后交由kylin
做近实时数据统计和展示,供运营实时查看。
迁移过程中,发现订单的topic
消费延迟了好久,初步怀疑是因为订单上游的并发度
不够所影响的,调整了两端的并行度重新发布,系统起来后,topic
消费延迟丝毫没有下降。排查发现:checkpoint
一直没做上,都堵住了,重新发布时只会在上一次checkpoint
开始,由于checkpoint
长时间没完成掉,所以重新发布数据量会很大。只能在这个堵住的环节下扔掉吧,估计是业务逻辑出了问题。
接收到订单的数据,会去溯源点击,判断该订单从哪个业务来,经过了哪些的业务,最终是哪块业务致使该订单成交。外部真正使用时,依赖「订单结果HBase」数据。
有可能点击数据会比订单数据处理要慢一些,而找不到的数据会间隔一段时间轮询,又因为Flink
提供状态State
和checkpoint
机制,所以把找不到的数据放入ListState
按一定的时间轮询就好了(即便系统由于重启或其他原因挂了,也不会把数据丢了)。
但订单数据报来了之后,一小批量数据一直在「订单结果HBase」没找到数据,就放置到ListState
上,然后来一条数据就去遍历ListState
,这样会导致:
数据消费不过来,形成反压;
checkpoint
一直没成功。
当时处理的方式就是把ListState清空掉,暂时丢掉这一部分的数据,让数据追上进度。后来排查发现是上游在消息报字段上做了「手脚」,解析失败导致点击丢失,造成这一连锁的后果。
5.2 反压 backpressure
反压意味着数据管道中某个节点成为瓶颈,处理速率跟不上上游发送数据的速率,上游需要进行限速,是流式计算中很常见的问题。
下游是怎么通知上游要发慢点的呢?
而Flink
在一个TaskManager
内部读写数据的时候,会有一个BufferPool
(缓冲池)供该TaskManager
读写使用(一个TaskManager
共用一个BufferPool
),每个读写ResultPartition(
用来发送数据)/InputGate
(用来接收数据)都会去申请自己的LocalBuffer。
5.2.1
一个TaskManager
情况下的反压
假设下游处理不过来,InputGate
的LocalBuffer
被填满,那ResultPartition
就没办法往InputGate
发了,此时ResultPartition
本身的LocalBuffer
也迟早会被填满,一直到Source
就不会拉数据了...
5.2.2 多个TaskManager
情况下的反压
Flink
通信的总体数据流向架构图:
可以发现:
远程通信用的
Netty
,底层是TCP Socket
来实现的。从宏观的角度看,多个TaskManager
只不过多了两个Buffer
(缓冲区)。
只要InputGate
的LocalBuffer
被打满,Netty Buffer
也迟早被打满,而Socket Buffer
同样也会被打满(TCP 本身就带有流量控制),再反馈到ResultPartition
上,数据又发不出去了,这就导致整条数据链路都存在反压的现象。
一个TaskManager
的task
可是有很多的,它们都共用一个TCP Buffer/Buffer Pool
,那只要其中一个task
的链路存在问题,那整个TaskManager
跟着遭殃。
5.3 Credit
机制
为解决以上问题,Flink1.5版本
之后引入了credit
机制。以上Flink
所实现的反压,宏观上就是直接依赖各个Buffer
是否满了,如果满了则无法写入/读取导致连锁反应,直至Source
端。而credit
机制,实际上可以理解为以"更细粒度"去做流量控制:
1. 每次
InputGate(接收端)
会告诉ResultPartition(发送端)
自己还有多少的空闲量可以接收,让ResultPartition
看着发;2. 如果
InputGate
已经没有空闲量了,那ResultPartition
就不发了。
6. Flink 背压原理
下游的处理速度跟不上上游的发送速度,从而降低了整体处理速度。在Flink
里,背压再加上Checkponit
机制,很有可能导致State
状态一直变大,拖慢完成checkpoint
速度甚至超时失败。当checkpoint
处理速度延迟时,会加剧背压的情况(很可能大多数时间都在处理checkpoint
了)。
一个例子:
一个
Flink
任务,只有一台TaskManager
去执行任务,在更新DB时发现会有并发的问题。
排查发现:更新DB的 Sink 并行度调高了,若并行度设置为1,没有并发问题,但处理速度太慢。然后在Sink之前根据userId
进行keyBy
(相同的userId都由同一个Thread处理,这样就没并发的问题了)。但如果userId
存在热点数据问题,会导致下游数据处理形成反压
。原本一次checkpoint
执行只需要30~40ms
,反压
后一次checkpoint
需要2min+
。
checkpoint
执行间隔相对频繁(6s/次
),执行时间2min+
,最终导致数据一直处理不过来,整条链路的消费速度从原来的3000qps
到背压后的300qps
,一直堵住(程序没问题,就是处理速度大大下降,影响到数据的最终产出)。