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,一直堵住(程序没问题,就是处理速度大大下降,影响到数据的最终产出)。
701

被折叠的 条评论
为什么被折叠?



