持久执行:在不可靠世界中构建可靠软件

4 阅读13分钟

IT系统不可靠,软件可靠性成难题。持久执行平台(如Temporal)通过状态管理、智能重试及热修复,确保应用任务完成,简化弹性系统构建,提升可靠性及可调试性。

译自:Durable Execution: Build reliable software in an unreliable world

作者:Charles Humble

软件可靠性对于开发者来说是一个长期存在的问题,因为IT系统建立在不可靠的组件之上:硬件会退化;软件有bug;网络会丢包;大型语言模型(LLMs)会产生幻觉;电源会故障。

一个正在执行的程序会一直运行直到完成,除非:

A. 程序或操作系统遇到bug B. 发生外部故障,例如机器断电或重启 C. 发生硬件故障

这里有一个小例子来说明这个问题。

在我写这篇文章的时候,我家里有电工在维修保险丝盒,电源跳闸了好几次。我最小的孩子,正在家里修改 Minecraft,电源中断时丢失了大量工作。这令人抓狂,发生这种情况是因为RAM是易失性的,需要持续供电才能保存数据。

像这样在家中丢失工作已经足够令人沮丧,但想象一下在企业规模上,成千上万的人使用同样的不可靠系统时的烦恼。

通过防御性代码和硬件冗余实现可靠性

传统上,软件工程师试图通过使用容错硬件和设计应用程序从崩溃和故障中恢复来实现不可靠组件的可靠性。

常见的提高可靠性的硬件方法包括RAID、网络接口卡(NICs)、冗余电源,以及在更昂贵的机器上使用的热插拔CPU。通过双盘阵列进行磁盘镜像可以增加平均故障间隔时间(MTBF),因为虽然单个驱动器仍有其有限的MTBF,但阵列直到两个驱动器都出现故障才会失效。

然而,正如 Tom WheelerTemporal 的首席开发者倡导者,在公司年度 Replay 大会前接受 The New Stack 采访时所说,“当你扩展并从一台服务器扩展到两台、10台、100台或1,000台服务器时,任何一个磁盘可能发生故障的可能性都会呈指数级增长。” 此外,基于硬件的方法只能保护你免受硬件故障的影响。

对于软件而言,提高可靠性最常见的方法是编写防御性代码。高级开发者倾向于编写更多防御性代码,因为他们见过太多事情出错。

使用持久执行重新思考应用程序开发

持久执行鼓励一种不同的方法。

持久执行平台,例如 TemporalAWS Lambda 持久函数Azure 持久函数CadenceCloudflare 工作流FlawlessInngestRestate,确保您的应用程序在不利条件下也能正常运行。

它们通过保证应用程序运行完成、存储状态信息并在失败时重建状态来实现这一点。

正如 Wheeler 所解释的,Temporal 平台“在崩溃后以安全的方式完全重建状态。执行会从那一点继续,这样你就不会重复崩溃前已成功完成的步骤。”

持久执行本身并没有什么你无法自己构建的功能——这些概念至少从 1990年 以来就存在于消息传递应用程序中。但自己实现是具有挑战性的。开发者花费大量时间编写代码来将应用程序状态存储在数据库中,并使用诸如 Apache Kafka 等系统将状态变化通知给其他应用程序。软件开发中存在一些模式,例如 事务发件箱模式,其目的就是解决当这些努力失败时可能出现的问题。

持久执行使这一切变得不必要。

“持久执行是一个非常简单的概念,但它对软件工程师开发方法有着深远的影响,” Wheeler 说。“你不必担心崩溃,就像你不必担心数据包丢失或以错误的顺序传递一样。”

“有了持久执行,你的应用程序就能克服这些问题,从而以更简单的方式实现目标。”

他认为这改变了开发者对编程的看法。“开发者习惯于缩短执行时间,因为程序运行时间越长,越有可能遇到问题。有了持久执行,你的应用程序就能克服这些问题,从而以更简单的方式实现目标。”

他继续说道:“想象一下,你正在为注册 SaaS 服务免费版的新用户进行入职培训。你需要跟踪使用情况并定期发送定制的升级优惠,直到他们取消或订阅付费版本。

实现这一功能可能需要调度器、应用程序数据库和消息队列。有了持久执行,你就不需要这种复杂性,因为时间不再是敌人。你可以使用 for 循环和 sleep 语句来实现它,持续时间可以是几天、几个月甚至几年。”

持久执行的机制

持久执行平台的目的在于管理状态、处理重试,并确保一系列任务(通常是长时间运行的)即使在基础设施出现故障的情况下也能成功完成。持久执行平台中的核心抽象通常被称为工作流(Workflow)。

工作流是一个函数,它定义了实现特定目标的步骤序列。例如,一个电子商务订单处理工作流可能会计算总金额、向客户的信用卡收费并发送确认邮件。

在不同调用之间可能表现不同的步骤——例如对外部系统的调用——不能直接包含在工作流中。相反,它们必须放置在称为活动的独立函数中,并在工作流中引用。

在运行时,应用程序服务器上的 Worker 进程执行这些函数。每个 Worker 都会向 Temporal 服务轮询任务,指定下一个要运行的函数,然后报告结果。Temporal 服务将每个任务的详细信息,包括由 Worker 报告的完成状态和结果,记录到一个称为事件历史(Event History)的只追加日志中。

如果发生崩溃,另一个 Worker 可以接管,从事件历史中重建故障前的状态,并恢复执行。

为了使这种回放机制正常工作,工作流代码必须是确定性的——即给定相同的输入,产生相同的命令序列。非确定性操作(例如涉及当前时间或随机数的条件逻辑)可能会在回放过程中导致不匹配。

具有副作用的操作(例如数据库更新、通知、磁盘或网络 I/O)必须封装在活动中。在回放期间,平台不会重新执行之前已完成的活动,而是使用存储在事件历史中的结果。

即使采用这种细致的方法,仍然存在活动可能被执行两次的微小可能性。想象一下,你的活动使用支付服务为客户的信用卡收费,在卡被收费和 Temporal 服务记录结果之间的几毫秒内发生崩溃。新的 Worker,由于不知道该活动已完成,会再次执行它。

因此,活动应该是幂等的,这样重复调用就不会导致不良行为。大多数支付服务,以及许多其他类型的第三方 API,都允许调用者在请求中包含一个幂等键。这个键使这些服务能够识别并忽略重复请求。

持久执行如何处理持久性处理故障

任何允许重试的平台都有重复事务的风险,而持久执行本身并不提供幂等性支持。然而,由于已完成的活动不会被重试,因此风险降低了。

另一类错误是持久性处理故障,在消息传递应用程序中被称为“毒药消息”。毒药消息是指接收应用程序反复未能处理的消息——通常是由于错误数据、不正确的格式或应用程序bug——导致其被回滚到队列并无休止地重新传递,可能形成无限循环。

同样的模式也可能发生在持久执行中,即应用程序在执行达到最后一个记录事件并切换到执行模式时崩溃。这些错误可能是瞬时性的或持久性的。

瞬时错误通常可以通过回退和重试来解决,但这种技术无法修复持久性错误。例如,如果调用支付服务因账户余额不足而失败,两秒后再次调用很可能因同样的原因失败。Temporal 允许你将特定消息类型和错误指定为不可重试的,这有助于防止无意义的重试尝试。

对于非瞬时性的毒药消息式错误,Temporal 和大多数其他持久执行平台允许开发者在运行中的应用程序中修复问题,这被称为“热修复”。应用热修复涉及暂时停止所有 Worker、部署新代码,然后重新启动 Worker。执行将像崩溃后一样恢复。

Wheeler 解释说:“假设你有一个电商应用程序,你为其添加了一些代码,用于计算任何超过500美元订单的10%折扣。你的工作流代码中有这个功能,但假设有一个打字错误,所以你不是除以10,而是除以零。这显然是非法操作,因此会导致除以零异常(或你所用语言中的等效异常)。它会失败,每次重试都会再次失败。

“但是你可以进入并修复代码,然后重新部署应用程序。由于它从故障前的状态重建,在你修复后,它将使用没有bug的新代码。这意味着你可以在生产环境中修复一个正在运行的系统,并使应用程序能够继续运行。”

另一方面,瞬时错误可能是由服务限流、网络连接暂时丢失或服务暂时不可用引起的。自动重试因瞬时错误而失败的操作可以改善用户体验和应用程序弹性。然而,频繁的重试可能会使网络带宽过载并导致争用。

指数退避是一种技术,其中等待时间在指定次数的重试尝试后增加。例如,Temporal 默认的退避系数为2.0,这意味着它将在2秒后重试,然后是4秒、8秒、16秒,依此类推,最长可达100秒。

理解 Temporal 编程模型

从编程角度来看,Worker 负责执行工作流和活动定义。Worker 实现由 Temporal 软件开发工具包(SDKs)提供。Temporal 通过其 SDK 支持多种流行的编程语言,包括 Go、Java、Python、TypeScript、.NET、Ruby 和 PHP。你还可以混合搭配不同的语言。

“你可以有一个用 Java 编写的工作流,它调用一个用 Go 编写的活动,也许还会调用另一个用 TypeScript 编写的活动,等等,” Wheeler 说。

Temporal Web UI 提供了每个执行的详细信息,包括当前和以前的执行,这有助于开发者定位问题来源。

“当我在 Boeing 担任软件工程师时,用户会打电话给我说,‘应用程序昨天失败了。’ 我会问,‘错误消息是什么?’ 他们会回答,‘我不记得了。那是昨天的事了。’ 有了 Temporal,我可以直接查看昨天发生的执行,” Wheeler 说。

Temporal 还允许你下载 JSON 格式的事件历史记录,并使用它在你选择的调试器(例如你的 IDE)中重放执行。

“几乎每个开发者都曾花费无数时间试图重现一个难以捉摸的bug,那种似乎百万分之一的概率,发生在最不合时宜的时候,” Wheeler 说。“与其大海捞针般地试图重现bug报告中的问题,你只需搜索 Web UI 找到实际发生的执行,下载 JSON 文件,在调试器中重放,并找出原因。”

热修复使你能够修复和部署代码。“也许最棒的是,你还可以通过将该执行作为自动化测试的一部分来防止回归,”他补充道。

Temporal Web UI 截图显示运行状态,其中 ChargeCustomer 活动由于支付服务离线错误而在第三次尝试时失败。

Temporal 工作流执行状态:在“运行中”状态,伴随一个失败的活动(ChargeCustomer)

Temporal 工作流成功完成后的截图

Temporal Web UI 截图显示完成状态,工作流执行成功,并在结果面板中反映出72.74美元的费用。

Temporal 如何从 Amazon、Microsoft 和 Uber 的早期工作中发展而来

尽管只有不到六年的历史,Temporal 基于始于2000年代中期的 Amazon 的工作。联合创始人 Maxim Fateev 和 Samar Abbas 创建了 Amazon 的 Simple Workflow Service (SWF)。Abbas 后来在 Microsoft 参与了 Azure Durable Task Framework 的工作,这是构建 Azure Durable Functions 的关键组件。2015年,他们在 Uber 再次合作,并构建了一个名为 Cadence 的持久执行平台。

自2021年采用以来,Temporal 对 Netflix 云操作的可靠性变得“日益关键”。

由于其他公司对 Cadence 功能的需求不断增长,2019年,他们分叉了 Cadence 代码库,创建了 Temporal 作为一个独立的开源项目。从那时起,他们增加了安全功能、提高了性能、添加了对多种编程语言的支持,并增强了管理界面。

Temporal 已被金融服务和零售等各行各业的公司用于生产环境。在过去的两年中,它也被应用于 AI 应用程序,特别是在基于代理的系统中管理可靠性和人工监督。随着持久执行的不断成熟,它有望简化开发者构建弹性系统的方式。

Netflix 是 Temporal 的早期采用者之一。根据 Netflix 高级软件工程师 Jacob Meyers 和 Rob Zienert 在 Netflix 技术博客 上所写:自2021年采用以来,Temporal 对 Netflix 云操作的可靠性变得“日益关键”。他们写道:“Temporal 帮助我们将 Netflix 的瞬时部署失败数量从4%降低到0.0001%。”

希望了解更多信息的读者,Replay 大会 由 Temporal 主办,将于2026年5月5日至7日在旧金山举行。Replay 是开发者超越“足够好”的可靠性极限之地。今年,重点是 AI —— 从 AI 代理到教导 Claude 和 Cursor 如何编程 Temporal,以及更深远的内容。立即注册并计划参加。