flink端到端状态一致性

目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在 Flink
处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据
源(例如 Kafka )和输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每
一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一
致性最弱的组件。具体可以划分如下:
内部保证 —— 依赖 checkpoint
source 端 —— 需要外部源可重设数据的读取位置
sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统
而对于 sink 端,又有两种具体的实现方式:幂等( Idempotent )写入和事务性
Transactional )写入。
幂等写入
所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,
也就是说,后面再重复执行就不起作用了。
事务写入
需要构建事务来写入外部系统,构建的事务对应着 checkpoint ,等到 checkpoint
真正完成的时候,才把所有对应的结果写入 sink 系统中。
对于事务性写入,具体又有两种实现方式:预写日志( WAL )和两阶段提交
2PC )。 DataStream API 提供了 GenericWriteAheadSink 模板类和
TwoPhaseCommitSinkFunction 接口,可以方便地实现这两种方式的事务性写入。
不同 Source Sink 的一致性保证可以用下表说明:

 3 检查点(checkpoint

Flink 具体如何保证 exactly-once ? 它使用一种被称为 " 检查点 " checkpoint
的特性,在出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点
的作用。
假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边
数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你
分神忘记数到哪里时,怎么办呢 ? 如果项链上有很多珠子,你显然不想从头再数一
遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此 ( 比如想记录前一分
钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口 )
于是,你想了一个更好的办法 : 在项链上每隔一段就松松地系上一根有色皮筋,
将珠子分隔开 ; 当珠子被拨动的时候,皮筋也可以被拨动 ; 然后,你安排一个助手,
让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开
始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助
手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。
Flink 检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是 : 对于指定
的皮筋而言,珠子的相对位置是确定的 ; 这让皮筋成为重新计数的参考点。总状态
( 珠子的总数 ) 在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检
查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。
当问题出现时,这种方法使得重新计数变得简单。
1 Flink 的检查点算法
Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住
这一基本点之后,我们用一个例子来看检查点是如何运行的。 Flink 为用户提供了用
来定义状态的工具。例如,以下这个 Scala 程序按照输入记录的第一个字段 ( 一个字
符串 ) 进行分组并维护第二个字段的计数状态。

 

 

该程序有两个算子 : keyBy 算子用来将记录按照第一个元素 ( 一个字符串 ) 进行分
组,根据该 key 将数据进行重新分区,然后将记录再发送给下一个算子 : 有状态的
map 算子 (mapWithState) map 算子在接收到每个元素后,将输入记录的第二个字段
的数据加到现有总数中,再将更新过的元素发射出去。下图表示程序的初始状态 :
入流中的 6 条记录被检查点分割线 (checkpoint barrier) 隔开,所有的 map 算子状态均
0( 计数还未开始 ) 。所有 key a 的记录将被顶层的 map 算子处理,所有 key b
的记录将被中间层的 map 算子处理,所有 key c 的记录则将被底层的 map 算子处
理。

 

上图是程序的初始状态。注意, a b c 三组的初始计数状态都是 0 ,即三个圆
柱上的值。 ckpt 表示检查点分割线( checkpoint barriers )。每条记录在处理顺序上
严格地遵守在检查点之前或之后的规定,例如 ["b",2] 在检查点之前被处理, ["a",2]
则在检查点之后被处理。
当该程序处理输入流中的 6 条记录时,涉及的操作遍布 3 个并行实例 ( 节点、 CPU
内核等 ) 。那么,检查点该如何保证 exactly-once ?
检查点分割线和普通数据记录类似。它们由算子处理,但并不参与计算,而是
会触发与检查点相关的行为。当读取输入流的数据源 ( 在本例中与 keyBy 算子内联 )
遇到检查点屏障时,它将其在输入流中的位置保存到持久化存储中。如果输入流来
自消息传输系统 (Kafka) ,这个位置就是偏移量。 Flink 的存储机制是插件化的,持久
化存储可以是分布式文件系统,如 HDFS 。下图展示了这个过程。

 

Flink 数据源 ( 在本例中与 keyBy 算子内联 ) 遇到检查点分界线( barrier )时,
它会将其在输入流中的位置保存到持久化存储中。这让 Flink 可以根据该位置重启。
检查点像普通数据记录一样在算子之间流动。当 map 算子处理完前 3 条数据并
收到检查点分界线时,它们会将状态以异步的方式写入持久化存储,如下图所示。

 

位于检查点之前的所有记录 (["b",2] ["b",3] ["c",1]) map 算子处理之后的情
况。此时,持久化存储已经备份了检查点分界线在输入流中的位置 ( 备份操作发生在
barrier 被输入算子处理的时候 ) map 算子接着开始处理检查点分界线,并触发将状
态异步备份到稳定存储中这个动作。
map 算子的状态备份和检查点分界线的位置备份被确认之后,该检查点操作
就可以被标记为完成,如下图所示。我们在无须停止或者阻断计算的条件下,在一
个逻辑时间点 ( 对应检查点屏障在输入流中的位置 ) 为计算状态拍了快照。通过确保
备份的状态和位置指向同一个逻辑时间点,后文将解释如何基于备份恢复计算,从
而保证 exactly-once 。值得注意的是,当没有出现故障时, Flink 检查点的开销极小,
检查点操作的速度由持久化存储的可用带宽决定。回顾数珠子的例子 : 除了因为数
错而需要用到皮筋之外,皮筋会被很快地拨过。

 

检查点操作完成,状态和位置均已备份到稳定存储中。输入流中的所有数据记
录都已处理完成。值得注意的是,备份的状态值与实际的状态值是不同的。备份反
映的是检查点的状态。
如果检查点操作失败, Flink 可以丢弃该检查点并继续正常执行,因为之后的某
一个检查点可能会成功。虽然恢复时间可能更长,但是对于状态的保证依旧很有力。
只有在一系列连续的检查点操作失败之后, Flink 才会抛出错误,因为这通常预示着
发生了严重且持久的错误。
现在来看看下图所示的情况 : 检查点操作已经完成,但故障紧随其后。

 

在这种情况下, Flink 会重新拓扑 ( 可能会获取新的执行资源 ) ,将输入流倒回到
上一个检查点,然后恢复状态值并从该处开始继续计算。在本例中, ["a",2] ["a",2]
["c",2] 这几条记录将被重播。
下图展示了这一重新处理过程。从上一个检查点开始重新计算,可以保证在剩
下的记录被处理之后,得到的 map 算子的状态值与没有发生故障时的状态值一致。

 

Flink 将输入流倒回到上一个检查点屏障的位置,同时恢复 map 算子的状态值。
然后, Flink 从此处开始重新处理。这样做保证了在记录被处理之后, map 算子的状
态值与没有发生故障时的一致。
Flink 检查点算法的正式名称是异步分界线快照 (asynchronous barrier
snapshotting) 。该算法大致基于 Chandy-Lamport 分布式快照算法。
检查点是 Flink 最有价值的创新之一,因为 它使 Flink 可以保证 exactly-once
并且不需要牺牲性能
(讲的很抽象,我的理解就是存储一个数据状态,等故障时,从故障点重算,而不是整体重算)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大数据开发工程师-宋权

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值