durable workflow对比简单任务序列的优势在哪里,他是如何实现成功步骤的内容提取和失败步骤任务丢弃的

群里没有直接讲 durable workflow 的条目,这块我按通用工程事实讲。 先说它到底解决什么问题。简单任务序列的本质是"一条 function 调用一条 function,状态全在进程内存里",它的隐含假设是:进程不会死、每一步都很快成功、失败就整体重来。一旦某一步是"调外部 API 创建了订单"或者"跑了 20 分钟的训练",这个假设就崩了:进程一挂,你既不知道订单建没建,也不舍得重跑。 一、相比简单任务序列的优势 1. 执行状态外置,进程可丢。durable workflow 把"执行到哪一步了"当成持久数据写进数据库(事件日志、状态表),而不是靠调用栈保存。于是可以做到:worker 崩了换个进程接着跑,机器重启不受影响,甚至同一份 workflow 可以在不同机器上被接手。这个性质在简单序列里是根本拿不到的,因为调用栈在内存里,死了就没了。 2. 重试是精确的,不是整体的。简单序列的重试粒度和它本身一样粗:失败了就从头再来,前面的副作用全部重复。durable workflow 的 checkpoint 是每步一个,恢复时从最后一个成功点继续。外部调用掉线、限流、超时这些"高频但瞬时"的失败,就不会再拖垮整条链路。 3. 能承载长时间和高成本。人可以等 5 秒,不能等 5 天。审批等三天、等一个 webhook 回调、等某个文件上传完成,这类"挂起"在内存序列里只能阻塞一个线程,在 durable workflow 里是"把状态写盘然后退出进程",回来时再唤醒。这也是它常被拿来对标"定时任务+状态机"的原因。 4. 失败可见、可对账。因为每一步的输入输出都落库,你能看到"第 3 步成功了但第 4 步失败了 5 次"这种具体现场,而不是只有一个堆栈。配合幂等键还能做对账:查到外部系统里这笔已经存在,就不用重做。 5. 语义清晰。它在语义上更接近"事务边界的粗粒度版本":约定"这一步整体成功或整体没发生",把不确定的外部调用包在一步里,让整条链路的中间态永远处于有限几个可枚举的点上。简单序列没有这样的点,中间态是连续的、不可描述的。 二、成功步骤的内容提取是怎么做的 核心机制叫"事件溯源 + 确定性重放",各家实现细节不同但骨架一样: 1. 每完成一步,把这一步的"结果"作为一条事件追加写入持久日志,内容包括步序号/名字、输入摘要、返回值、时间戳。Temporal 里叫 history event,AWS Step Functions 是状态转移记录,Camunda 之类是流程变量表,本质都是 append-only log。 2. 恢复时不是从断点"读变量继续",而是从头重放这段历史。workflow 代码在重放到第 N 步时,看到日志里已有这一步的结果,就直接把存下来的返回值返回给代码,跳过实际调用。所以"内容提取"其实是"重放时命中缓存"——你代码里写的是 result = await activity,框架偷偷换成 result = replay_from_log。这也是它要求 workflow 代码必须确定性的原因:重放要能得到和当年一样的控制流。日期、随机数、UUID 这类不确定的东西得走框架提供的接口,让结果也进日志。 3. 落到具体的"内容",就是序列化。返回值必须是可序列化的结构(JSON 或 protobuf),做不到就只能存个引用。这带来一个实践约束:把大对象(几十 MB 的文件、二进制)传进返回值会撑爆历史日志,正确做法是存到对象存储、日志里只留 key。反例是本地闭包、数据库连接、文件句柄,这些不能进状态。 4. 有些实现是另一种取舍:状态机式(Step Functions)不重放代码,而是把每个状态的输出写成一个 JSON 对象,下一步从显式引用的字段取。好处是状态完全显式、可视化;坏处是逻辑和状态描述分离,改流程要改定义文件。代码即 workflow(Temporal、Restate 这类)反过来,改代码时要靠版本/补丁机制保证老实例重放不乱——这是它的主要痛点,"deterministic replay 撞上代码发布"是这套东西最常见的坑。 三、失败步骤的"丢弃"是怎么做的 这里要区分三种不同的处理,很多讨论会把它们混成"丢弃": 1. 重试失败:不丢,退避重试,超过策略上限才判失败。这一层和简单序列的差别只是粒度和持久化。 2. 已产生副作用之后的失败:这才是难点,本质不能靠框架解决,要靠幂等。做法是给每步一个业务幂等键(workflow id + step id + attempt),外部服务按这个键去重;或者失败后先查一次外部状态再决定重做还是跳过。框架能保证"至少执行一次",永远不会替你保证"恰好一次",所以别指望它帮你把外部副作用抹掉。 3. 真正的丢弃/回滚:分两种语义。一种是补偿(saga):失败后反向执行已成功步骤的补偿操作(取消订单、释放预留、删掉临时文件),正向步骤的"内容"不删,作为审计保留。另一种是终止清理:workflow 标记为 failed 或 terminated,超时、重试耗尽、人工取消属于这一类,此时队列里的后续任务作废,运行中的 activity 尽力 abort(能不能真中断取决于它有没有检查取消信号),未开始的步骤不再调度。 至于"丢弃"的具体实现方式,多数系统是软删除 + 状态标记,而不是物理删除日志——日志要留下来给重放和对账用。物理清理是另一个策略问题:设置保留期、归档到冷存储,或者按合规要求清掉 payload。这里有个容易忽略的点:把状态标成 terminated 并不会自动撤销外部系统里已经发生的事,那还是得走补偿或人工修。 四、一个容易踩的坑 很多人以为上了 durable workflow 就能"自动续跑",结果第一步就卡住:workflow 代码里读了个时间戳做分支,重放时值变了,控制流走岔,历史对不上直接报错。所以正确姿势是:workflow 只做编排(决策、分支、等待),所有不确定、不幂等、有副作用的东西全部压缩到 activity 里,activity 里可以随便读时间、随便调外部 API,因为它失败重跑就是新的一次执行记录。这条边界划清楚了,其他都好办。 如果你是在给自己那类编排系统(主 agent 派活、worker 执行、状态对账)选型,短链路 + 副作用少的话自己写状态表 + cron 对账就够了,别上 Temporal;一旦出现"等外部回调几天"或"跨进程重启要续跑",再去用现成的。