一文详解Flink Exactly-Once

前言

Flink的 ”精确一次“ 处理语义是,Flink提供了一个强大的的语义保证,也就是说在任何情况下都能保证数据对应用的效果只有一次,不会多也不会少。

那么Flink是如何实现”端到端的精确一次处理“语义的呢?其实做到端到端的精确一次处理主要考虑两个方面:

  • 怎么保证数据不丢失
  • 怎么保证数据不重复

背景

通常情况下,流式计算系统都会为用户提供数据处理的可靠模式功能,用来表明在实际生产运行中会对数据处理做哪些保障。一般来说,流处理引擎通常为用户的应用程序提供三种数据处理语义:最多一次,至少一次和精确一次

  • 最多一次(At-most-Once):这种语义理解起来很简单,用户的数据只会被处理一次,不管成功还是失败,不会重试也不会重发。
  • 至少一次(At-least-Once):这种语义下,系统会保证数据或事件至少被处理一次。如果发生错误或者丢失,那么会从源头重新发送一条然后进入处理系统。所以同一个事件或者消息会被处理很多次。
  • 精确一次(Exactly-Once):表示每一条数据只会被精确地处理一次,不多也不少。
Exactly-Once是Flink,Spark等流处理系统的核心特性之一,这种语义会保证每一条消息只被流处理系统处理一次。”精确一次“语义是Flink 1.4.0版本引入的一个重要特性,而且,Flink号称支持”端到端的精确一次“语义。

这里解释一下”端到端的精确一次“,它指的是Flink应用从Source端开始到Sink端结束,数据必须经过的起始点和结束点。Flink自身是无法保证外部系统”精确一次“语义的,所以Flink若要实现所谓”端到端的精确一次“的要求,那么外部系统必须支持”精确一次“语义,然后借助Flink提供的分布式快照和两阶段提交才能实现。

分布式快照机制

Flink提供了失败恢复的容错机制,而这个容错机制的核心就是持续创建分布式数据流的快照来实现,这也为了数据不丢失提供了基础保障。

同Spark相比,Spark仅仅是针对Driver的故障恢复Checkpoint。而Flink的快照可以到算子级别,并且对全局数据也可以做快照。

Barrier

Flink分布式快照的核心元素之一是Barrier(数据栅栏),可以把Barrier简单理解成为一个标记,该标记是严格有序的,并且随着数据流往下流动。每个Barrier都带有自己的ID,Barrier极其轻量,并不会干扰正常的数据处理。

一文详解Flink Exactly-Once_第1张图片

如上图所示,假如我们有一个从左向右流动的数据流,Flink会依次生成snapshot 1,snapshot 2,snapshot 3....Flink中有一个专门的”协调者“负责收集每个snapshot的位置信息,这个协调者也是高可用的。

Barrier会随着正常数据继续往下流动,每当遇到一个算子,算子会插入一个标识,这个标识的插入时间是上游所有的输入流都接受到了snapshot n。与此同时,当我们sink算子接收到所有上游流发送的Barrier时,那么就表明这一批数据处理完毕,Flink会向协调者发送确认消息,表明当前snapshot n完成了。当所有sink算子都确认这批数据成功处理后,那么本次的snapshot被标识完成。

这里就会有一个问题,因为Flink运行在分布式环境中,一个operator的上游会有很多流,每个流的barrier n到达的时间不一致怎么办?这里Flink采取的措施是:快流等慢流

当其中一个流到的早,其他流到的晚。当第一个barrier n到来后,当前的operator会继续等待其他流的barrier n。直到所有barrier n到来后,operator才会把所有的数据向下发送。

异步和增量

按照上面介绍的机制,每次把快照存储到我们的状态后端时,如果同步进行就会阻塞正常任务,从而引入延迟。因此Flink在做快照存储的时候,可采用异步方式。

此外,由于checkpoint是一个全局状态,用户保存的状态可能非常大,多数达G或者T级别。在这种情况下,checkpoint的创建会非常慢,而且执行时占用的资源也比较多,因此Flink提出了增量快照,也就是说,每次都是进行的全量checkpoint,是基于上次进行更新的。

两阶段提交

上文提到基于checkpoint的快照操作,快照机制能够保证作业出现fail-over后可以从最新的快照进行恢复,即分布式快照机制可以保证flink系统内部的”精确一次“处理。但是我们实际生产系统中,Flink会对接各种各样的外部系统,比如kafka,HDFS等,一旦Flink作业出现失败,作业会重新消费旧数据,这个时候就会出现重新消费的情况,也就是重复消费。

针对这种情况,Flink在1.4版本引入了一个很重要得功能:两阶段提交,也即是TwoPhaseCommitSinkFunction。两阶段搭配特定得source和sink(特别是0.11版本kafka)使得”精确一次处理语义“成为可能。

在Flink中两阶段提交的实现方式被封装到TwoPhaseCommitSinkFunction这个抽象类中,我们只需要实现其中的beginTransaction,preCommit,commit,abort四个方法就可实现”精确一次“的处理语义,实现的方式可以在官网中查到。

  • beginTransaction,在开启事务之前,我们在目标文件系统的临时目录中创捷一个临时文件,后面在处理数据时将数据写入此文件。
  • preCommit,在预提交阶段,刷写(flush)文件,然后关闭文件,之后就不能再写入文件了。我们还将为属于下一个检查点的任何后续写入启动新事务。
  • commit,在提交阶段,我们将预提交的文件原子性的移动到真正的目标目录中,这里会增加输出数据可见性的延迟
  • abort,在中止阶段,删除临时文件。

一文详解Flink Exactly-Once_第2张图片

如上图所示,Kafka-Flink-Kafka案例实现”端到端精确一次“语义的过程,整个过程包括:

  • 从kafka读取数据
  • 窗口聚合操作
  • 将数据写回kafka

整个过程可以总结为下面四个阶段:

  • 一旦Flink开始做checkpoint操作,那么就会pre-commit阶段,同时Flink JobManger会将检查点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(v 0.10)/exactly once(v 1.0)
Twitter Streaming 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
Flie sinks at least once
Sockets sinks at least once
Standard output at least once
Redis sink at least once

总结

由于强大的异步快照机制和两阶段提交,Flink实现了”端到端的精确一次语义“,在特定的业务场景下十分重要,我们在进行业务开发需要语义保证时,要十分熟悉目前Flink支持的语义特性。

你可能感兴趣的:(flink)