第 5 章 有状态的计算
5.2 检查点:保证 exactly-once
Flink 如何保证 exactly-once 呢?它使用一种被称为“检查点”的特性,在 出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点的 作用。
假设你和两位朋友正在数项链上有多少颗珠子,如图5-2 所示。你捏住珠
图灵社区会员 ArthurWYS([email protected]) 专享 尊重版权
52 | 第 5 章
子,边数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手 中的珠子。当你分神忘记数到哪里时,怎么办呢?如果项链上有很多珠子,
你显然不想从头再数一遍,尤其是当三人的速度不一样却又试图合作的时 候,更是如此(比如想记录前一分钟三人一共数了多少颗珠子,回想一下 第4 章中的一分钟滚动窗口)。
图5-2:数环状项链上的珠子看上去毫无意义(甚至有些徒劳无功,因为可以永不 停歇地计数),但是它可以用来很好地类比处理永不结束的事件流。在某些文化中,
人们仍旧将数珠子视作消磨时间的好方法
于是,你想了一个更好的办法:在项链上每隔一段就松松地系上一根有色 皮筋,将珠子分隔开;当珠子被拨动的时候,皮筋也可以被拨动;然后,
你安排一个助手,让他在你和朋友拨到皮筋时记录总数。用这种方法,当 有人数错时,就不必从头开始数。相反,你向其他人发出错误警示,然后 你们都从上一根皮筋处开始重数,助手则会告诉每个人重数时的起始数值,
例如在粉色皮筋处的数值是多少。
Flink 检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是:对于 指定的皮筋而言,珠子的相对位置是确定的;这让皮筋成为重新计数的参 考点。总状态(珠子的总数)在每颗珠子被拨动之后更新一次,助手则会
有状态的计算 | 53 保存与每根皮筋对应的检查点状态,如当遇到粉色皮筋时一共数了多少珠 子,当遇到橙色皮筋时又是多少。当问题出现时,这种方法使得重新计数 变得简单。
检查点是Flink 最有价值的创新之一,因为它使 Flink 可以保
证exactly-once,并且不需要牺牲性能。
Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。
记住这一基本点之后,我们用一个例子来看检查点是如何运行的。Flink 为 用户提供了用来定义状态的工具。例如,以下这个Scala 程序按照输入记录 的第一个字段(一个字符串)进行分组并维护第二个字段的计数状态。
val stream: DataStream[(String, Int)] = ...
val counts: DataStream[(String, Int)] = stream .keyBy(record => record._1)
.mapWithState((in: (String, Int), count: Option[Int]) =>
count match {
(checkpoint barrier)隔开,所有的map算子状态均为0(计数还未开始)。
所有key 为 a 的记录将被顶层的map算子处理,所有key 为 b 的记录将被中 间层的map算子处理,所有key 为 c 的记录则将被底层的map算子处理。
54 | 第 5 章
图5-3:程序的初始状态。注意,a、b、c 三组的初始计数状态都是 0,即三个圆 柱上的值。ckpt 表示检查点屏障。每条记录在处理顺序上严格地遵守在检查点之前 或之后的规定,例如["b",2] 在检查点之前被处理,["a",2] 则在检查点之后被处理 当该程序处理输入流中的6 条记录时,涉及的操作遍布 3 个并行实例(节 点、CPU 内核等)。那么,检查点该如何保证 exactly-once 呢?
检查点屏障和普通记录类似。它们由算子处理,但并不参与计算,而是会 触发与检查点相关的行为。当读取输入流的数据源(在本例中与keyBy算 子内联)遇到检查点屏障时,它将其在输入流中的位置保存到稳定存储中。
如果输入流来自消息传输系统(Kafka 或 MapR Streams),这个位置就是偏 移量。Flink 的存储机制是插件化的,稳定存储可以是分布式文件系统,如 HDFS、S3 或 MapR-FS。图 5-4 展示了这个过程。
有状态的计算 | 55 保存检查点屏障在
输入流中的位置
稳定存储
图5-4:当 Flink 数据源(在本例中与 keyBy 算子内联)遇到检查点屏障时,它会将 其在输入流中的位置保存到稳定存储中。这让Flink 可以根据该位置重启输入 检查点屏障像普通记录一样在算子之间流动。当map算子处理完前3 条记录 并收到检查点屏障时,它们会将状态以异步的方式写入稳定存储,如图5-5 所示。
邮 电
56 | 第 5 章
异步备份计数器
位置1 位置2 位置3
图5-5:位于检查点之前的所有记录(["b",2]、["b",3] 和 ["c",1])被 map 算子处理之 后的情况。此时,稳定存储已经备份了检查点屏障在输入流中的位置(备份操作发 生在检查点屏障被输入算子处理的时候)。map 算子接着开始处理检查点屏障,并触 发将状态异步备份到稳定存储中这个动作
当map算子的状态备份和检查点屏障的位置备份被确认之后,该检查点操 作就可以被标记为完成,如图5-6 所示。我们在无须停止或者阻断计算的 条件下,在一个逻辑时间点(对应检查点屏障在输入流中的位置)为计算 状态拍了快照。通过确保备份的状态和位置指向同一个逻辑时间点,后文 将解释如何基于备份恢复计算,从而保证exactly-once。值得注意的是,当 没有出现故障时,Flink 检查点的开销极小,检查点操作的速度由稳定存储 的可用带宽决定。回顾数珠子的例子:除了因为数错而需要用到皮筋之外,
皮筋会被很快地拨过。(Flink 的开发团队正在研究如何只保存状态的变化,
而不保存状态的值,这样做可以让开销变得更小。)
图灵社区会员 ArthurWYS([email protected]) 专享 尊重版权
有状态的计算 | 57 位置1 位置2 位置3
检查点操作完成
图5-6:检查点操作完成,状态和位置均已备份到稳定存储中。输入流中的所有记 录都已处理完成。值得注意的是,备份的状态值与实际的状态值是不同的。备份反 映的是检查点的状态
如果检查点操作失败,Flink 会丢弃该检查点并继续正常执行,因为之后的 某一个检查点可能会成功。虽然恢复时间可能更长,但是对于状态的保证 依旧很有力。只有在一系列连续的检查点操作失败之后,Flink 才会抛出错 误,因为这通常预示着发生了严重且持久的错误。
现在来看看图5-7 所示的情况:检查点操作已经完成,但故障紧随其后。
58 | 第 5 章
位置1 位置2 位置3 处理失败!
图5-7:故障紧跟检查点,导致最底部的实例丢失
在这种情况下,Flink 会重新拓扑(可能会获取新的执行资源),将输入流 倒回到上一个检查点,然后恢复状态值并从该处开始继续计算。在本例中,
["a",2]、["a",2] 和 ["c",2] 这几条记录将被重播。
图5-8 展示了这一重新处理过程。从上一个检查点开始重新计算,可以保 证在剩下的记录被处理之后,得到的map算子的状态值与没有发生故障 时的状态值一致。值得注意的是,输出流会含有重复的数据。具体来说,
["a",2]、["a",4] 和 ["c",3] 会出现两次。如果 Flink 将输出流写入特殊的输出 系统(比如文件系统或者数据库),那么就可以避免这个问题,本章稍后将 进一步讨论。
有状态的计算 | 59 位置1 位置2 位置3
恢复位置及状态值
图5-8:Flink 将输入流倒回到上一个检查点屏障的位置,同时恢复 map 算子的状态值。
然后,Flink 从此处开始重新处理。这样做保证了在记录被处理之后,map 算子的状 态值与没有发生故障时的一致
Flink 检查点算法的正式名称是异步屏障快照(asynchronous barrier snapshotting)。
该算法大致基于Chandy-Lamport 分布式快照算法。