声明:本系列博客为原创,最先发表在拉勾教育,其中一部分为免费阅读部分。被读者各种搬运至各大网站。所有其他的来源均为抄袭。
《2021年最新版大数据面试题全面开启更新》
Flink Exactly-once实现原理解析
Flink的“精准一次”处理语意是,Flink提供了一个强大的语义保证,也就是说在任何情况下都能保证数据对应用生产的效果只有一次,不会多也不会少。
Flink是如何实现“端到端的精确一次处理”语义的呢?
背景
通常情况下,流式计算系统都会为用户提供数据处理的可靠模式功能,用来表明在实际生产运行中会对数据处理做哪些保障。一般来说,流处理引擎通常为用户的应用程序提供三种处理语义:最多一次、至少一次和精确一次
- 最多一次(At-most-once):用户的数据只会被处理一次,不管成功还是失败,不会重试也不会重发
- 至少一次(At-least-once):系统保证数据或事件至少被处理一次。如果中间发生错误或者丢失,那么会从源头重新发生一条然后进入处理系统,所以同一个时间或者消息会被处理多次
- 精确一次(Exactly-once):表示每一条数据只会被精确地处理一次,不多也不少
Exactly-Once是Flink、Spark等流处理系统等核心特性之一,这种定义会保证每一条消息只被流处理系统处理一次。“精确一次”语义是Flink 1.4.0 版本引入的一个重要特性,而且,Flink号称支持“端到端的精确一次”语义。
“端到端(End to End)的精确一次”,它指的是Flink应用从Source端开始到Sink端结束,数据必须经过的起始点和结束点。Flink自身是无法保证外部系统“精确一次”语义的,所以Flink若要实现所谓“端到端的精确一次”的要求,那么外部系统必须支持“精确一次”语义,然后借助Flink提供的分布式快照和两阶段提交才能实现
分布式快照机制
Flink提供了失败回复的容错机制,而这个容错机制的核心就是持续创建分布式数据流的快照来实现。
与Spark相比,Spark仅仅是针对Driver的故障恢复Checkpoint。而Flink的快照可以找到算子级别,并对全局数据也可以做快照。Flink分布式快照受到Chandy-Lamport分布式快照算法启发,同时进行了量身定做。
Barrier
Flink分布式快照的核心元素之一是Barrier(数据栅栏),也可以把Barrier简单地理解成一个标记,该标记严格有序的,并且随着数据流往下流动,每个Barrier都带有自己的ID,Barrier及其轻量,并不会干扰正常的数据处理。
如上图所示,假如有一个从左向右流动的数据流,Flink会依次生产snapshot1、snapshot2、snapshot3...,Flink中有一个专门的“协调者”负责收集每个snapshot的位置信息,这个“协调者”也是高可用的。
Barrier会随着正常数据继续往下流动,每当遇到一个算子,算子会插入一个标识,这个标识的插入时间是上游所有的输入流都接收到snapshot n。与此同时,当我们的sink算子接受到所有上游发送到Barrier时,那么就表明这一批数据处理完毕,Flink会向“协调者”发送确认消息,表明当前的snapshot n完成了。当所有sink算子都确认这批数据成功处理后那么本次的snapshot被标识为完成。
这里有一个问题,因为Flink运行在分布式环境中,一个operator的上游会有很多流,每个流的barrier到达的时间不一致怎么办?Flink采取的措施是:快流等慢流
如上图barrier n来说,其中一个流到的早,其他的流到的比较晚。当第一个barrier n到来后,当前的operator会继续等待其他流的barrier n,直到所有的barrier n到来后,operator才会把所有的数据向下发送。
异步和增量
按照上面的机制,每次在把快照存储到状态后端时,如果是同步进行就会阻塞正常任务,从而导致延迟。因此Flink在做快照存储时,可采用异步方式。
此外,由于checkpoint是一个全局状态,用户保持的状态可能性非常大,多数达G或者T级别,在这种情况下,checkpoint的创建会非常慢,而且执行时占用的资源也比较多,因此Flink提出了增量快照的概念。也就是说每次进行的全量checkpoint,是基于上次进行更新的。
两阶段提交
上面介绍了基于checkpoint的快照操作,快照机制能够保证作业出现fail-over后可以从最新的快照进行恢复,即分布式快照可以保证Flink系统内部的“精确一次”处理。但是在实际生产系统中,Flink会对接各种各样的外部系统,比如Kafka、HDFS等,一旦Flink作业出现失败,作业会重新消费旧数据,也就是重复消费。
针对这种情况,Flink1.4版本引入了一个很重要的功能:两阶段提交,也就是TwoPhaseCommitSinkFunction。两阶段搭配特定的source和sink使得“精准一次处理语义”成为可能。
在Flink中两阶段提交的方法被封装到了TwoPhaseCommitSinkFunction这个抽象类中,我们需要实现其中的beginTransaction、preCommit、commit、abort四个方法就可以实现“精确一次”的处理语义,实现的方法可以在官网中查到:
- beginTransaction:在开启事务之前,我们在目标文件系统的临时目录中创建一个临时文件,后面在处理数据时将数据写入次文件
- preCommit:在预提交阶段,刷写(flush)文件,然后关闭文件,之后就不能写入到文件了,我们还将为属于下一个检查点的任务后续写入启动新事物
- commit:在提交阶段,将预提交的文件原子性移动到真正的目标目录中,请注意,这会增加输出数据可见性的延迟
- abort:在中止阶段,删除临时文件
如上图所示,我们用kafka-Flink-Kafka这个案例来介绍一下实现“端到端精确一次”抑郁的过程,整个过程包括:
- 从Kafka读取数据
- 窗口聚合操作
- 将数据写会Kafka
整个过程可以总结为厦门四个阶段:
- 一旦Flink开始做checkpoint操作,那么就会进入pre-commit阶段,同时Flink JobManager会讲检查点Barrier注入数据流中
- 当所有的barrier在算子中成功进行一遍传递,并完成快照后,则pre-commit阶段完成
- 等所有的算子完成“预提交”,就会发起一个“提交”动作,但是任何一个“预提交”失败都会导致Flink回滚到最近的checkpoint
- pre-commit完成。必须要确保commit也要成功,上图中sink operators和kafka sink会共同来保证。
现状
Flink目前支持的精确一次source列表如下表所示,你可以使用对应的connector来实现对应的语义要求:
数据源 | 语义保证 | 备注 |
---|---|---|
Apache kafka | exactly once | 需要对应的Kafka版本 |
AWS Kinesis Streams | exactly once | |
RabbitMQ | at most once(v0.1)/ exactly once(v1.0) | |
Twitter Streaming API | at most once | |
Collections | exactly once | |
Files | exactly once | |
Sockets | at most once |
如果需要实现真正的“端到端精确一次语义”,则需要sink的配合,目前Flink支持的列表如下所示:
写入目标 | 语义保证 | 备注 |
---|---|---|
HDFS rolling sink | exactly once | 依赖 Hadoop 版本 |
Elasticsearch | at least once | |
Kafka producer | at least once / exactly once | 需要 Kafka 0.11 及以上 |
Cassandra sink | at least once / exactly once | 幂等更新 |
AWS Kinesis Streams | at least once | |
File sinks | at least once | |
Socket sinks | at least once | |
Standard output | at least once | |
Redis sink | at least once |