[译]Exactly once is NOT exactly the same

近日学习Pulsar文档时,注意到Pulsar提到其提供的是effectively-once语义,而不是其它流计算引擎announce的exactly-once语义,并引用了Exactly once is NOT exactly the same这篇文章加以注明。此处就将这篇观点很有意思的文章尝试翻译如下:

Exactly once is NOT exactly the same

分布式事件流处理正逐渐成为大数据领域中一个热门话题。著名的流处理引擎(Streaming Processing Engines, SPEs)包括Apache Storm、Apache Flink、Heron、Apache Kafka(Kafka Streams)以及Apache Spark(Spark Streaming)。流处理引擎中一个著名的且经常被广泛讨论的特征是它们的处理语义,而“exactly-once”是其中最受欢迎的,同时也有很多引擎声称它们提供“exactly-once”处理语义。
然而,围绕着“exactly-once”究竟是什么、它牵扯到什么以及当流处理引擎声称提供“exactly-once”语义时它究竟意味着什么,仍然存在着很多误解与歧义。而用来描述处理语义的“exactly-once”这一标签同样也是非常误导人的。在这篇博文当中,我将会讨论众多受欢迎的引擎所提供的“exactly-once”语义间的不同之处,以及为什么“exactly-once”更好的描述是“effective-once”。我还会讨论用来实现“exactly-once”的常用技术间的权衡(tradeoffs)

背景

流处理(streaming process),有时也被称为事件处理(event processing),可以被简洁地描述为对于一个无限的数据或事件序列的连续处理。一个流,或事件,处理应用可以或多或少地由一个有向图,通常是一个有向无环图(DAG),来表达。在这样一个图中,每条边表示一个数据或事件流,而每个顶点表示使用应用定义好的逻辑来处理来自相邻边的数据或事件的算子。其中有两种特殊的顶点,通常被称作sources与sinks。Sources消费外部数据/事件并将其注入到应用当中,而sinks通常收集由应用产生的结果。图1描述了一个流处理应用的例子。

A typical Heron processing topology

图1 一个典型的Heron处理拓扑

一个执行流/事件处理应用的流处理引擎通常允许用户制定一个可靠性模式或者处理语义,来标示引擎会为应用图的实体之间的数据处理提供什么样的保证。由于你总是会遇到网络、机器这些会导致数据丢失的故障,因而这些保证是有意义的。有三种模型/标签,at-most-once、at-least-once以及exactly-once,通常被用来描述流处理引擎应该为应用提供的数据处理语义。
接下来是对这些不同的处理语义的宽泛的定义:

At-most-once

这实质上是一个“尽力而为”(best effort)的方法。数据或者事件被保证只会被应用中的所有算子最多处理一次。这就意味着对于流处理应用完全处理它之前丢失的数据,也不会有额外的重试或重传尝试。图2展示了一个相关的例子:

At-most-once processing semantics

图2 At-most-once处理语义

At-least-once

数据或事件被保证会被应用图中的所有算子都至少处理一次。这通常意味着当事件在被应用完全处理之前就丢失的话,其会被从source开始重放(replayed)或重传(retransmitted)。由于事件会被重传,那么一个事件有时就会被处理超过一次,也就是所谓的at-least-once。图3展示了一个at-least-once的例子。在这一示例中,第一个算子第一次处理一个事件时失败,之后在重试时成功,并在结果证明没有必要的第二次重试时成功。

At-least-once processing semantics

图3 At-least-once处理语义

Exactly-once

倘若发生各种故障,事件也会被确保只会被流应用中的所有算子“恰好”处理一次。
拿来实现“exactly-once”的有两种受欢迎的典型机制:

  1. 分布式快照/状态检查点(checkpointing)
  2. At-least-once的事件投递加上消息去重

用来实现“exactly-once”的分布式快照/状态检查点方法是受到了Chandy-Lamport分布式快照算法1的启发。在这种机制中,流处理应用中的每一个算子的所有状态都会周期性地checkpointed。倘若系统发生了故障,每一个算子的所有状态都会回滚到最近的全局一致的检查点处。在回滚过程中,所有的处理都会暂停。Sources也会根据最近的检查点重置到正确到offset。整个流处理应用基本上倒回到最近的一致性状态,处理也可以从这个状态重新开始。图4展示了这种机制的基本原理。

Distributed snapshot

图4 分布式快照

在图4中,流处理应用T1时在正常地工作,同时状态也被checkpointed。T2时,算子处理一个输入数据时失败了。此时,S = 4的状态已经保存到了持久化存储当中,而S = 12的状态仍然位于算子的内存当中。为了解决这个不一致,T3时processing graph倒回到S = 4的状态并“重放”流中的每一个状态直到最新的状态,并处理每一个数据。最终结果是虽然某些数据被处理了多次,但是无论执行了多少次回滚,结果状态依然是相同的。
用来实现“exactly-once”的另一种方法是在每一个算子的基础上,将at-least-once的事件投递与事件去重相结合。使用这种方法的引擎会重放失败的事件以进一步尝试进行处理,并在每一个算子上,在事件进入到用户定义的逻辑之前删除重复的事件。这一机制需要为每一个算子维护一份事务日志(transaction log)来记录哪些事件已经处理过了。使用类似这一机制的引擎有Google的MillWheel 2Apache Kafka Streams。图5展示了这一机制的重点。
At-least-once delivery plus deduplication

图5 At-least-once结合去重

exactly-once确实是exactly-once吗?

现在,让我们来重新审视“exactly-once”究竟为用户做出了什么样的保证。“exactly-once”的标签对于描述是什么被执行了“exactly-once”是有误导性的。
有些人认为“exactly-once”描述的是对于事件处理的保证,在这种保证下流中的每一个事件都只会被处理一次。而现实情况是,没有一个流处理引擎可以保证“exactly-once”的处理。面对各式各样的故障,确保每一个算子上用户定义的逻辑只对每个事件执行一次是不可能的,因为用户代码的部分执行是一种永远存在的可能性。
考虑这样一种场景,你有一个执行map操作的流处理算子,map操作会打印输入事件的ID并不加任何改变的将事件返回。下列伪代码描述了这个算子:

Map (Event event) {
    Print "Event ID: " + event.getId()
    Return event
}

每一个事件都拥有一个GUID(Global Unique ID)。如果用户逻辑的“exactly-once”执行可以被保证的话,那么每一个事件ID都只会被打印一次。然而,这永远是得不到保证的,因为故障会发生在用户定义逻辑执行过程中的任何时间、任何地点。引擎无法靠它自己确定用户定义的处理逻辑执行到了什么地方。因此,任意的用户定义逻辑无法被保证仅被执行一次。这同时也说明了外部的操作,诸如用户定义的逻辑中实现的数据库写入,也无法被保证仅被执行一次。这些操作仍然需要通过幂等的方式来实现。
所以,当引擎声称“exactly-once”处理语义时它们究竟保证了什么?如果用户定义的逻辑无法确保仅执行一次,那么什么是什么被执行了恰好一次?当引擎声称“exactly-once”处理语义时,它们实际上表达的是它们可以确保由引擎管理的对于状态的更新只会被提交到后端的持久化存储中一次(what they're actually saying is that they can guarantee that updates to state managed by the SPE are committed only once to a durable backend store)。
上述的所有机制都使用了一个持久化的后端存储作为事实的来源,来保存每一个算子的状态并自动的提交对于状态的更新。对于机制1(分布式快照/状态检查点),它的后端存储用来保存全局一致的状态检查点(每一个算子的被检查点记录下的状态)。对于机制2(at-least-once的事件投递加上去重),持久化后端存储保存的是每一个算子的状态以及记录了每一个算子完整处理过的所有事件的事务日志。
向作为事实来源的持久化后端提交状态或应用更新可以被描述为只会发生恰好一次。然而,计算状态的更新/改变,例如在事件上执行任意的用户定义逻辑,在故障发生时是有可能执行多次的。换而言之,对于事件的处理可能会发生多次,但是那些处理的影响只会反映到持久化后端状态存储中一次。在Streamlio,我们已经决定effectively-once是对于这种语义的最好的描述。

分布式快照 vs at-least-once事件传递加上去重

从语义的角度,分布式快照与at-least-once事件传递加去重机制提供了相同的保证。然而由于两种机制在实现上的差异,它们之间仍然存在着明显的性能差异。
流处理引擎上的机制1(分布式快照/状态检查点)的性能开销可以是最小的,因为引擎本质上只是通过流处理应用的所有算子发送一部分特殊事件与常规事件,而状态检查点可以在后台异步执行。然而,对于大型的流处理应用,故障会发生得更频繁,导致引擎需要暂停应用并回滚所有算子的状态,而这反过来又会影响性能。流处理应用规模越大,发生故障的可能性越高、越频繁,而反过来对于应用性能的影响也越明显。然而,同样的,这一机制是非常非入侵性的(non-intrusive),而且只需要最少的额外资源来运行。
机制2(at-least-once事件投递加上去重)可能会需要更多的资源,尤其是存储。在这一机制中,引擎需要记录被一个算子的每一个实例完整处理过的每一个元组,以便执行去重操作,同时也是为了每一个事件本身执行去重操作。这会导致大量的数据需要记录,尤其是在流处理应用规模很大或者有多个应用运行的情况下。而与每一个算子上的每一个事件相关的去重操作同样存在着性能开销。然而,在这一机制中,流处理应用的性能不太可能受到应用规模大小的影响。在机制1中,如果任一算子上发生了任何故障,全局的暂停以及状态回滚都需要执行;而在机制2中,一次故障的影响则更为局部化。当一个算子上发生了故障,可能没有被完全处理的事件仅会从上游数据源开始重放/重传。这一性能影响独立于故障发生在应用的哪里,同样对这一应用中的其它算子几乎不会造成什么性能上的影响。从性能角度来看,两种机制的优势和劣势如下表所示:

分布式快照/状态检查点

优势 劣势
更少的性能与资源开销 从故障中恢复时对于性能的影响更大
nothing 拓扑变大时对于性能有潜在影响

At-least-once投递加上去重

优势 劣势
故障的性能影响是局部性的 可能需要大量存储与基础设施来支持
故障的影响不会随着拓扑规模的增长而变大 每一个算子的每一个事件都存在性能开销

虽然从理论的角度来看,两种机制间存在着一些差异,但是它们都可以归结为at-least-once的处理加上幂等。对于所有的机制,当故障发生时事件都会被重放/重传(实现at-least-once),然后通过状态回滚或者事件去重,当内部管理状态更新时,算子本质上会变为幂等。

总结

在这篇博客中,我希望让你相信“exactly-once”这一名词是非常有误导性的。提供“exactly-once”处理语义事实上意味着由流处理引擎管理的对于一个算子状态的独立更新只会被反映一次。“Exactly-once”并不会保证对于事件的处理、尤其是任意用户定义逻辑的执行,只会发生一次。在Streamlio,对于这种语义我们更喜欢effective once这一名词,因为不一定保证处理只发生一次,但是对于引擎管理的状态的影响只会反映一次。两种受欢迎的机制,分布式快照与消息去重,被用来实现exactly/effectively-once处理语义。两种机制对于消息处理与状态更新提供了相同的语义保证,但是在性能上有所差异。这篇文章并不是要说服你一种优于另一种,因为每一种机制都有其自己的优势与劣势。

参考文献

  1. Chandy, K. Mani and Leslie Lamport. Distributed snapshots: Determining global states of distributed systems. ACM Transactions on Computer Systems (TOCS) 3.1 (1985): 63-75.
  2. Akidau, Tyler, et al. MillWheel: Fault-tolerant stream processing at internet scale. Proceedings of the VLDB Endowment 6.11 (2013): 1033-1044.

猜你喜欢

转载自www.cnblogs.com/wennn/p/9932840.html
今日推荐