微服务篇:具有确定性的流处理

事件驱动型微服务通常拥有比前几章介绍的那些拓扑更为复杂的拓扑结构。在从多个事件流中消费和处理事件的同时,还需要有状态的处理(第 7 章会介绍)来解决许多业务问题。微服务也会受到跟非微服务系统同样的故障和崩溃的影响。可能一些微服务正以近实时的方式处理事件,而其他新启动的微服务正在处理历史数据以赶上最新的事件,这种情况并不少见。

本章会解决以下 3 个主要问题。

  • 当从多个分区消费事件时,微服务如何选择事件处理的顺序?
  • 微服务如何处理乱序及迟到事件?
  • 对于以近实时的方式处理流和从流的开始位置进行处理这两种情况,如何确保微服务产生确定性结果?

以上问题可以通过检查时间戳、事件调度、水位和流时间以及它们针对确定性处理发挥的作用来回答。业务逻辑中出现缺陷、错误和变更时也需要对事件进行重新处理,这使得确定性结果很重要。本章还探讨了乱序和迟到事件是如何发生的、处理它们的策略以及如何减轻它们对工作流的影响。

 尽管我尽了最大努力寻找一种简洁的方式来解释一些关键概念,但本章包含的信息仍然相当多。在很多地方我会推荐你查阅更多的参考资料,让你自己探索,因为其细节已经超出了本书的范围。

事件驱动工作流的确定性

事件驱动型微服务有两种主要的处理状态。它可能正以近实时的方式处理事件,这通常是已经长期运行的微服务。它也可能正在处理历史事件以赶上当前事件,这通常见于在扩容的和新的服务。

如果将输入事件流的消费者组偏移量重置到最开始的时间并让微服务重新运行,它是否能产生跟第一次运行时相同的输出?确定性处理的首要目标就是让微服务无论是以实时方式处理事件,还是正在追赶当前事件,都产生相同的输出。

注意,有些工作流是明显不具备确定性的,比如那些基于当前挂钟时间(wall-clock time)和要查询外部服务的工作流。外部服务可以根据被请求的时间提供不同的结果,特别是当它们的内部状态独立于发起查询的服务而更新时。在这些情况下是没有确定性保证的,所以一定要注意工作流中的任何非确定性操作。

完全确定性的处理是理想的情况,在这种情况下所有事件都无延迟地及时到达,没有生产者和消费者发生故障,也没有间歇性的网络问题。但现实是微服务只能尽最大努力获得确定性,因为我们别无选择,只能处理这些场景。许多组件和进程会一起工作以达到这个目标,但在大多数情况下,尽最大努力获得的确定性足以满足需求。你需要具备这些条件:一致的时间戳、精心挑选的事件键、分区分配、事件调度以及处理迟到事件的策略。

时间戳

事件可以在任何时间、任何地点发生,并且需要与来自其他生产者的事件进行协作。如果要跨分布式系统比较事件,经过同步的一致的时间戳是一个硬性要求。

保存在事件流中的事件既有偏移量又有时间戳。消费者使用偏移量来确定已经读取过哪些事件,而时间戳表明事件是何时被创建的,用来确定一个事件相对于其他事件的发生时间,以及确保能以正确的顺序处理事件。

下面这些时间语义的概念如图 6-1 所示。该图展示了在事件驱动工作流中这些概念所处的时间位置。

事件时间

  事件发生时由生产者赋值给事件的本地时间戳。

代理摄取时间

  由事件代理赋值给事件的时间戳。在代理中可以将该语义配置为事件记录中的事件时间或实际的摄取时间,前者更为常见。在生产者的事件时间不可靠的场景中,代理摄取时间足以替代它。

消费者摄取时间

  消费者获取事件的时间。可以将其设为事件代理记录中指定的事件时间的值,也可以是挂钟时间。

处理时间

  消费者处理完事件的挂钟时间。

你会发现,可以通过事件代理将事件时间传播给消费者,从而使消费者逻辑能够根据事件发生的时间做出决策。这有助于回答本章开头提到的 3 个问题。现在已经给出了时间戳的类型,下面来看看它们是如何产生的。

同步分布式时间戳

一个基本的物理限制是,无法保证两个独立的系统有完全相同的系统时钟时间。许多物理属性限制了系统时钟的精确性,比如底层时钟电路的材料差异、芯片工作温度的变化以及同步期间不一致的网络通信时延。然而,建立几乎同步的本地系统时钟是可能的,并且最终对大多数计算目的来说足够好。

一致的时钟时间主要通过与“网络时间协议”(network time protocol,NTP)服务器保持同步来获得。亚马逊和谷歌等云服务提供商在它们的不同地区提供了额外的卫星时钟和原子钟,以便即时同步时间。

在局域网内与 NTP 服务器的同步可以提供非常精确的本地系统时钟,在 15分钟之后只会有几毫秒的偏移。根据 NTP 发明者 David Mills 的说法,在最佳情况下,更频繁地同步可以将偏移减少到 1 毫秒或更少,尽管在实践中间歇性的网络问题可能会阻止该目标的达成。跨开放互联网的同步可以导致更大的偏移,精度会降低到正负 100 毫秒的范围。如果想重新同步全球不同地区的事件,这是一个需要考虑的因素。

NTP 同步也容易出现故障,因为网络中断、配置错误和暂时性问题会阻止实例同步。NTP 服务器本身也会变得不可靠或无响应。实例中的时钟可能会受到多租户问题的影响,比如共享底层硬件的基于虚拟机的系统。

对绝大多数业务情况来说,频繁与 NTP 服务器同步可以为系统事件时间提供足够的一致性。NTP 服务器和 GPS 使用的改进已经开始将 NTP 同步精度持续提升到亚毫秒级。用时间戳赋值的创建时间和摄取时间可以高度一致,尽管少部分乱序问题仍然会发生。本章后面将介绍迟到事件的处理。

处理带时间戳的事件

时间戳提供了一种以一致的时间顺序处理分布于多个事件流和分区中的事件的方式。许多情况要求基于时间来保持事件之间的顺序,并且无论何时处理事件流,都需要一致的、可重复产生的结果。使用偏移量作为比较方法仅适用于单个事件流分区内的事件,而事件通常需要从多个不同的事件流中进行处理。

示例:在处理多个分区时选择事件的顺序

银行必须确保以正确的时间顺序处理存款和取款事件流。它对存款和取款进行有状态的实时统计,当客户的账户余额低于 0 元时,就会对其进行透支罚息。在这个例子中,银行将存款事件放在一个事件流中,将取款事件放在另一个事件流中,如图 6-2 所示。

用原始方法来消费和处理事件的话,可能是一个循环处理程序,首先处理 10元存款事件,然后处理 25 元取款事件(会产生负值和透支罚息),再处理20 元存款事件。这是不正确的,它没有体现事件发生的时间顺序。这个例子很清晰地表明,在消费和处理事件时,必须考虑事件的时间戳。下一节将更详细地讨论这一点。

事件调度和确定性处理

确定性处理要求一致地处理事件,这样在将来的某个时间便可以复现结果。事件调度就是当从多个输入分区消费事件时,选择要处理的下一个事件的过程。对于基于日志且不可变的事件流,事件的消费是以基于偏移量的顺序进行的。但是,如图 6-2 所示,事件必须基于记录所提供的事件时间进行交错处理以确保正确的结果,无论这个事件来自哪个输入分区。

 最常见的事件调度实现从所有分配的输入分区中选择具有最早时间戳的事件并将其分派到下游处理拓扑中。

事件调度是许多流处理框架的一个特性,但在基础的消费者实现中通常不存在。你需要确定自己的微服务是否需要实现它。

 如果消费和处理事件的顺序会影响业务逻辑,那么微服务将需要事件调度。

 自定义事件调度器

一些流处理框架允许实现自定义的事件调度器。例如,Apache Samza 让你可以实现 MessageChooser 类。在这个类中,可以基于很多因素来选择处理哪个事件,包括事件流之间的优先级、挂钟时间、事件时间、事件元数据以及事件本身的内容。但是,当你实现自己的事件调度器时应该小心,因为许多自定义调度器实际上具有不确定性,如果需要重新处理事件,则无法产生可复现的结果。

 基于事件时间、处理时间和摄取时间进行处理

如图 6-1 所示,基于时间的事件处理顺序要求你选择用哪个时间点作为事件的时间戳。是选择本地赋值的事件时间,还是选择代理摄取时间?这两种时间戳在生产–消费工作流中都只出现一次,挂钟时间和消费者摄取时间则根据应用程序执行时间的不同而变化。

在大部分场景中,特别是当所有消费者和生产者都运行良好且没有一个消费者组有事件积压时,上述 4 个时间点彼此相差不过几秒。相反,对处理历史事件的微服务来说,事件时间和消费者摄取时间会有很大的不同。

为了更准确地描述现实世界中的事件,最好使用本地赋值的事件时间,前提是它的准确性值得依赖。如果生产者有不可靠的时间戳(并且无法修复),那么次优选择是基于事件被摄取到事件代理中的时间来设置时间戳。只有在事件代理和生产者无法通信的少数情况下,真实事件时间和代理赋值的时间之间才可能有很大的延迟。

消费者提取时间戳

消费者如果想确定如何对处理的事件排序,就必须知道记录的时间戳。在消费者摄取阶段,要用一个时间戳提取器从被消费的事件中提取时间戳。这个提取器可以从事件有效载荷的任何部分获取信息,包括键、值和元数据。

提取器会给每个被消费的记录设置一个时间戳,用于表示其“事件时间”。一旦设置了这个时间戳,消费者框架就会在处理期间使用它。

对外部系统的“请求–响应”调用

在事件驱动拓扑中,对外部系统发起的所有非事件驱动请求都可能导致非确定性结果。根据定义,外部系统是在微服务外部进行管理的,这意味着在任意时间点上其内部状态及对请求做出的响应可能是不同的。这是否重要完全取决于微服务的业务需求,并由你来评估。

水位

水位(watermark)用于在处理拓扑中跟踪事件时间的进度,并且表明给定事件时间(或早于该时间)的所有数据都已得到处理。这是在许多主流流处理框架(比如 Apache Spark、Apache Flink、Apache Samza 和 ApacheBeam)中常用的技术。来自谷歌的一篇文章(“The Dataflow Model: APractical Approach to Balancing Correctness, Latency, and Cost inMassive-Scale, Unbounded, Out-of-Order Data Processing”)详细描述了水位的知识,并给所有想深入学习的人提供了很好的起点。

水位是对处于相同处理拓扑中的下游节点的一种声明,它声明处于或早于时间 t 的所有事件都已得到处理。接收到水位的节点可以更新自己的内部事件时间,并向其下游依赖拓扑节点传播自己的水位。这个过程如图 6-3 所示。

在图 6-3 中,消费者节点有最高水位时间,因为它是从源事件流中消费的。新的水位会周期性地产生,比如经过了一段挂钟时间或事件时间之后,抑或已经处理了最小数量的事件之后。这些水位会向拓扑内的下游其他处理节点传播,这些节点会据此更新自己的事件时间。

并行处理中的水位

水位对于在多个消费者实例之间协调事件时间特别有用。图 6-4 展示了一个

包含两个消费者实例的简单处理拓扑。每个消费者实例都从分配给自己的分区中消费事件,然后应用 groupByKey 函数,进而由 aggregate 函数处理。这需要一次洗牌(shuffle)操作,所有具有相同键的事件都被发送到同一个下游 aggregate 实例。在此情况下,来自实例 0 和实例 1 的事件会基于键相互发送给对方,以确保具有相同键的所有事件都处在相同的分区内。

最源头的函数创建了水位,这个函数从事件流分区中消费事件。水位定义了消费者的事件时间,并随着消费者节点事件时间的递增而向下游传播(参见图 6-4 的 1) 处)。

当水位到达时,下游节点更新它们的事件时间,然后创建自己的新水位并向下游传播给后续节点。有多个输入的节点,比如 aggregate,从多个上游输入中消费事件和水位。节点的事件时间是其所有输入源事件时间的最小值,节点会在内部保持对这些时间的跟踪(参见图 6-4 的 2) 处)。

在这个例子中,一旦来自 groupByKey-1 节点的水位到达,两个aggregate 节点都会将它们的事件时间从 13 更新到 15(参见图 6-4 的3) 处)。注意,水位并不会影响节点的事件调度,它只是告知节点,所有时间戳早于水位的事件都应该被认为是迟到的事件。本章的后面部分会介绍处理迟到事件的内容。

Spark、Flink 和 Beam 等重量级处理框架需要一个专用的处理资源集群来大规模地执行流处理。这一点特别重要,因为这个集群还提供了跨任务通信和集中协调每个处理任务的方法。对事件的再分区,比如这个例子中的groupByKey+aggregate 操作,在事件代理中使用集群内部通信而不是事件流的方式实现。

流时间

在流处理程序中维护时间的第二个选项,就是所谓的流时间,它是 ApacheKafka 流所推崇的方法。从一个或多个事件流中读取数据的消费者应用程序维护着其拓扑的流时间,它是所有已被处理事件的最大时间戳。消费者实例从每个分配给它的事件流分区中消费并缓存事件,应用事件调度算法选择下一个要处理的事件,然后如果要处理的事件时间戳大于之前的流时间,则更新流时间。流时间永远不会递减。

图 6-5 展示了一个流时间的例子。消费者节点基于其接收到的最大事件时间值维护着一个单一的流时间。这个流时间目前设为 20,因为这是最近一次处理事件的事件时间。下一个要处理的事件是两个输入缓存中的最小值——在这个例子中是事件时间为 30 的事件。事件向下分发给处理拓扑,然后流时间会被更新为 30。

拓扑在处理每个事件的过程中会维护对应的流时间,直到开始处理下一个事件。在拓扑包含再分区流的情况下,每个拓扑会分裂为两个子拓扑,而每个子拓扑会维护自己的各不相同的流时间。事件是以深度优先的方式进行处理的,这样在任何给定的时间内,一个子拓扑里只有一个事件得以处理。这不同于基于水位的方法。使用水位方法的话,每个处理节点的输入会缓存事件,每个节点的事件时间是独立更新的。

并行处理中的流时间

再次考虑图 6-4 中两个消费者实例的例子,但这次使用 Kafka Streams 所支持的流时间方法(参见图 6-6)。一个显著的区别是,Kafka Streams 的方法使用所谓的内部事件流将再分区的事件发送回事件代理。然后实例重新消费这个流,在新的流里,所有再分区的数据根据键被重新分配到对应的分区。这在本质上与重量级集群的洗牌机制是一样的,但不需要有专门的集群(注意:Kafka Streams 非常支持微服务)。

在这个例子中,根据各自的键,对来自输入流的事件进行再分区,事件被写入再分区事件流。键为 A 和 B 的事件最终进入 P1,而键为 X 和 Z 的事件最终进入 P0。请注意,每个事件已维护的事件时间不会被当前挂钟时间所覆盖。回想一下,再分区只是被当作对已有事件数据的逻辑洗牌。如果重写事件的事件时间,则会完全破坏原来的时间顺序。

注意图中所示的子拓扑。由于再分区事件流的出现,处理拓扑实际上被切成了两半,这意味着每个子拓扑的工作可以并行地进行。子拓扑 1 和子拓扑 3从再分区流中消费事件并将它们聚合到一起,而子拓扑 0 和子拓扑 2 生产需要进行再分区的事件。每个子拓扑维护着自己的流时间,因为它们从独立的事件流中消费事件。

 水位策略也可以用于再分区事件流。Apache Samza 提供了一个类似于 Kafka Streams 的独立模式,不过是使用水位而不是流时间。

乱序事件和迟到事件

在理想世界中,所有事件的生产都不存在问题,且无延时地提供给消费者。遗憾的是,我们都生活在现实世界中,理想情况永远不会存在,所以必须有应对乱序事件的计划。如果一个事件的时间戳不等于或不大于事件流中位于它前面的事件的时间戳,就被称为是乱序的。在图 6-7 中,事件 F 就是乱序的,因为它的时间戳小于 G 的时间戳;事件 H 也是如此,它的时间戳小于 I 的时间戳。

有边界的数据集,比如进行批处理的历史数据,通常对乱序数据有相当强的弹性。可以把整个批处理想象成一个大的窗口,如果批处理还没有开始,那么以数分钟甚至数小时偏差乱序到达的事件就不会有真正的影响。在这种方式下,批处理的有界数据集可以产生高确定性的结果,但这是以高延迟为代价的,特别是对传统的大数据批处理作业来说,这些作业的结果在 24 小时后才能使用(加上批处理时间)。

对于无边界的数据集,比如那些持续更新的事件流,开发人员在设计微服务时必须考虑延迟和确定性的要求。这就从技术需求扩展到了业务需求的范畴,所以任何事件驱动型微服务开发人员都必须问一句:“我的微服务是否要根据业务需求处理乱序和迟到的事件?”乱序事件要求业务就如何处理它们做出具体决定,并且确定延迟和确定性的优先级。

考虑一下之前银行账户的例子。无论事件的顺序如何排列,也无论事件迟到多久,先存款紧接着立即取款的情况必须能按正确的顺序得到处理,以免错误地收取欠款费用。为了缓解这种情况,应用程序逻辑可能需要维护状态,以便在业务指定的时间段(比如一小时的宽限时间窗)内处理乱序数据。

 对单个分区中的事件来说,无论其时间戳是多少,总是根据它们的偏移量顺序来进行处理。这就会导致乱序事件。

只有从消费者微服务的视角来看,一个事件才能被认为是迟到的。一个微服务可能认为所有乱序事件都是迟到的,而另一个微服务可能很宽容,需要经过几小时的挂钟时间或事件时间之后才认为事件是“迟到”的。

使用水位和流时间的迟到事件

假设有两个事件,一个事件的时间是 t(将该事件称为事件 t),另一个事件的时间是 t'(将该事件称为事件 t')。事件 t' 有比事件 t 更早的时间戳。

水位

  事件 t' 在水位 W(t) 之后到达会被认为是迟到的。如何处理此事件取决于具体的节点。

流时间

  如果事件 t' 在流时间之后到达,而流时间已经递增超过了 t',则 t'会被认为是迟到的。如何处理此事件取决于拓扑中的每个操作。

 一个事件只有在晚于某个消费者指定的截止时间到达时才是迟到的。

乱序事件的原因和影响

乱序事件有几种发生方式。

01. 来源于乱序数据

当然,最明显的是当事件来源于乱序数据时。这种情况会发生在从已经乱序的流中消费数据时或当事件正来自存在乱序时间戳的外部系统时。

02. 多个生产者写到多个分区

多个生产者向多个输出分区写数据会导致乱序事件。对一个现有的事件流再分区是乱序事件发生的一种方式。图 6-8 展示了两个消费者实例对两个分区进行再分区的情况。在这个场景中,源事件表明用户与哪个产品发生了交互。例如,Harry 已经跟 ID 为 12 和 ID 为 77 的产品发生了交互。假设有一位数据分析师需要重新设置这些数据的键为用户ID,这样他们就可以执行基于会话的用户参与度分析。最终的结果输出流可能会有一些乱序事件。

注意,每个实例维护着自己的内部流时间,并且两个实例之间没有同步机制。这可能导致时间偏差,产生乱序事件,如图 6-9 所示。

在流时间上,实例 0 只是稍微领先实例 1,但由于它们是相互独立的事件流,时间 t=90 和 t=95 的事件在再分区事件流中被认为是乱序的。不均衡的分区大小、不同的处理速率和大量的事件积压加剧了这个问题。这里的影响是,之前有序的事件数据现在变得乱序了,这样作为消费者,你就无法依赖在原有事件流中持续递增的时间了。

 一个单线程生产者在正常操作下不会产生乱序事件,除非它的数据来源于乱序数据源。

由于一旦检测到有较大时间戳的事件,流时间就会增加,因此最终可能会出现大量事件因重新排序被认为是迟到的情况。这种情况对处理产生的影响,取决于消费者选择如何处理乱序事件。

时间敏感的函数和窗口化

迟到事件是基于时间的业务逻辑的主要关注点,比如聚合特定时间段内的事件或在特定时间段后触发事件等。迟到事件是指在业务逻辑已经针对特定时间段完成了事件处理之后到达的事件。窗口函数是基于时间的业务逻辑的一个很好的例子。

窗口化意味着根据时间将事件分组到一起。这对于有相同键的事件特别有用,通过相同键的事件可以看到在那一时间段内发生了什么。有 3 种主要的事件窗口类型。同样,请务必检查流处理框架,以获得更多信息。

 事件时间或处理时间都可用于完成窗口化,但应用程序通常更多地使用事件时间。

01. 滚动窗口

滚动窗口是一种固定大小的窗口。前面和后面的窗口不会发生重叠。图6-10 展示了 3 个滚动窗口,每个窗口在 t、t+1 等位置对齐。这种类型的窗口化可以帮忙回答“产品的使用高峰期是什么时候”这样的问题。

02. 滑动窗口

滑动窗口具有固定的窗口大小和递增的步长(称为窗口滑动量)。它只反映当前窗口内事件的聚合。滑动窗口可以帮忙回答“在过去的一段时间内有多少用户点击了我的产品”这样的问题。图 6-11 展示了滑动窗口的例子,包括窗口大小以及它向前滑动的量。

03. 会话窗口

会话窗口是一种大小动态变化的窗口。窗口的终止是由超时决定的,而超时的发生是由于会话不活动了,发生在超时之后的任何激活动作都将启动一个新会话。图 6-12 展示了会话窗口的例子,由于用户 C 的不活动而产生了一个会话缺口。这类窗口可以帮忙回答“用户在给定的浏览器会话中会看到什么”这样的问题。

这里的每个窗口函数都必须处理乱序事件。你必须决定要等待任意一个乱序事件多长时间,才能认为其太迟而不再考虑。流处理的一个基本问题就是你永远无法确定是否已接收到了所有事件。等待乱序事件是可以的,但最终你的服务还是需要有放弃动作,因为它不能无限期地等待。其他要考虑的因素包括存储多少状态、迟到事件的可能性以及丢弃迟到事件对业务的影响。

处理迟到事件

在开发工程解决方案之前,应在业务层面确定处理乱序和迟到事件的策略,因为策略会依据数据重要程度的不同而变化。像金融事务和系统故障这样的关键事件,无论它们在流中的位置如何,都需要得到处理。相反,一些测量类型的迟到事件(比如测量的温度或力量值),可以因为不再相关而直接丢弃。

业务需求还决定了多久的延迟是可接受的,因为等待事件到达可能会增加确定性,但代价是更高的延迟。这会对时间敏感型应用程序或具有严格 SLA 的应用程序的特性产生负面影响。值得庆幸的是,微服务提供了必要的灵活性,可以根据每个服务定制确定性、延迟和乱序事件处理特性。无论框架是使用水位还是流事件,都有以下几种处理迟到事件的方式。

丢弃事件

  简单地丢弃事件。窗口一旦关闭,所有基于时间的聚合就已完成。

等待

  延迟窗口结果的输出,直到经过了固定的时间之后才进行输出。这是以较高的延迟带来较高的确定性。旧窗口需要保持可更新状态,直到预先确定的时间过去。

宽限期

  一旦窗口被认为完成就输出窗口结果。然后,窗口在预定的宽限期内可用。一旦一个迟到事件到达该窗口,就会更新聚合并输出更新的结果。这跟等待策略类似,不同的是一旦迟到事件到达就会进行更新。

无论微服务等待多久,太迟的事件最终都会被丢弃。对于微服务应该如何处理迟到事件,没有现成的技术规则,只要确保业务需求得到充分满足即可。

如果微服务的业务需求中没有指定处理迟到事件的协议,那么业务必须努力解决这个问题。

以下几个问题可以帮你确定处理迟到事件的良好准则。

  • 迟到事件发生的可能性有多大?
  • 你的服务需要多长时间来防止迟到事件?
  • 丢弃迟到事件的业务影响是什么?
  • 等待很长时间来捕获迟到事件对业务的好处是什么?
  • 需要多少磁盘和内存来维护状态?
  • 等待迟到事件所产生的成本是否大于收益?

再处理与近实时处理

不可变事件流提供了重置消费者组偏移量和从任意时间点重新处理事件的能力。这被称为再处理,每个事件驱动型微服务都需要在设计时考虑再处理问题。通常只有使用事件时间进行事件处理的微服务要执行再处理,那些依赖于挂钟时间的聚合和窗口化的微服务则不用。

事件调度是能够正确再处理来自事件流的历史数据的重要部分。它确保了微服务以它们在近实时处理事件时一样的顺序来处理事件。处理乱序事件也是这个过程中的重要部分,因为通过事件代理对事件流再分区(而不是使用像Spark、Flink 或 Beam 这样的重量级框架)会导致乱序事件。

当你想要对事件流进行再处理时,下面是一些可遵循的步骤。

01. 确定起始点。作为最佳实践,所有有状态的消费者应该从它们订阅的每个事件流的起点开始再处理事件。这尤其适用于实体事件流,因为它们包含有关所讨论实体的重要事实。

02. 确定重置哪些消费者偏移量。所有包含用于有状态处理的流的偏移量都应该被重置到流的起点,因为如果你从一个错误的位置开始再处理,那么很难确保最终能得到正确的状态。(考虑一下,如果你重新处理某人的银行存款余额,而不小心漏掉了以前的工资支票,会发生什么情况。)

03. 考虑数据量。有些微服务可能要处理大量事件。考虑再处理事件需要花费多长时间,以及任何可能存在的瓶颈。可能需要采用限额(参见 14.4节)以确保不会因大量 I/O 而压垮事件代理。此外,如果预计会产生大量再处理输出数据,则需要通知所有的下游消费者。如果不具备自动伸缩能力(参见 11.7.3 节),则需要据此扩容服务。

04. 考虑再处理的时间。再处理可能需要花费很多时间,所以要计算需要多少中断时间。当你的服务进行再处理时,确保下游消费者也能够应对可能出现的过时数据。增加消费者实例的数量以最大化并行度可以有效减少中断时间,一旦完成再处理,可以再减少实例数。

05. 考虑影响。当进行再处理时,有些微服务可能会执行你不想发生的动作。例如,一个服务会在用户的包裹已发货时向用户发送电子邮件,在再处理事件时不应再向用户发送电子邮件,因为这将是一种糟糕的用户体验,从业务角度来看完全没有意义。请仔细考虑再处理对系统业务逻辑的影响,以及可能给下游消费者造成的潜在问题。

生产者/事件代理的连接性问题

在这个场景中,记录是以时间戳顺序创建的,但要到一个比较迟的时间才能发布(参见图 6-13)。正常操作下,一旦事件发生,生产者就会进行发送,同时消费者近乎实时地消费它们。我们很难发现这种问题场景发生的时间,甚至在回顾时也可能会忽略。

假设一个生产者有大量记录准备发送,但无法连接事件代理。这些记录被打上的时间戳是事件发生时的本地时间。生产者会重试多次,最终要么成功发送,要么放弃并发生失败(理想情况下会出现一个失败告警,这样就可以确定失败的连接并进行修复)。这个场景如图 6-14 所示。来自流 A 的事件仍然在被消费,水位 / 流时间在相应地递增。但是,当消费流 B 时,消费者发现没有新事件,所以它只好假设没有可用的数据。

最终生产者将能够向事件流写入记录。这些事件会以它们正确的实际发生的事件时间顺序发布,但是由于挂钟延迟,近实时消费者会将它们标记为迟到并按迟到事件对待。

消除此问题的一种方法是在处理事件之前等待一段预先确定的时间,不过这种方法会导致延迟成本并且只有在生产延迟短于等待时间时才有效。另一种方法是在代码中使用稳健的迟到事件处理逻辑,这样你的业务逻辑就不会被此场景影响了。

小结与延展阅读

本章首先介绍了确定性以及在无边界流中达到确定性的最佳方法。然后研究了如何在多个分区之间选择要处理的下一个事件,以确保在近实时处理和再处理时达到最大努力的确定性。无边界事件流加上间歇性故障的本质意味着永远无法实现完全确定性。合理的、尽力而为的、大部分时间能正常工作的解决方案提供了延迟和正确性之间的最佳折中。

乱序和迟到事件是设计中必须考虑的因素。本章探讨了如何使用水位和流时间来识别并处理这些事件。如果想了解更多关于水位的信息,可以看看Tyler Akidau 的优秀文章“Streaming 101: The world beyond batch”和“Streaming 102: The world beyond batch”。更多关于分布式系统时间的考虑和见解,可以在 Mikito Takada 的在线图书 Distributed Systems forFun and Profit 中找到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值