说明
这篇文章是我用AI创作的第一篇技术文章,算是Vibe writing,创作过程如下:
- 躺床上,花了两个小时与Gemini探讨不同的Durable execution方案的差异
- 起床,将这些差异整理了一个大致的提纲
- 让Gemini根据我的提纲生成文章,读了一遍,有一股明显的“鹦鹉感”,就是明显感觉它并不真正知道自己到底是在写啥,非常缺乏体感,而且很多技术细节写的有问题。
- 把Gemini写的文章交给了GPT,果然遭到了GPT的鄙视,说文章整体结构还行,但是很多细节写的有问题,于是GPT给我修复了这些细节问题,并修改了一些语言风格,不再那么“鹦鹉”。
- 将GPT的作品交给了Cluade Opus,Opus说写得很好,同时也指出了一些小毛病,但我感觉无伤大雅,也就懒得改了,直接发布。
正文
在现代分布式系统架构中,Durable Execution(持久化执行)正逐渐成为微服务编排的重要能力,也越来越多地出现在需要长周期运行的 AI Agent 系统中。它希望提供一种“可恢复的执行环境”:当进程崩溃、机器重启、网络闪断等故障发生时,业务逻辑能够从中断点附近自动恢复并继续推进,直至完成。
需要强调的是,Durable Execution 通常保障的是“流程推进的可靠性”(例如步骤调度的 at-least-once 语义、定时器与信号不丢),而对外部系统产生的副作用(发消息、扣款、写第三方)是否会被重复执行,仍然需要借助幂等、去重或事务性出站(Transactional Outbox)等手段来共同完成。
业界实现 Durable Execution 大体存在两种架构流派:
- 基于事件溯源(Event Sourcing / History + Replay):以 Temporal(以及 Azure Durable Functions 背后的 Durable Task Framework 等)为代表,通过持久化事件历史并以确定性重放来恢复内存状态。
- 基于状态(State-based / Snapshot / Actor):以 Akka/Orleans 等虚拟 Actor(Virtual Actor)体系为代表,通过持久化“当前状态快照”(或以状态为中心的持久化)来恢复与继续执行。
本文将剖析两种实现的底层机制、优劣势与适用场景,为架构选型提供参考。
基于 Event Sourcing(History + Replay)的实现
这种模式的核心理念可以概括为:“代码即流程,历史即状态”。系统不直接持久化“当前内存快照”,而是记录导致状态演化的事件序列;当需要恢复时,通过回放历史事件来重建内存状态。
工作方式
- 追加写入(Append Only):外部输入(Signal)、内部调度(Timer Fired)、以及副作用步骤的结果(如 Activity Completed)会被记录为不可变的事件历史(History Log)。
- 重放恢复(Replay):当 Worker 崩溃、迁移调度或重新拉起时,系统会加载该流程实例的历史事件,从头(或从某个检查点语义上)重新执行业务代码,以重建同样的内存状态。
- 确定性执行(Determinism):只要业务逻辑是确定的(Deterministic),在相同历史事件驱动下,重放后的内存状态就会与故障前一致。
这里的“从头重放”不等价于“每次都做同样昂贵的全量计算”,但其成本确实与 history 长度强相关:history 越长,任务调度与重放开销越显著。
优点
-
Worker 无状态,天然适合 Pull 模型
- Worker 本身不需要保存实例状态,按需拉取(Pull)任务与历史并重放即可。这使得 Worker 可随意扩缩容,集群协调相对简单。
- Pull 模型也带来天然的背压(Backpressure):Worker 依据自身能力消费任务,不容易被突发流量“推爆”。
-
跨语言、相对低侵入
- 核心是“服务端持久化 + 协议驱动”的交互模型;多语言 SDK 只要遵循确定性与运行时约束即可协同。
-
控制流表达能力强
- 对开发者而言,可以直接用通用语言的 if/for/try-catch 来表达复杂分支、循环、补偿与异常路径(Saga 等),在描述复杂业务逻辑时往往比绘制大型状态机更清晰。
-
可观测性好,便于调试与审计
- 历史事件记录了流程的完整轨迹,不仅能看到“现在是什么状态”,还能看到“为何走到这里”。
- 支持类似“时间旅行”的调试方式:将生产 history 下载到本地进行重放,较精确地复现问题现场(前提是代码版本/依赖与确定性条件可控)。
缺点
-
历史记录膨胀(History Bloat)
- 随流程运行时间变长、交互次数增多,history 通常线性增长。加载与处理大量 history 会带来 CPU/IO 消耗,并增加任务延迟尾部(tail latency)。
- 常见缓解手段如 Continue-as-New、压缩/归档、定期切段 等,但会引入额外运维与工程复杂度(例如流程分段、查询跨段、可观测性拼接等)。
-
确定性约束要求严格
- 需要避免或正确封装随机数、系统时间、非确定性的并发、非幂等的本地副作用等。
- 这对团队工程纪律要求较高:代码评审、SDK 约束、最佳实践都要跟上,否则容易“上线后才发现不可重放”。
-
写放大与持久层压力
- 即便是简单步骤流转,也可能产生多条历史事件(例如 Scheduled/Started/Completed/Failed 等),高并发下对持久层 IOPS 与延迟非常敏感。
-
交互密集型场景成本容易失控
- 外部信号(Signal)频繁到来时,往往会触发更多的 workflow task 处理;处理成本与 history 长度相关。
- 如果实例既“活得久”又“交互频繁”,history 会快速膨胀,进而带来调度开销、重放开销和存储成本的叠加,工程上需要非常谨慎地设计(例如聚合信号、降低交互频率、把高频状态外置等)。
基于 State(Snapshot / Actor)的实现
这种模式的核心关注点是:“当前状态是什么”。系统以“状态对象(State Object)”为中心持久化:当节点恢复时,直接加载最新状态并继续推进,而非依赖从历史中重跑代码来重建状态。
这种模式的典型形态是 虚拟 Actor(Virtual Actor) 或显式 FSM(有限状态机)/步骤流:每个实例(Actor/流程)对外表现为一个可寻址实体,串行处理输入,更新自身状态并持久化。
核心特征
-
恢复依赖最新状态,而非全量历史
- 故障恢复时通常直接读取最近一次持久化状态(或快照 + 少量增量),恢复成本更接近 O(1)(与状态大小相关),而不是与交互次数线性相关。
-
逻辑切分与状态管理更显式
- 切分单元:开发者往往需要将业务逻辑拆分为阶段(Stage/Step)、消息处理器或 Actor 方法。这看似增加显式建模成本,但也常常带来更好的模块化与边界清晰度。
- 状态选择:需要明确哪些是持久化状态(Persistent),哪些是临时变量(Ephemeral)。这种显式控制可避免把整个调用栈/上下文“整体序列化”的高开销与不确定性。
-
调度与通信:多为“投递到实体”的模型
- 通常表现为向某个 Actor/实体投递消息(类似 Push 到 mailbox),由运行时负责路由到正确的 activation。
- 运行时需要 membership、placement、路由与故障探测等能力(不同框架差异较大:Orleans 更偏“虚拟化 + 按需激活”,Akka Cluster 更偏“显式集群语义”)。
-
实现形态:原生运行时 vs Sidecar
- 原生一体化(Native Runtime):如 Microsoft Orleans、Akka(及其生态)。调度与业务在同一运行时中,通信路径短,性能与延迟更优。
- Sidecar 模式:如 Dapr Actors。跨语言更容易,但多一跳代理与通用化抽象,性能与可控性通常略受影响。
-
历史记录通常是可选项(但取决于具体持久化方案)
- 许多 state-centric 设计把历史用于审计/回溯,而恢复主要依赖快照状态。
- 但也存在“事件日志 + 快照”混合持久化的 Actor 方案(例如某些 Akka Persistence 用法),此时仍会依赖日志来恢复。选型时需要区分“接口形态是 Actor”与“底层持久化是事件溯源”这两个维度。
优势侧重点
- 高并发、交互密集、实时流处理更占优
- Actor 实例可以在内存中保持热状态,串行处理消息,延迟可以做到毫秒级甚至更低。
- 通过 write-behind(异步落库)、批量写入、状态压缩等策略,可以在吞吐、延迟与持久性之间做工程权衡。
- 状态更新更接近 O(1)
- 在高频交互场景下,每次仅更新当前状态并持久化(或异步持久化),通常不会因为“交互次数累计”而让恢复与处理成本线性恶化。
各自适应的场景
选型不应只看技术热度,而应结合业务逻辑形态、交互频率、性能目标与运维成本综合评估。
从业务逻辑形态看
-
高度结构化(步骤清晰)
- State 模式更合适:例如审批流、工单流转、供应链状态、明确的阶段推进。用 FSM/Actor 显式表达阶段与状态,天然可视化、也更便于治理。
-
高度代码化(分支/循环/异常复杂)
- History + Replay 更合适:例如复杂 Saga、涉及大量补偿与异常捕获、动态分支与循环。用通用语言控制流表达通常比维护一个巨大状态机更可维护。
从性能与交互特征看
-
场景 A:普通长周期任务(并发与交互都不高)
- 结论:两者皆可。
- 倾向:History + Replay 往往胜在开发体验与抽象统一,“代码即流程”带来很高的表达效率。
-
场景 B:大量触发、执行中很少交互的长周期任务(Fire-and-Forget)
- 特征:海量并发启动,但运行过程中与外部的信号交互很少。
- 结论:State 模式通常成本更低。
- 理由:History + Replay 可能产生更显著的日志写放大与持久层压力;而 state-centric 方案往往只需存储关键状态或最终状态,IOPS 更可控。
-
场景 C:完全事件驱动、交互密集型的长周期任务
- 特征:实例需要持续处理大量外部事件(例如游戏房间状态、实时风控订单修改、竞价/撮合状态、在线协作会话等)。
- 结论:通常更适合 State/Actor。
- 理由:这类场景的关键矛盾是“高频交互 + 长时间存活”。History + Replay 在工程上需要非常强的约束与削峰策略(例如信号聚合、Continue-as-New、外置高频状态),否则 history 膨胀与重放成本会显著放大尾延迟与存储成本;而 Actor 的 O(1) 状态更新模型天然更匹配。
-
场景 D:极低延迟的短任务(同步请求、毫秒级 SLA)
- 结论:通常是“无状态服务 + 直连数据库/缓存”最可控,Actor 次之,工作流/编排类往往不作为首选。
- 理由:Actor 框架虽快,但 activation、placement、迁移等仍可能带来抖动;而工作流引擎的持久化与调度路径更长,除非你需要其可靠性语义,否则很难在纯延迟上取胜。
需要注意版本问题
两种模式在代码升级兼容性上的差异很大:
- History + Replay:对代码变更非常敏感,必须保证新版本代码对旧 history 的 replay 仍然 deterministic,否则会出现 non-determinism error。Temporal 官方有专门的 versioning API(getVersion / patched)来处理。
- State-based:通常只需要做状态结构的兼容(例如加字段给默认值),不涉及”重跑逻辑”的一致性问题,升级相对简单。
最后
Durable Execution 很可能会成为工作流引擎、微服务编排与 AI Agent 系统中的基础能力。但即便都宣称“支持 Durable Execution”,不同实现路径(History + Replay vs State/Actor)在性能上限、工程约束、故障恢复成本、可观测性与运维复杂度上都会呈现显著差异。
更稳妥的策略是:先刻画业务的“逻辑形态”和“交互特征”,再结合团队工程能力(确定性约束、幂等体系、可观测性与治理)做选择;必要时采用分层架构——用Temporal做低频编排、用 Actor/状态服务承载高频状态与实时交互——往往能同时获得可靠性与性能。