轻松实现 Saga 模式

轻松实现 Saga 模式

翻译自 Making the Saga Pattern Work Without All the Headaches

Saga 模式是持久微服务执行的绝佳工具,但它会使维护变得困难。这是使其适用于您的系统的方法。

Featued image for: Making the Saga Pattern Work Without All the Headaches

第 1 部分:Sagas 的问题

我们都曾经历过在项目中发现软件过程比想象中复杂的情况,处理这种过程复杂性通常很痛苦,但也未必。

一个具有里程碑意义的软件开发手册,称为 Saga 设计模式,帮助我们应对处理过程复杂性已经超过30年。它为数千家公司提供了服务,帮助他们构建更复杂的软件以服务于更苛刻的业务流程。

这种模式的缺点是成本更高,且更为复杂。

在本文中,我们将首先分解传统的编码 Saga 模式来处理事务复杂性,并看看为什么它不起作用。接着,我将更深入地解释开发团队如果不关注这个管道代码问题会发生什么。最后,我将向您展示如何避免随之而来的项目失败。

满足持久执行的需要

Saga 模式应运而生,以应对复杂软件流程中的一个紧迫需求:持久执行。当你编写的事务仅需要进行单个、简单的数据库调用并获得快速响应时,你不需要在代码中考虑除该事务外的任何事情。然而,当事务依赖于多个数据库或其他事务执行来完成任务时,情况会变得更加困难。

例如,一个预订汽车出行的应用程序可能需要先检查客户账户是否正常,然后检查其位置,再查看该地区有哪些车可用。接下来需要预订出行、通知司机和客户,然后在完成行程时收取客户的付款,并将所有信息写入中央存储,更新司机和客户的账户历史记录。

这样处理依赖事务的流程需要在整个事件序列中跟踪数据和状态。它们必须能够在事务流程中出现问题时生存下来。如果一个事务需要比预期更长的时间才能返回结果(例如,移动连接出现问题或数据库达到峰值负载并需要更长时间来响应),软件必须能够适应这种情况。

它必须等待所需事务完成,不断重试直到成功,并协调执行队列中的其他事务。如果一个事务在完成之前崩溃,该过程必须能够回滚到一致的状态,以保持整个应用程序的完整性。

在需要在几秒钟内得到响应的用例中,这已经很困难了。有些应用程序可能需要在几个小时或几天内执行,这取决于事务的性质和它们支持的过程。对于开发人员来说,挑战是在执行期间维护过程的状态。

了解 Saga 模式

Saga 模式为这一旅程提供了一份路线图。该模式最早在 1987 年的一篇论文中讨论,通过使复杂过程能够彼此通信,将持久执行引入其中。中央控制器管理该服务通信和事务状态。

该模式为开发人员提供了持久执行所需的三个要素。它可以串联事务以支持长时间运行的过程,并通过失败重试来保证其执行。它还通过确保要么整个过程完成,要么根本不完成来提供一致性。

然而,使用 Saga 模式需要付出巨大的代价。原则上概念并没有问题,但一切取决于实现。开发人员传统上必须将该模式作为应用程序的一部分自己编写代码。这使得其设计、部署和维护变得非常困难,以至于应用程序可能会成为该模式的奴隶,最终占用开发人员大部分时间。

最终,随着添加更多的事务,开发人员将花费更多的时间来维护管道代码。最初的线性开发工作负载现在变成了指数级别。随着每一次新变化,开发所需的时间将不成比例地增加。

手动编写 Saga 模式涉及将一个连贯的过程分解成块,然后用代码包装它们以管理其操作,包括在失败时重试它们。开发人员还必须管理这些任务的调度和协调,跨越依赖于彼此的不同进程。他们必须在数据库、队列和计时器之间权衡,以管理这种进程间通信。

增加软件进程和依赖关系的数量需要更多的开发人员时间来创建和维护管道基础设施,这反过来又推高了应用成本。这种日益复杂化也使得开发人员更难证明其代码的可靠性和安全性,这对运营和合规性都有影响。

抽象是关键

抽象化是保留 Saga 模式持久执行优势并摆脱其负面影响的关键。我们必须将事务排序从应用程序中抽象出来,以隐藏它们,并将其抽象到另一个级别。

抽象化是计算机中的一个众所周知的过程。它让每个应用程序都拥有所有权的假象,消除了开发人员需要适应它的需要。虚拟化系统通过 hypervisor 来实现。 TCP 堆栈通过自动重试网络连接来实现,以使开发人员不必编写自己的握手代码。关系数据库在回滚失败的事务时也会进行这种操作,以保持数据的一致性。

使用单独的平台来管理持久执行可以将这些好处带给事务排序,Temporal 将其称为工作流。开发人员仍然控制工作流,但他们不必关心底层机制。

将持久执行抽象为工作流除了实现的简便性之外还有几个好处。经过验证的工作流管理层使复杂的事务序列比自制的临时抽屉代码更不容易出错。每个项目消除数千行自定义代码还使剩余代码更易于维护并减少技术债务。

开发人员在调试时最清楚地看到这些好处。当您必须模拟和管理管道代码时,根本原因分析和补救变得指数级难度。工作流隐藏了整个潜在问题层。

高效的开发人员是快乐的开发人员

基于工作流的持久执行提高了开发者的体验。开发者不再沉迷于事务管理的迷宫中,而是可以专注于真正重要的工作。这提高了士气,有助于留住他们。根据预计,在 2021 年至 2031 年间,美国软件工程师的职位空缺数量将增长 25 %,人才竞争十分激烈,公司无法承受太多的人员流失。

公司在使用 Saga 模式处理软件进程中的上下文切换方面已经朝着正确的方向迈出了一步。但是,通过将这些 Saga 模式抽象为一个独立的服务而不是应用层,公司可以走得更远。做好这件事可以将组织的软件成熟度提升几年。

第 2 部分:避免临界点

在本文的前半部分,我谈到了在应用层协调事务和保留状态是多么繁重。现在,我们将讨论它是如何使软件项目偏离轨道的以及你可以采取什么措施解决这个问题。

任何一个规模合理的软件工程项目都会遇到需要持久执行的需求。

理想情况下,创建新软件功能所需的成本和时间应该是一致和可计算的。编写持久性代码打破了这种一致性。它使开发工作所需的时间和精力看起来更像是一个曲线而不是线性的斜坡。

转折点是当编写新功能所花费的时间和精力开始急剧增加的时候。这时候才真正意识到管理长期事务的复杂程度。我将描述它是什么,为什么会发生以及草率编写管道代码不是解决这个问题的正确方法。

什么触发了临界点

在临界点到来之前,开发者的经验通常都是线性的。应用程序框架可以支持开发者添加新特性,没有什么不可预见的问题。这使得开发团队能够按预期的实现时间扩展应用程序的功能。

只要开发者进行定量的变更,即添加更多类似的东西,这种线性扩展就能够正常工作。但当有人需要进行与众不同的更改时,并发现应用程序框架存在短处时,往往就会出现问题。这通常是一种需要改变应用程序工作方式的质的变化。

这种变化可能涉及调用多个数据库,或者第一次依赖多个相关的事务。它可能需要调用一个不可预测执行时间的软件进程。

这种变化可能不足以引发临界点,但开发者的工作生活会开始发生变化。他们可能会编写管理跨进程通信的管道代码,以保证执行并保持事务的一致性。但这只是开始。编写那些代码需要时间,现在,开发者必须扩展代码以应对引入的每个新的质变。

他们会这样做一段时间,但情况会越来越糟。最终,随着他们添加更多的事务,开发者将花费更多的时间来维护管道代码。原本是线性开发工作量现在变成了指数级别。每次进行新变化,开发时间的增加是不成比例的。

“末日之会”

有些人在跨过临界点之前并不知情。没有经验的初级开发人员常常在毫无防备的情况下陷入其中。高级开发人员通常处境最糟,他们知道临界点即将到来,但政治原因往往使他们无能为力,只能等待并收拾残局。

最终,有人引入了一个变化,揭示了这个问题。这是压垮骆驼的最后一根稻草。也许一项变化打乱了软件交付进度,有影响力的人抱怨了。接着,就会召开“末日之会”。

这个会议是团队承认他们当前的方法不可持续的场所。应用程序变得如此复杂,以至于这些临时的管道更改不再支持项目的进度表或预算。

这种认识使开发人员经历了五个悲伤的阶段:

  • 否认阶段。这个问题可能已经存在一段时间了。人们试图忽略这个问题,并辩称继续现状不会有问题。然后进入...
  • 愤怒阶段。在会议上,有人解释说这样做不行。他们的预算被打破了,进度也被推迟了,问题必须得解决。他们不会接受否定的答案。于是人们试图进行...
  • 讨价还价阶段。人们想出创造性的方式,用更多的临时改变来支撑更长时间。但最终他们意识到这不是可扩展的,导致...
  • 沮丧阶段。最终,开发人员意识到他们必须做出更根本的架构变化。他们的临时管道代码已经开始变得难以控制,事情已经逆转。这和...
  • 接受阶段。每个人都带着一种末日的感觉离开了会议,知道这之后什么也不会好转。现在是取消几个周末开始工作的时候了。

这种末日的感觉是有根据的。正如我解释的那样,管道代码很难编写和维护。从临界点开始,开发人员发现编写和维护代码变得更加困难。他们突然间不再有他们习惯的线性编程体验。他们花费的时间编写事务管理代码比处理看板上的软件功能还多。这会导致开发人员过度疲劳,最终导致流失。

防止临界点

如何避免这个“临界点”,使曲线变平滑,保持软件功能和开发时间之间的线性比例?第一个建议通常是承认这次失败,并承诺下次从一开始就编写正确的代码或重复使用已经拼凑好的代码。

但这并不行。这会留下相同的问题,也就是管道代码最终会变得无法管理。这并不是一个临界点,而是开发过程更早地失去线性。这将导致从项目逐渐陷入发展不安。

相反,团队需要做该从一开始就做的事情:进行一项支持系统化持久执行的重大架构更改。

我们已经讨论了向前的方法是抽象化。在编写项目代码的任何一行之前,从应用层将管道函数抽象出来到它们自己的服务层。这将通过移除非线性工作来减轻开发人员的负担,使他们能够扩展并保持实现新功能所需的时间不变。

这种抽象化维护了程序员的线性体验。他们始终感到对自己的时间掌控,确信自己在做事。他们不再需要考虑围绕缓存和队列等任务的战略决策。他们也不必担心将庞大的软件工具和库组合在一起来管理这些任务。

使用抽象服务将事务工作流程抽象出来的项目经理将与开发人员一样满意。对于他们来说,确定性和可预测性是关键要求,这使得具有突破线性开发的“临界点”尤其成问题。将事务排序的任务抽象化可以消除开发人员的意外工作负担并保持线性,为他们提供实现计划和预算承诺所需的确定性。

支持这种抽象和将管道代码转换为可管理工作流程的工具将帮助您保持可预测的软件开发实践,消除可怕的“临界点”,并为您节省项目纠正的压力。最好在项目开始之前部署这些抽象服务,但即使您的团队现在处于危机状态,它也为您提供了一种解决困境的方法。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注