尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

恢复后如何避免重复执行?

恢复后如何避免重复执行? 第178题恢复后如何避免重复执行1. 核心结论恢复后避免重复执行不能只依赖从Checkpoint继续也不能依赖消息队列只消费一次更可靠的设计是持久化状态机 Checkpoint / Event Log Worker Lease Idempotency Key 副作用状态表 恢复前Reconciliation 只重放未提交步骤核心原则是同一个逻辑步骤即使因为崩溃、超时、消息重复而被调度多次也只能产生一次有效业务结果。工程上通常接受底层存在At-Least-Once Delivery然后通过幂等、去重和状态对账实现接近Effectively-Once Business Effect的效果。2. 为什么恢复后最容易出现重复执行假设Agent流程Step 1: 分析代码 Step 2: 创建工单 Step 3: 修改规则 Step 4: 通知用户执行到Step 2时外部工单系统已经成功创建工单。但Agent还没来得及写Checkpointprocess crash恢复后如果只看到checkpoint Step 1系统可能再次执行create_ticket()于是出现两个工单。所以CheckpointProgress CheckpointProgressCheckpointProgress与ExternalSideEffect ExternalSideEffectExternalSideEffect之间可能存在不一致。这就是恢复设计真正要解决的问题。3. 队列不能当成任务状态消息队列适合表达还有工作需要处理但不能作为业务真值。例如message received ↓ 执行副作用 ↓ Worker crash ↓ ack没有发送 ↓ message redelivery队列重新投递完全合理。如果业务逻辑没有幂等保护redelivery → duplicate side effect所以任务的真实状态必须独立持久化。4. 显式建立任务状态机例如整个任务CREATED READY RUNNING WAITING COMPLETED FAILED CANCELLED NEEDS_REVIEW每个Step也维护自己的状态PLANNED STARTED COMMITTED FAILED UNKNOWN COMPENSATED恢复时不问上次执行到第几个步骤而问每一个逻辑步骤目前处于什么已确认状态5. PLANNED、STARTED、COMMITTED必须区分例如PLANNED表示系统已经决定执行create_ticket但还没有向外部系统发请求。这种状态恢复后通常可以直接执行。STARTED表示请求已经发送但本地没有确认最终业务结果。这是最危险的状态。恢复后不能直接认为失败也不能直接重新执行。应该先Reconciliation。COMMITTED表示已经确认该业务副作用成功发生。恢复时应该直接skip绝不能再次执行。6. Checkpoint应该保存什么一个可靠Checkpoint至少包含task_id workflow_version state_version current_phase completed_steps step_status idempotency_keys operation_ids tool_results artifact_refs external_side_effects retry_count budget_used model_version prompt_version如果涉及RAG或代码分析还应保存repository_commit index_version evidence_refs这样恢复时才能重建真正一致的上下文。7. 有副作用工具必须使用Idempotency Key例如任务task_id T123 step create_ticket可以生成kHash(T123, create_ticket, logical_target) kHash(T123,\ create\_ticket,\ logical\_target)kHash(T123,create_ticket,logical_target)调用create_ticket( idempotency_key k )第一次请求成功但响应丢失。恢复以后再次使用same k服务端应该识别这是同一个逻辑操作。于是返回已有结果而不创建第二个工单。AWS 的分布式系统实践也明确使用稳定的客户端请求标识使有副作用操作能够在网络失败后安全重试。8. Idempotency Key必须稳定错误方式每次Retry重新生成UUID第一次keyA恢复后keyB对于服务端来说A ! B它会认为这是两次新业务请求。正确方式应该让幂等键与Logical Operation绑定而不是和某一次物理请求绑定。即LogicalOperation→StableIdempotencyKey LogicalOperation \rightarrow StableIdempotencyKeyLogicalOperation→StableIdempotencyKey9. 幂等键还需要绑定请求语义不能仅有key abc还应保存对应请求摘要$$RequestHashHash(NormalizedParameters)$$如果同一个Idempotency Key后来却携带完全不同参数第一次pay 100 第二次pay 1000系统应该拒绝。否则会混淆重复请求和新的业务意图10. 恢复时先判断三类步骤可以把每个步骤归为三类。第一类COMMITTED处理SKIP第二类PLANNED处理EXECUTE第三类STARTED / UNKNOWN处理RECONCILE因此恢复算法大致是for step in workflow: if step COMMITTED: skip elif step PLANNED: execute elif step in {STARTED, UNKNOWN}: reconcile这比简单resume_from(step_id)安全得多。11. UNKNOWN状态为什么一定要对账例如系统调用block_account(user_123)随后Timeout。恢复时可能存在情况A没有封禁 情况B已经封禁 情况C正在处理中客户端不知道。因此应该查询get_operation(operation_id)或get_account_status(user_123)然后再决定。如果已经BLOCKED就把本地Step补写为COMMITTED如果明确NOT_EXECUTED才能重新执行。12. 不能把Timeout自动写成FAILED这是非常常见的错误。Timeout意味着ResponseUnknown ResponseUnknownResponseUnknown不等于OperationFailed OperationFailedOperationFailed尤其有副作用工具中Request ↓ Server Commit ↓ Response Lost客户端看到的仍然是Timeout。如果恢复程序把timeout failed然后重新调用就会制造重复业务操作。因此这种状态更合理地标成UNKNOWN13. Worker Lease用于防止并发双执行假设Worker A正在处理task T突然网络变慢。调度器误以为A已经死掉于是把任务交给Worker B如果没有并发控制A执行 B执行会同时发生。因此Worker获取任务时应获得lease并周期性heartbeat只有持有有效执行权的Worker才能继续推进状态。14. Lease本身仍然不够假设Worker A发生长时间GC Pause。它的Lease已经过期。Worker B获得新Lease并继续任务。随后A恢复。如果A不知道自己的执行权已经失效仍可能继续写外部系统。所以可以进一步使用Fencing Token / Generation Number。例如Worker A → generation 7 Worker B → generation 8对支持这种机制的状态更新generationrequestgenerationcurrent generation_{request} generation_{current}generationrequest​generationcurrent​则拒绝旧Worker继续提交。但对于无法识别Fencing Token的第三方系统最终仍然需要幂等操作保证副作用不重复。15. Lease解决执行权幂等解决业务结果两者职责需要分开。Lease解决当前原则上谁应该执行而Idempotency解决即使同一个操作真的被请求了两次是否会产生两次业务效果因此可靠系统需要LeaseIdempotency Lease IdempotencyLeaseIdempotency而不能只选择其中一个。16. 状态提交最好使用Compare-and-Swap假设Step当前状态STARTED version 17Worker完成以后准备更新COMMITTED应该带expected_version 17数据库执行类似UPDATE WHERE version 17如果另一个Worker已经把版本推进到18旧Worker的提交失败。这可以避免Lost Update。17. Event Log比只保存current_state更容易恢复只保存current_state STEP_5很难知道历史发生了什么。更好的设计是记录事件TaskCreated StepPlanned ToolStarted ToolSucceeded ArtifactCreated StepCommitted WorkerLeaseLost RetryScheduled然后当前状态St S_tSt​由历史事件E1,E2,…,Et E_1,E_2,\ldots,E_tE1​,E2​,…,Et​重建。这样更容易ReplayAuditDebugRecovery。Durable Execution系统通常也会持久化执行历史使流程在进程或基础设施故障后恢复而不是依赖原Worker存活。18. Replay必须只重放确定性决策如果恢复时重新运行LLM“接下来应该做什么”它可能生成和崩溃前完全不同的答案。因此长期任务最好区分已经提交的决策和尚未做出的决策已经做出的关键Agent决策应记录为EventSelectedTool search_repository Arguments {...}恢复时重放已有历史。只有到达真正没有历史记录的新决策点才重新调用模型。这样可以减少恢复前后路径漂移。19. Model和Prompt版本也应写入Checkpoint假设任务运行三天。期间Model v1 → Model v2或者Prompt v4 → Prompt v5恢复后如果直接使用最新版行为可能变化。Checkpoint应记录model_version prompt_version tool_schema_version workflow_version对严格可复现任务可以继续使用原版本。需要升级时应通过明确的迁移逻辑而非静默替换。20. 跨系统副作用需要Outbox或Saga假设一个Step包含1. 本地数据库写状态 2. 向外部消息系统发送事件如果DB成功 ↓ 进程Crash ↓ 消息没发就会出现状态不一致。Transactional Outbox可以把业务状态 待发送事件放进同一本地事务。之后独立Worker可靠发送Outbox。发送发生重复时消费者仍应幂等。21. 多个外部系统无法用普通事务整体回滚例如创建云资源 ↓ 修改DNS ↓ 修改防火墙这些可能属于三个独立服务。普通数据库事务无法BEGIN ... ROLLBACK覆盖全部系统。因此要使用类似Saga。每个Step定义Forward Action Compensation Action例如create_resource ↔ delete_resource失败时反向补偿已完成步骤。22. Compensation也必须幂等例如恢复时系统不确定delete_resource是否已经执行。如果补偿本身不能重复调用又会引入第二层重复副作用问题。因此$$Compensation(Compensation(x))Compensation(x)$$应尽量成立。并且补偿结果也记录COMPENSATED23. 有些副作用根本无法真正补偿例如发送邮件发送以后不能真正unsend再如通知外部机构也可能不可逆。这类步骤应在执行前加强Human Approval参数校验幂等Dry RunPolicy Check。如果恢复时状态仍不明确应优先Human Handoff而不是自动猜测。24. 恢复流程可以设计成固定状态机例如LOAD CHECKPOINT ↓ ACQUIRE LEASE ↓ VERIFY VERSION ↓ RECONCILE UNKNOWN SIDE EFFECTS ↓ SKIP COMMITTED STEPS ↓ REPLAY PERSISTED EVENTS ↓ EXECUTE UNCOMMITTED STEP ↓ ATOMICALLY COMMIT STATE ↓ CONTINUE这套恢复流程应主要由确定性代码实现。不能完全交给LLM自行决定。25. 什么情况下直接人工接管以下情况应停止自动恢复副作用状态无法确认外部系统之间状态互相矛盾补偿动作风险很高原Workflow版本已无法恢复权限发生变化自动重试预算耗尽此时需要输出完整恢复包task_id last checkpoint event history operation ids idempotency keys confirmed side effects unknown side effects recommended next action交给人工判断。26. Schema升级也会影响恢复长任务可能运行数小时甚至数天。期间状态Schema可能从v1升级v2因此Checkpoint必须记录schema_version读取旧状态时通过Migration(v1 → v2)转换。同时最好保留Backward Read Compatibility。否则代码一发布新Worker可能连旧Checkpoint都读不出来。27. 如何测试真的不会重复执行最重要的是Fault Injection。例如构造Tool已经成功创建资源 ↓ 故意在写COMMITTED之前Kill Worker然后启动恢复。预期资源数量仍然只有1个再测试外部操作成功 ↓ Response丢失 ↓ Worker Crash ↓ 任务Redelivery系统仍不应创建第二个资源。这些比只测“正常恢复成功”重要得多。28. 关键指标可以报告$$DuplicateSideEffectRate\frac{DuplicateSideEffects}{LogicalSideEffectOperations}$$理想目标DuplicateSideEffectRate≈0 DuplicateSideEffectRate\approx0DuplicateSideEffectRate≈0还应报告RecoverySuccessRate RecoverySuccessRateRecoverySuccessRateMeanRecoveryTime MeanRecoveryTimeMeanRecoveryTimeAmbiguousOutcomeRate AmbiguousOutcomeRateAmbiguousOutcomeRateHumanEscalationRate HumanEscalationRateHumanEscalationRate以及ReplayCount/Task ReplayCount/TaskReplayCount/Task这样才能观察恢复机制到底是否稳定。29. 不要轻易宣称“Exactly Once”分布式系统中存在Worker crashNetwork partitionTimeoutMessage redelivery第三方服务以后端到端的严格 Exactly-Once 往往很难由单一组件保证。更准确的工程表达通常是At-Least-Once Execution Idempotent Side Effects Deduplication Durable State Reconciliation从而实现Effectively-Once Business Semantics。这种说法比简单宣称Exactly Once更严谨。30. 面试时可以压缩成下面这段恢复后避免重复执行我不会依赖队列“只投递一次”而是把任务状态和副作用状态持久化。每个步骤至少分成PLANNED → STARTED → COMMITTED。Checkpoint记录已完成步骤、状态版本、幂等键、Operation ID和外部副作用。如果恢复时看到COMMITTED就直接跳过PLANNED可以执行如果处于STARTED或UNKNOWN先向外部系统做Reconciliation确认操作是否已经生效再决定是否重放。所有创建、修改、付款、封禁这类有副作用工具都使用稳定的Idempotency Key所以即使消息重复投递或者第一次请求成功但响应丢失多次调用也只产生一次业务效果。Worker层用Lease和Heartbeat减少双执行再用generation/fencing token防旧Worker恢复后继续提交但Lease不能代替业务幂等。跨系统流程则用Outbox、Saga和Compensation处理部分提交。恢复时只重放未提交步骤已经Committed的事件通过Event Log重建不重新执行。最后我会重点做故障注入让外部操作已经成功以后故意Kill Worker、丢Response、重复投递消息。如果恢复后Duplicate Side Effect Rate仍接近0才能说明恢复机制真正可靠。31. 当前资料的事实边界源文件没有提供候选人实际系统中的状态数据库Checkpoint SchemaIdempotency Key生成方式Lease实现Fencing TokenEvent StoreOutboxSagaFault Injection结果Duplicate Side Effect Rate。因此目前不能声称具体系统已经证明“恢复绝不重复执行”。源文件直接支持的设计要求是显式状态机与事件日志、版本化Checkpoint、租约/心跳、幂等键、副作用状态记录、恢复前外部对账、只重放未提交步骤、跨系统补偿以及无法自动恢复时人工接管。32. 来源AWS Builders’ Library,Making retries safe with idempotent APIs。AWS Well-Architected Framework,Make mutating operations idempotent。Temporal Documentation,Durable Execution。
返回列表