😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持 💖💖💖 将激励 🔥 博主输出更多优质内容!!!
流处理基础概念(三):状态和一致性模型
状态在数据处理中无处不在,任何一个稍复杂的计算都要用它。为了生成结果,函数会在一段时间或基于一定个数的事件来累积状态(例如计算聚合或检测某个模式)。有状态算子同时使用 传入的事件 和 内部状态 来计算输出。以某个滚动聚合算子为例,假设它会输出至今为止所见到的全部事件之和。该算子以内部状态形式存储当前的累加值,并会在每次收到新事件时对其进行更新。类似地,假设还有一个算子会在每次检测到 “高温” 事件,且在随后 10 分钟内出现 “烟雾” 事件时报警。这个算子需要将 “高温” 事件存为内部状态,直到接下来发现 “烟雾” 事件或超过 10 分钟的时间限制。
在使用批处理系统分析无限数据集的情况下,状态的重要性会越发凸显。在现代流处理引擎兴起之前,处理无限数据的通用办法是将到来事件分成小批次,然后不停地在批处理系统上调度并运行作业。每当一个作业结束,其结果都会写入持久化存储中,同时所有算子的状态将不复存在。一旦某个作业被调度到下个批次上执行,它将无法访问之前的状态。该问题通常的解决方案是将状态管理交由某个外部系统(如数据库)完成。反之,在持续运行的流式作业中,每次处理事件所用到的状态都是持久化的,我们完全可以将其作为编程模型中的最高级别,按理说,我们也可以使用外部系统来管理流处理过程的状态,只是这样可能会引入额外延迟。
由于流式算子处理的都是潜在无穷无尽的数据,所以必须小心避免内部状态无限增长。为了限制状态大小,算子通常都会只保留到目前为止所见事件的摘要或概览。这种摘要可能是一个数量值,一个累加值,一个对至今为止全部事件的抽样,一个窗口缓冲或是一个保留了应用运行过程中某些有价值信息的自定义数据结构。
不难想象,支持有状态算子将面临很多实现上的挑战:
- 状态管理。系统需要高效地管理状态并保证它们不受并发更新的影响。
- 状态划分。由于结果需要同时依赖状态和到来的事件,所以状态并行化会变得异常复杂。幸运的是,在很多情况下可以把状态按照键值划分,并独立管理每一部分。举例而言,如果你要处理从一组传感器得到的测量值数据流,则可以用分区算子状态(
partitioned operator state
)来单独维护每个传感器的状态。 - 状态恢复。最后一个也是最大的挑战在于,有状态算子需要保证状态可以恢复,并且即使出现故障也要确保结果准确。
1.任务故障
在流式作业中,算子的状态十分重要,因此需要在故障时予以保护。如果状态在故障期间丢失,那恢复后的结果就会不正确。流式作业通常会运行较长时间,因此状态可能是经过数天甚至数月才收集得到。通过重新处理所有输入来重建故障期间丢失的状态,不仅代价高,而且很耗时。
在前面的博客中,我们讲述了如何将流处理程序建模成 Dataflow 图。在实际执行前,它们需要被翻译成物理 Dataflow 图,其中会包含很多相连的并行任务。每个任务都要运行一部分算子逻辑,消费输入流并为其他任务生成输出流。典型的现实系统设置都可以轻松做到在很多物理机器上并行运行数以百计的任务,对于长期运行的流式作业而言,每个任务都随时有可能出现故障。如何确保能够透明地处理这些故障,让流式作业得以继续运行?事实上,你不仅需要流处理引擎在出现故障时可以继续运行,还需要它能保证结果和算子状态的正确性。
1.1 什么是任务故障
对于输入流中的每个事件,任务都需要执行以下步骤。
- 接收事件并将它们存在本地缓冲区。
- 选择性地更新内部状态。
- 产生输出记录。
上述任何一个步骤都可能发生故障,而系统必须在故障情况下明确定义其行为。如果故障发生在第一步,事件是否会丢失?如果在更新内部状态后发生故障,系统恢复后是否会重复更新?在上述情况下,结果是否确定?
我们假设网络连接是可靠的,不存在记录丢失或重复,且所有事件最终都会以先进先出的顺序到达各自终点。由于 Flink 使用的是 TCP 连接,上述需求都能满足。我们还假设任何故障都会被检测到,没有任务故意捣乱。换言之,所有正常运行的任务都会遵循上面提到的步骤。
在批处理场景下,上面提到的都算不上问题。由于批处理任务可以轻易 “从头再来”,所以不会有任何事件丢失,状态也可以完全从最初开始构建。然而在流式场景中处理故障就没那么容易了,流处理系统通过不同的结果保障来定义故障时的行为。
2.结果保障
在讨论不同类型的保障之前,我们需要澄清一些在讨论流处理引擎任务故障时容易导致困惑的点。当提到 结果保障,我们指的是 流处理引擎内部状态的一致性。也就是说,我们关注故障恢复后应用代码能够看到的状态值。请注意,保证 应用状态的一致性 和保证 输出的一致性 并不是一回事儿。一旦数据从数据汇中写出,除非目标系统支持事务,否则结果的正确性将难以保证。
2.1 AT-MOST-ONCE 至多一次
任务发生故障时最简单的措施就是既不恢复丢失的状态,也不重放丢失的事件。至多一次是一种最简单的情况,它保证 每个事件至多被处理一次。换句话说,事件可以随意丢弃,没有任何机制来保证结果的正确性。这类保障也被称作 “没有保障”,因为即便系统丢掉所有事件也能满足其条件。无论如何,没有保障听上去都是个不靠谱的主意。但如果你能接受近似结果并且仅关注怎样降低延迟,这种保障似乎也可以接受。
2.2 AT-LEAST-ONCE 至少一次
对大多数现实应用而言,用户期望是不丢事件,这类保障称为至少一次。它意味着 所有事件最终都会处理,虽然有些可能会处理多次。如果正确性仅依赖信息的完整度,那重复处理或许可以接受。例如,确定某个事件是否在输入流中出现过,就可以利用至少一次保障正确地实现。它最坏的情况也无非就是多几次定位到目标事件。但如果要计算某个事件在输入流中出现的次数,至少一次保障可能就会返回错误的结果。
为了确保至少一次结果语义的正确性,需要想办法从源头或缓冲区中重放事件。持久化事件日志 会将所有事件写入永久存储,这样在任务故障时就可以重放它们。实现该功能的另一个方法是采用 记录确认(record acknowledgments
)。该方法会将所有事件存在缓冲区中,直到处理管道中所有任务都确认某个事件已经处理完毕才会将事件丢弃。
2.3 EXACTLY-ONCE 精确一次
精确一次是最严格,也是最难实现的一类保障,它表示 不但没有事件丢失,而且每个事件对于内部状态的更新都只有一次。本质上,精确一次保障意味着应用总会提供正确的结果,就如同故障从未发生过一般。
提供精确一次保障是 以至少一次保障为前提,因此同样需要数据重放机制。此外,流处理引擎需要确保内部状态的一致性,即在故障恢复后,引擎需要知道某个事件对应的更新是否已经反映到状态上。事务性更新 是实现该目标的一个方法,但它可能会带来极大的性能开销。Flink 采用了 轻量级检查点机制 来实现精确一次结果保障。我们会在后续讨论 Flink 的容错算法。
2.4 END-TO-END EXACTLY-ONCE 端到端的精确一次
至今为止你看到的保障类型都仅限于流处理引擎自身的应用状态。在实际流处理应用中,除了流处理引擎也至少还要有一个数据来源组件和一个数据终点组件。端到端的保障指的是在整个数据处理管道上结果都是正确的。在每个组件都提供自身的保障情况下,整个处理管道上端到端的保障会受制于保障最弱的那个组件。注意,有时候你可以 通过弱保障来实现强语义。一个常见情况就是某个任务执行一些诸如求最大值或最小值的幂等操作。该情况下,你可以用至少一次保障来实现精确一次的语义。