先看事故:请求超时,系统却把同一篇简报发布了两次
周予经营一项面向 63 名付费客户的行业情报服务。每周一,她从 12 个固定来源整理 9 项变化,生成网站简报,检查 17 个引用链接,再给客户发邮件。最初的自动化只有“定时触发 → 抓取 → 生成 → 发布 → 发信”。前 7 周一切正常,第 8 周却出现了一次“请求结果未知”的事故。
| 时间 | 自动化看到的情况 | 外部实际情况 |
|---|---|---|
| 09:12:03 | 发出创建文章请求 | 内容管理系统已创建文章 A |
| 09:12:33 | 30 秒未收到响应,标记失败 | 响应丢失,文章 A 仍公开 |
| 09:13:03 | 无条件重试创建 | 内容管理系统又创建文章 B |
| 09:13:08 | 收到文章 B 的网页地址,继续发邮件 | 网站存在两个同标题版本 |
| 09:31 | 客户报告链接内容不同 | 才发现重复发布与版本分叉 |
问题不是“超时时间太短”,而是系统把“调用方没收到确认”误写成“外部动作没发生”。网络断开、执行器崩溃、人工双击、定时任务重叠和供应商延迟都可能制造这种不确定状态。
重建后,每期简报先建立持久任务和操作记录;内容管理系统超时后先按业务键查询,不直接重建;内容审批绑定具体草稿;邮件发送另有受众哈希值和第二道审批。48 个故障用例全部收敛后,系统连续运行 30 期:28 期完成,1 期因来源缺失而阻塞,1 期在发信前回退网站并完成补偿;3 次内容管理系统响应丢失和 2 次邮件响应丢失都没有造成重复外部动作。
本文为虚构教学案例。数字用于展示可靠性设计,不代表任何内容管理系统、邮件平台或行业表现;涉及付款、法律承诺、健康、安全、身份和不可逆客户权益时,应由有经验的工程、安全、隐私或法律人员设计与审阅。
先画边界和不变量:状态机要保护业务承诺,不是给流程多加颜色
周予把动作分成读取、内部写入、可撤回外部写入和不可完全撤回动作。来源抓取失败可以重试;保存快照需防重复;网站版本通常可回退;邮件一旦送达不能真正撤回,只能停止后续发送或更正。不同副作用决定不同状态与恢复策略。
| 不变量 | 必须始终成立 | 证明位置 |
|---|---|---|
| I01 | 一期、语言、内容类型只有一个任务 | 任务键唯一约束 |
| I02 | 发布内容必须与获批版本一致 | 审批与发布回执 |
| I03 | 一项发布意图至多一个内容管理系统对象 | 操作键与外部查询 |
| I04 | 未公开验证不能进入邮件审批 | 状态转换守卫 |
| I05 | 邮件受众与正文变化后旧审批失效 | 受众与内容哈希值 |
| I06 | 成功必须有远端可查询回执 | 文章与活动回执 |
| I07 | 状态未知绝不等于失败或成功 | 操作状态枚举 |
| I08 | 补偿不删历史事实 | 仅追加事件/收据 |
不变量先于状态名。若流程可以从“有草稿”直接跳到“发信成功”,无论看板多漂亮都不可靠。状态机的工作是阻止非法承诺,并保存恢复所需证据。
每条不变量都要有破坏测试。临时移除发布守卫,让“等待内容审批”直接提交内容管理系统,I02 测试必须失败;把发送的受众哈希值改一个字符,旧批准必须被拒;在操作表中预先放入第二条相同操作键的记录,唯一约束必须阻断。只在文档里写“不应该重复”,却没有执行层控制,事故发生时仍会失守。
不变量之间也有顺序:I02 和 I03 保护发布发生前与发生中,I06 约束“完成”声明,I08 保护恢复记录。周予把每个外部动作的先决条件、影响和后置条件并排写清:调用前有批准,调用中有操作记录,调用后有远端回执;缺任何一段都不能推进。
每次运行先建立任务、步骤和操作三类持久记录
任务描述整期目标,步骤记录内部阶段,操作专门记录一次可能产生外部副作用的意图。浏览器窗口、聊天记录和平台通知都不能替代这三类持久状态。
| 记录 | 关键字段 | 本例 |
|---|---|---|
| 任务 | 任务编号、任务键、状态、状态版本 | 简报-2026-W30、版本 17 |
| 输入 | 来源快照、缺失、截止 | 12/12、07:55 |
| 产物 | 路径、哈希值、结构版本 | 草稿 D-8f3c |
| 步骤 | 名称、尝试、开始/结束、错误 | 检查链接尝试 2 |
| 操作 | 操作键、意图哈希值、远端编号、状态 | 发布…、18427、已确认 |
| 批准 | 范围、产物哈希值、执行者、过期 | 内容 D-8f3c、周予 |
| 租约 | 负责人、租约截止时间、心跳 | 工作节点 02、09:15 |
| 事件 | 序列、发件人/收件人、原因、证据 | 41、发布→已发布 |
任务状态是业务事实来源;步骤日志解释运行细节;操作解决“请求发出后到底发生了什么”。把三者塞进一个最近错误字符串,会丢掉并发、重试与对账所需信息。
这些记录有不同的保留策略。任务、批准、外部操作和补偿证据随客户交付记录保存;体量很大的调试日志按较短周期清理;来源内容按授权处理。如果原文不能长期保留,可在授权范围内保存不可逆哈希值,用来证明当时使用的是哪个版本。运行表不直接复制 9 项事实正文或 63 个邮件地址,避免为了观察系统而扩大敏感数据面。
写状态使用一个命令处理器:读取任务与版本,验证事件在当前状态合法,核对所需回执,在同一本地事务中追加事件并更新状态/版本。若事件保存成功但状态更新失败,事务整体回滚;若外部系统不在同一事务内,则由操作记录跨越这个边界。这样“记录说发布过但任务仍在草稿”可以被对账发现。
状态转换必须列出事件、前置条件、保存证据和非法路径
主路径为 已创建 → 收集 → 起草 → 检查 → 等待内容审批 → 内容已批准 → 发布 → 已发布 → 等待发送审批 → 发送 → 成功。等待重试、已阻塞、补偿和已补偿是分支;状态不是由页面按钮随意覆盖,而由守卫验证后追加事件。
| 当前 | 事件 | 下一状态 | 守卫/证据 |
|---|---|---|---|
| 已创建 | 执行器已领取 | 收集 | 租约有效 |
| 收集 | 来源已冻结 | 起草 | 12项或有批准缺失说明 |
| 起草 | 产物已生成 | 检查 | 草稿校验和 |
| 检查 | 硬检查已通过 | 等待内容审批 | 9 事实、17 链接回执 |
| 等待内容审批 | 内容已批准 | 内容已批准 | 参与者 + 哈希值 + 过期时间 |
| 内容已批准 | 发布已开始 | 发布 | 操作键唯一 |
| 发布 | 远端已确认 | 已发布 | 文章编号与公开页探测 |
| 已发布 | 已请求发送审批 | 等待发送审批 | 统一资源定位符、受众哈希值 |
| 等待发送审批 | 发送已批准 | 发送 | 独立批准且未过期 |
| 发送 | 活动已确认 | 成功 | 活动编号与状态查询 |
| 任一执行态 | 无法恢复/需人工处理 | 已阻塞 | 原因 + 负责人 + 后续操作 |
| 已发布前后 | 已决定补偿 | 补偿 | 事件与计划 |
禁止路径包括“等待内容审批 → 已发布”“内容已批准 → 发送”“发布超时 → 已创建”和“已阻塞 → 成功”。恢复不是手工把状态改绿,而是补足证据后触发一条合法事件。
已阻塞也不是终点垃圾桶。记录阻塞原因枚举、人工负责人、所需证据、恢复事件和过期时间:缺来源恢复到收集,权限修复后重新执行原步骤,内容冲突回检查,审批过期则重新收集/检查而不是从已批准继续。不同原因共用一个“重试”按钮会绕过原守卫。
状态图还写终端语义:成功、已补偿和保护性关闭都不可由执行器继续;需要新意图时建立新任务版本并用取代连接。历史任务不改写为“最终成功”,否则无法解释当时是否已对客户产生影响。
用版本号、租约和心跳防止两个执行器同时推进同一任务
第 8 周事故也可能由两个定时器同时领取任务造成。任务行保存状态版本;更新时要求“当前版本必须为 16”,只有一个执行器能将它写成 17,另一个收到冲突后重新读取。领取时还建立短租约,执行器每 30 秒报告一次心跳,租约过期后才能被另一个执行器接管。
| 并发场景 | 错误做法 | 控制 |
|---|---|---|
| 两个调度器同刻触发 | 各建一条任务 | 任务键唯一约束 |
| 两个执行器同时领取 | 看见排队就都开始 | 版本比较更新与租约 |
| 执行器崩溃 | 任务永久运行中 | 租约到期 + 清理者 |
| 旧执行器恢复 | 继续写新状态 | 租约令牌/隔离版本拒绝 |
| 人工与执行器同时改 | 最后写入优先 | 状态版本冲突 |
| 心跳延迟 | 立刻双执行 | 宽限期+远端操作对账 |
租约表示临时执行权,不表示外部动作没有发生。新执行器接管发布状态时,第一步读取操作并查询内容管理系统,而不是因租约换人就重发请求。
隔离令牌随每次租约递增,并传给支持条件写入的内部工具。执行器 01 拿到令牌 7 后暂停,工作节点 02 接管并获得令牌 8;即使执行器 01 稍后恢复,它的内部写入也会因令牌过旧而被拒。无法接收令牌的外部内容管理系统,则依靠操作键、远端查询和本地操作记录保护,不能把租约误当成跨系统锁。
清理程序不会在租约刚过期时同时启动多个接管。它先用原子比较更新把负责人从旧令牌切到新令牌,保存恢复来源和最后一次心跳,再根据任务状态选择恢复入口。起草阶段可以重新生成;发布和发送阶段只能先对账。同样是“执行器失联”,所在状态不同,恢复动作也不同。
幂等键标识业务意图,不是给每次网络请求生成一个新通用唯一标识符
本例的任务键可以读作“周报:2026 年第 30 周:中文”。发布操作还要加入内容哈希值;邮件操作还要加入公开网页地址、受众哈希值和正文哈希值。同一意图的重试必须复用同一个操作键。参数变了却沿用旧键,应返回“意图不一致”;重试时生成新键,服务端就无法知道它们属于同一次业务动作。
| 操作 | 操作键组成 | 同键允许 | 必须新键 |
|---|---|---|---|
| 快照 | 来源 + 截断 | 同一抓取重试 | 截止改变 |
| 发布 | 任务键 + 草稿哈希值 | 超时后查询/重试同内容 | 获批内容变化 |
| 公开更新 | 文章编号与草稿哈希值 | 同版本重复提交 | 新网站版本 |
| 发送 | 任务键+网页地址+受众哈希值+正文哈希值 | 状态查询/安全重试 | 受众或正文变化 |
| 补偿 | 事件编号与操作 | 失败后继续同一补偿 | 补偿决定改变 |
RFC 9110 将幂等方法解释为:重复发送相同请求,服务端的预期效果应与只发送一次相同;它也提醒,若无法确认原请求未生效,就不要自动重试非幂等请求。业务接口仍要按自己的意图设计,HTTP 方法名本身不能替代唯一约束、参数比较和结果保存。
键还有作用域与寿命。第 30 周的中文简报与英文简报是两个任务键;同一期重新发布新内容,因为草稿哈希值不同,也构成新的发布意图;但单纯的网络重试必须沿用旧键。外部服务若只保留键 24 小时,48 小时后到达的延迟请求可能不再被去重。因此本地操作记录要长期保存远端编号,超出服务窗口后禁止自动重提。键的过期策略是接口合同的一部分。
同键参数冲突不能“以第一次为准”静默成功。若第一次请求D-8f3c,第二次误带D-91aa,系统返回意图不一致并同时保存两组哈希值供调查;否则调用方可能误以为新内容已发布。随机通用唯一标识符只保证碰撞少,不表达同一次业务意图,重跑时生成新通用唯一标识符反而破坏幂等。
完整走一遍:创建已经成功但响应丢失,怎样收敛到文章 18427
第 30 周的内容 D-8f3c 获批后,系统先把操作状态写为“已准备”,再发送带操作键的创建请求。30 秒后连接断开,操作改为“状态未知”而不是“失败”,任务也停留在发布阶段。恢复执行器按操作键查询内容管理系统,找到文章 18427;确认外部键、草稿哈希值和任务编号一致后,才保存远端编号并探测公开页面。
已准备 → 请求已发送 → 状态未知
→ 按操作键
→ 找到文章 18427
→ 核对意图哈希值 + 内容哈希值
→ 已确认 → 公开页探测 → 已发布
| 对账结果 | 处理 |
|---|---|
| 找到 1 条且哈希值一致 | 回填远端编号,继续探测 |
| 找到 1 条但哈希值不同 | 冲突,已阻塞,不覆盖 |
| 找到 2 条 | 记录事件,冻结发布并人工选择 |
| 未找到但平台仍可能处理 | 等待 2 分钟再查,不立即重建 |
| 明确未找到且请求可安全复用键 | 在预算内重试同意图 |
| 查询接口不可用 | 保持状态未知并告警 |
状态未知必须被当成正式状态。只有外部证据,或语义明确的幂等重试,才能消除它;不能靠“通常 30 秒没回就是失败”来猜。
时间线继续到邮件:09:14 的前两次公开探测都读到旧缓存,第 3 次才在 09:14:42 看到文章 18427 和内容 D-8f3c,任务由此进入“已发布”。09:18,周予批准发送;09:19:05,邮件平台接受活动,但响应再次丢失。系统没有新建活动,而是用本地保存的活动创建键查询,并在 09:20:11 找到活动 930。受众哈希值与正文哈希值一致后,才保存远端编号。两次“状态未知”遵循同一原则,但查询字段、等待窗口和完成证据并不相同。
对账回执要保存查询时间、查询参数摘要、返回对象数、选定的远端编号、意图比较结果和操作者。若外部搜索采用最终一致性,第一次“未找到”不能当作“绝对不存在”,还要按照平台已知的可见延迟等待。既没有可靠查询、创建动作又不具备幂等语义的系统,不适合自动重试高影响写入,应停在人工操作。
错误分类先决定动作:瞬态、永久、业务、状态未知和人工不是一回事
| 类别 | 例子 | 自动重试 | 下一步 |
|---|---|---|---|
| 瞬态 | 域名解析失败、429、短暂 5xx | 有限 | 退避与抖动 |
| 永久输入 | 字段结构错误、统一资源定位符非法 | 否 | 已阻塞,修输入 |
| 鉴权/权限 | 令牌过期、403 | 否 | 已阻塞,更新授权 |
| 业务 | 9 项事实中 2 项无来源 | 否 | 人工删主张或补资料 |
| 未知副作用 | 提交后超时 | 不盲重试 | 查询与对账 |
| 人工决策 | 同级来源冲突 | 否 | 等待或阻塞 |
| 不变量违背 | 同操作键出现两个对象 | 否 | 记录事件并冻结 |
同一个 HTTP 500,在只读抓取中可能只是瞬态错误;在创建请求发出后响应丢失时,却代表副作用未知。错误分类必须绑定具体操作,不能只按状态码全局处理。
适配器负责把平台原始错误映射为内部类别,并保存原始代码供审计。例如,内容管理系统返回 409 时,如果同时返回同一外部键对应的对象,它就是待核对候选;如果标识符已被别的任务占用,则属于业务冲突。邮件平台返回 429 时,可以按照响应中的建议等待时间重试;但如果邮件已被接受,只是后续查询遇到 429,发送操作仍然是状态未知,不能重新发信。映射表要按工具版本测试,不能让大模型临场阅读错误文字,再决定高影响动作。
每个类别有负责人:瞬态由执行器,输入由内容负责人,认证由账户负责人,业务由周予,不变量违背由事件负责人。最近错误写错误类别、工具、操作、尝试、原始记录和下一步动作;一句“调用失败,请重试”既无法路由责任,也无法判断能否安全重试。
重试策略要有范围、间隔、抖动、上限和总预算
周予为每个工具分别写策略,而不是统一规定“失败重试 3 次”。来源抓取在 20 秒调用超时后最多尝试 3 次,等待约 30 秒、2 分钟和 10 分钟,并加入小幅随机抖动;遇到 429,优先遵循服务端给出的等待提示。AI 草稿的结构错误只修一次;事实检查失败不靠重新生成碰运气;内容管理系统或邮件处于未知状态时,先查询。
| 步骤 | 最大尝试次数 | 预算 | 重试前 | 已耗尽 |
|---|---|---|---|---|
| 源获取 | 3 | 15 分钟 | 分类、缓存、服务端等待提示 | 缺失或被阻止 |
| AI 草稿结构 | 2 | 5 分钟 | 返回结构错误 | 已阻塞 |
| 链接读取 | 2 | 3 分钟 | 区分超时与 404 | 状态未知或不通过 |
| 内容管理系统创建 | 2 个提交上限 | 8 分钟 | 必须先完成操作查询 | 被阻止且状态未知 |
| 公开探测 | 6 | 3分钟 | 只读检查 | 保留旧版 |
| 邮件创建或发送 | 不盲目重建 | 10 分钟 | 活动查询 | 被阻止且状态未知 |
| 补偿 | 3 | 20 分钟 | 查当前远端状态 | 人工事件 |
重试层不能叠加失控:开发工具包 3 次 × 应用 3 次 × 工作流 3 次,会把一次业务意图放大成最多 27 次外部调用。配置时要盘点所有重试层,只保留一个可观察的总预算。无限重试既会拖延任务,也可能加重依赖方的故障。
预算按任务累计,而不只按单个步骤。12 个来源若各自重试 3 次,极端情况下会产生 36 次请求,并把任务拖过 10:30。因此,收集阶段设置 15 分钟总截止时间和 3 个并发上限。第一个来源原计划等待 10 分钟,也可能被任务截止时间截断;此时策略返回“预算耗尽”,而不是继续跑完整个重试序列。可靠性设计必须服从业务时效。
抖动用于避免多个运行同刻重试,不代表等待随意。回执保存策略版本、基础延迟、服务器提示、实际延迟和尝试。若7天重试率升至基线2倍,先查依赖和负载,不通过提高最大尝试次数把告警压下去。电路开启时新请求快速失败并进入可见被阻止/重试等待,给下游恢复空间。
超时至少有调用、步骤、任务、租约和审批五种时钟
| 超时/截止日期 | 本例 | 到期含义 | 处理 |
|---|---|---|---|
| 呼叫超时 | 获取 20 秒、内容管理系统 30 秒 | 本次等待结束 | 分类,不推断副作用 |
| 步骤截止日期 | 收集 15 分钟 | 阶段超预算 | 保存进度,被阻止/重试等待 |
| 任务截止日期 | 周一10:30 | 本期时效不足 | 不再自动发送 |
| 租约到期 | 90 秒无心跳 | 执行器可能失联 | 新工作节点隔离接管 |
| 内容审批 | 24 小时 | 来源或草稿可能过期 | 回到收集或检查 |
| 发送审批 | 2 小时 | 网页地址或受众可能变化 | 重新验证再审 |
超时不是远端动作已经取消的证据。客户端停止等待后,内容管理系统或邮件平台可能仍在继续处理;因此,调用超时只改变本地掌握的信息。任务截止时间则代表业务决定:超过 10:30,宁可人工通知或延期,也不能为了“完成自动化”而发送未经核对的内容。
截止日期沿调用链向下传,内部步骤不能各自拿满默认超时。09:58开始公开探测时,距离任务截止日期只剩32分钟;发送复核至少保留10分钟人工窗口,系统可在10:20停止自动路径。审批计时使用可靠服务器时间并记录时区,不能用浏览器本地时钟决定过期。
超时后还要区分“请求取消”和“已经取消”。纯内部渲染可以取消并清理候选项;对外部发布发送取消请求,本身也只是一项新操作,必须查询是否生效。用户点击“取消”表示改变了意图,不代表远端已经恢复原状;界面应分别显示“已请求取消”“已确认取消”或“状态未知”。
审批必须绑定对象、证据、范围和有效期,不能只存一个“已批准”
内容审批包展示 12 个来源快照、9 项事实及其位置、17 个链接、警告、草稿哈希值和计划执行的外部动作。批准记录写明执行者、授权范围“发布”、哈希值 D-8f3c、检查回执 C-441 和过期时间。正文任何字节或关键元数据发生变化,旧审批都不能再用于发布。
| 批准 | 绑定 | 不授权 |
|---|---|---|
| 内容发布 | 草稿哈希值、检查、目标内容管理系统与别名 | 发邮件 |
| 发送 | 公开网页地址、正文哈希值、63 人受众哈希值 | 改受众或正文 |
| 已接受的警告 | 指定警告编号与理由 | 接受其他警告 |
| 补偿 | 事件、远端对象、动作 | 删除审计记录或重发邮件 |
发布后第二道发送审批先访问公开统一资源定位符,确认页面哈希值、标题、17个链接与预期,再显示63名受众的分组和抑制结果。把“发布并通知”合成一个按钮会让网页错误在人工看到之前传播。
审批可以撤销。周予若在发布前发现来源更正,就记录“审批已撤销”事件;守卫要确认审批尚未撤销,且产物仍与获批版本一致。若发布已经执行,撤销不能假装请求从未发生,任务应转入状态未知或补偿,并先查询远端。批准界面因此要显示操作是否已经开始,以及此刻撤销可能产生的实际效果。
代理审批、批量批准和永久批准不用于首版。周予若无法在24小时内审,任务过期;系统不会因“这类周报以前都批”自动延续许可。审批时间是重要运行成本,最近 30视图分别统计等待内容与发送复核,避免把人工瓶颈误诊为模型慢。
来源、草稿和检查产物都要冻结,批准后重新生成必须重新审
12 个来源分别保存网页地址、抓取时间、内容哈希值和许可范围;抓取失败时,不能让 AI 凭记忆补事实。事实表中的 9 项声明全部有来源位置,17 个链接的检查回执区分 200、重定向、超时和已阻塞。草稿、事实表、链接报告和渲染页面共同组成审批内容包。
| 包项 | 版本证据 | 硬性放行门槛 |
|---|---|---|
| 来源快照 | 12 个哈希值与截止时间 | 缺关键来源则阻塞 |
| 事实表 | 9 组声明与证据 | 缺少证据支持的主张为 0 |
| 草稿 | 内容哈希值 | 与批准一致 |
| 链接 | 17 个最终网页地址与检查时间 | 损坏为 0;状态未知转人工 |
| 渲染 | 公开预览哈希值 | 标题、正文和引用存在 |
| 发布说明 | 任务、操作和审批编号 | 可追到一次运行 |
AI若在内容管理系统写入前“顺手润色”一个段落,哈希值变化就回到检查和等待内容审批。批准保护的是具体产物,不是对主题的泛化许可。
计算哈希值前先规范化明确可忽略的元数据,例如构建时间不进入内容哈希值,但标题、正文、引用、链接和语言都进入;否则每次导出产生新哈希值导致无意义重审,或反过来漏掉重要变化。哈希规则本身有版本,批准记录“哈希规则版本=2”,升级算法时不能直接比较旧值。
来源在审批后变化也会使包过期。系统不持续联网偷偷替换12个快照,而是保存截止;若监控发现关键来源更新,生成源变更警告,由周予决定是否回收集。已批准事实不会因网页后来改变而被历史重写。
外部写入先记录意图,再执行,再保存远端编号和验证回执
发布与发送都使用意图日志。先在本地事务中创建已准备的操作并关联任务;执行器领取后才调用外部平台;响应成功先保存远端编号,再推进任务。若进程在外部成功和本地保存之间崩溃,恢复可按操作键查询,而不是丢失关联。
| 操作状态 | 说明 | 可否再执行 |
|---|---|---|
| 已准备 | 意图已持久化,尚未调用 | 可由单一租约执行 |
| 执行中 | 请求已发,结果未定 | 不并发重发 |
| 状态未知 | 连接/执行器丢失 | 只查/对账 |
| 已确认 | 远端编号与意图匹配 | 返回原结果 |
| 已拒绝 | 远端明确未开始且输入错误 | 修输入后新意图 |
| 已补偿 | 原副作用已有补偿 | 不伪装未发生 |
外部平台即使原生支持幂等键,本地仍要保存自己的操作记录。Stripe 当前接口文档说明,其幂等请求会按键保存首次结果,后续相同键返回相同结果,并对参数不一致作出约束;这是特定产品的语义。使用任何平台,都应核对键的保留期、参数比较、并发和错误结果规则,不能假设所有接口都一样。
“已准备”的操作与任务转换在同一个本地事务中提交,目标类似事务性出站队列:不允许任务已经进入发布,却找不到待执行意图。执行器按操作编号领取,并标记为执行中。外部结果无法与本地数据库原子提交,所以仍要保留状态未知和对账路径;出站队列能减少丢请求,却不能消灭跨系统的不确定性。
操作不可变意图与可变执行分开:操作键、意图哈希值、目标固定;尝试、状态、远端编号和最近观察更新并留事件。人工不得在同操作里换标识符或受众来“修复失败”,因为那已是新意图。仪表盘可据此区分同一意图的多次尝试与两个不同业务动作。
邮件是无法回退点:状态查询、受众抑制和更正都要单独设计
发送操作键包含任务、公开网页地址、邮件正文哈希值和受众哈希值。创建活动后先保存活动编号;真正发送前,再检查抑制名单和 63 人计数。响应超时时按活动编号或操作键查询,绝不另建活动。提供商“已接受”不等于邮件“已送达”,回执要分别记录已接受、已送达、退信和投诉。
| 时间点 | 可撤回能力 | 事故动作 |
|---|---|---|
| 活动草稿 | 可删除候选 | 修正后新哈希值和新审批 |
| 已接受未开始 | 视平台能力尝试取消 | 查询确认,不承诺成功 |
| 部分送达 | 不可整体撤回 | 停余量、确定受影响人 |
| 全部送达 | 不可撤回 | 更正/解释,由本人批准 |
| 退信 | 不盲目改用其他地址重发 | 核对客户记录与授权 |
第 30 周最终回执为已接受 63、已送达 62、已退信 1;成功只表示计划动作已经完成并留下状态记录,不等于 63 人都阅读了邮件。退信进入人工客户维护队列,不触发智能体自行猜测邮箱。
受众哈希值由排序后的收件人编号和抑制名单版本生成,不把完整地址写进批准记录。发送前重新计算:若有人退订,哈希值变化会使旧审批失效;即使受众人数减少,也不能自动沿用许可,因为授权范围已经改变。重新审批页要突出新增、移除及其原因;任何新增收件人都需人工确认。
邮件状态轮询也有上限,不能让执行器一直等待所有供应商的最终状态。平台接受活动后,任务可以记录为“发送结果待确认”并释放租约,再由定时任务轮询;30 分钟后仍有待定项,本例会改为“等待观察”并通知人工查询,而不是提前记为成功。业务状态名必须与对客户的承诺一致,不能为了让完成率好看而提前成功。
回滚、补偿、对账和转发修复是四种不同恢复
代码部署错误可把应用产物回到上一版,称回滚;网站内容可把当前版本指回已批准旧版,但已产生的统一资源定位符访问/缓存仍需处理;邮件只能补偿;状态未知先对账;数据迁移若不可安全回退则转发修复。一个“撤销”按钮无法覆盖这些语义。
| 副作用 | 首选恢复 | 所需证据 |
|---|---|---|
| 内部草稿 | 丢弃候选项 | 来源未改哈希值 |
| 内容管理系统新版本 | 指回上一正确版本并验证 | 文章与版本编号 |
| 重复文章 | 冻结、选择规范版本、重定向或下架 | 两个远端编号与访问记录 |
| 邮件错误 | 停止剩余并修正 | 活动与收件人状态 |
| 客户数据写错 | 补偿事件或转发修复 | 前后及负责人 |
| 未知发布 | 查询/对账 | 操作键 + 远程搜索 |
Microsoft Azure 架构中心的补偿事务模式强调补偿是业务特定、可能失败、需记录进度并可恢复,而且不一定把系统恢复成初始状态。本文借用这一分布式恢复思想;63人邮件案例仍需按实际服务能力和客户沟通责任处理。
补偿计划逐项记录原操作、当前远端状态、补偿动作、先决条件、负责人和验证方法。例如,在把文章 18427 回退到版本 v29 前,先确认当前仍是错误的 v30;若他人已发布 v31,直接指回 v29 会覆盖新工作,必须转人工或采用向前修复。补偿不能无条件执行“反操作”。
补偿自身使用独立幂等键,也必须能够暂停后恢复。步骤 1 关闭自动发送,步骤 2 切回网站,步骤 3 验证公开页面哈希值,步骤 4 处理缓存和重定向,步骤 5 决定如何沟通;若步骤 3 失败,状态继续保持“补偿中”,已完成的步骤不重复产生副作用。恢复完成后记为“已补偿”,原发布事件仍然保留。
事件记录、告警和三个日常视图让一个人看得懂自动化现在在做什么
事件日志只追加、不改写:序列、任务、操作、发件人与收件人、执行者、时间、原因和证据位置。日志不写完整客户内容或令牌。指标从事件和操作记录中计算,不能用聊天摘要猜运行历史。
| 视图 | 显示 | 立即关注 |
|---|---|---|
| 今日操作 | 待复核、已阻塞、补偿 | 负责人、下一步和过期时间 |
| 卡住或未知 | 租约过期、操作未知、步骤超时 | 外部对账,不重跑 |
| 最后 30 运行 | 完成、恢复、重试、人工时长、成本 | 漂移与重复副作用 |
| 告警 | 阈值 | 自动安全动作 |
|---|---|---|
| 重复远端对象 | 第 1 次 | 冻结任务并禁用写入 |
| 未批准哈希值发布 | 第 1 次 | 回退候选并记录事件 |
| 未知操作超过 8 分钟 | 1 次 | 通知负责人并保持不重发 |
| 重试次数上升 | 达到 7 日基线的 2 倍 | 降速并打开熔断器 |
| 审批已过期 | 到期 | 禁止推进 |
| 任务已过 10:30 | 1 | 禁止自动发信 |
成功率不是唯一指标;“失败可见、状态不乱、外部副作用不重复、能在预算内恢复”更接近可靠性的工作定义。
指标分母必须明确。重试率按工具调用计算,还是按任务计算,会得出不同结论;本例同时记录 19/360 次来源调用和受影响的运行数。未知时长从“请求已发送”算到“已确认”或“已锁定”;审批等待从进入等待状态算到批准或过期;平均恢复时间从事件确认算到所有不变量恢复,不能把告警前的潜伏时间丢掉。
告警必须带着处置所需的信息。重复远端对象消息要包含任务、操作键、两个远端编号、哈希值、最近一次批准和冻结结果;只发一句“检测到异常”,会迫使周予在压力下重新调查。每月演练一次未知发布,每季度演练一次补偿;若运行手册离开作者的记忆就无法执行,应视为失败。
用 48 个故障场景验收状态转换和恢复,不只跑一次正常演示
| 系列 | 案例数 | 示例 | 必须结果 |
|---|---|---|---|
| 正常与状态守卫 | 8 | 合法主路径、非法跳转 | 仅合法事件生效 |
| 重试与错误 | 10 | 域名解析、429、结构错误、403 | 按类别处理 |
| 幂等与状态未知 | 10 | 内容管理系统或邮件响应丢失、延迟请求 | 0 个重复副作用 |
| 并发与租约 | 8 | 双触发、执行器崩溃、旧租约恢复 | 单一推进 |
| 审批版本 | 6 | 哈希值或受众变化、审批过期 | 旧批准失效 |
| 补偿 | 6 | 网站回退、重复对象、更正失败 | 可恢复或人工事件 |
| 合计 | 48 | — | 所有不变量保持 |
测试同时检查任务状态、事件、操作记录和模拟远端对象数。只断言最终成功,会漏掉“先发了两次再删一条”这种严重的过程错误。故障注入在非生产环境执行;涉及邮件时使用沙盒收件组,不能拿 63 名真实客户做演练。
48 个案例还要冻结预期事件序列。例如,内容管理系统状态未知的案例应依次出现“发布已开始 → 请求已发送 → 状态未知 → 开始对账 → 远端已确认 → 已发布”,且创建调用次数必须为 1。若实现直接从状态未知跳到已发布,缺少查询证据,即使远端对象数最终为 1 也算失败。并发案例让两个执行器交错执行,用来验证旧隔离令牌不能再写状态。
修复一项失败后,要重跑全部 48 项,并保留三类非故障基线:正常运行不应因可靠性控制而明显变慢;人工审批不能在超时后被自动视为批准;监控故障不应导致客户内容写入调试日志。恢复功能本身也可能制造新风险,不能只测试事故分支。
30 期运行账本:恢复成功不等于没有问题
30 期共计划 360 次来源读取。期间出现 19 次瞬态错误:6 次域名解析失败、8 次超时、5 次速率限制;18 次在预算内恢复,1 次关键来源持续缺失,任务阻塞且没有发布。6 次 AI 结构错误均在一次修正内通过;3 次事实证据失败全部转人工,没有盲目重新生成。
| 结果 | 运行/操作 | 结果 |
|---|---|---|
| 首路径完成 | 24 期 | 无重试或人工异常 |
| 恢复后完成 | 4 期 | 内容管理系统未知 3 次,另有其他瞬态组合 |
| 已阻塞 | 1 期 | 缺关键来源,0 次发布、0 封邮件 |
| 已补偿 | 1 运行 | 网站预览错误,发信前回上一正确版本 |
| 邮件状态未知 | 2 次操作 | 查询找到原活动,0 次重建 |
| 重复文章或活动 | 0 | 不变量保持 |
| 合计 | 30 期 | 28 期成功、1 期阻塞、1 期补偿 |
网站已补偿的那一期没有计入成功,也没有发送邮件;修正来源后创建新的任务版本,而不是改写历史状态。30 期只是小规模案例证据,不能证明未来没有故障;它能证明的是,设计中预期的几类故障可以留下证据并收敛。
28 期成功中,24 期没有异常,4 期至少经历一次可恢复事件。因此,“完成率 28/30”不能说明恢复设计是否有效;还要分别检查 5 个未知操作是否都在预算内完成对账、重复计数是否为 0、未批准发布是否为 0,以及人工接管时是否拿到了完整证据。已阻塞与已补偿说明保护机制生效,不能全都藏进一个模糊的失败数里。
运行账本还记录人工时间:内容审批中位数7分钟、发送审批3分钟、异常对账最长11分钟。自动化节省未在本文计算,因为没有重建人工基线;可靠性文章不把“30期跑完”顺便包装成效率收益。
把事故运行手册写成查询—冻结—判断—恢复—验证—沟通,而不是“重启试试”
| 步骤 | 操作 | 停止/证据 |
|---|---|---|
| 1 识别 | 任务、操作、远端编号和当前状态 | 找不到编号不执行写操作 |
| 2 冻结 | 停调度器、撰写人和自动重试 | 记录冻结时间 |
| 3 对账 | 查内容管理系统或邮件活动的实际对象与状态 | 保持状态未知直到有证据 |
| 4 决定 | 继续、回滚、补偿、转发修复 | 负责人批准高影响动作 |
| 5 执行 | 使用独立补偿操作键 | 保存每步回执 |
| 6 验证 | 不变量、公开页、受众、哈希值 | 未通过不宣布恢复 |
| 7 沟通 | 受影响人、事实、下一步 | 不推测未确认原因 |
| 8 学习 | 故障案例、工具或策略变更 | 全部 48 个案例回归 |
演练场景是内容管理系统出现两条使用同一操作键的对象。系统自动冻结,不删除其中任何一条;人工比较哈希值和访问记录,选择 18427 作为规范版本,将另一条下架并记录重定向与缓存处理,同时确认邮件尚未发送。运行手册在 16 分钟内完成技术处置,客户沟通另由周予决定。若补偿步骤本身失败,状态继续保持“补偿中”并告警,不能手工改成正常。
演练结束后再做一次证据盘点:两个远端对象仍可由事件追溯;规范页面的哈希值与获批的 D-8f3c 一致;重复网页地址不再公开返回冲突内容;调度器和撰写程序只在负责人确认后恢复;下一期使用新的任务键。少一项都不能关闭事件。“用户现在看不见错误”不代表内部一致性已经恢复。
复制这份最小可靠运行包,从一个会写外部系统的步骤开始改
最小包包含任务结构、状态转换表、不变量、操作与幂等表、错误与重试策略、审批方式、超时表、补偿矩阵、事件日志、仪表盘和故障集。先选择现有自动化中最危险的一项外部写入,不必一次重建全部流程。
本案例参考了五类公开资料:RFC 9110 对 HTTP 幂等语义及非幂等请求自动重试边界的说明;AWS Builders' Library 对客户端请求标识、同一意图重试和延迟请求等接口设计问题的讨论;Microsoft 对有限重试、退避、抖动以及避免层叠和无限重试的建议;Azure 补偿事务模式对业务特定补偿、进度记录、幂等补偿和人工介入的说明;Stripe 幂等请求文档提供的一种具体接口实现。产品细节和键的保留期会变化,实际使用必须以目标服务的最新文档和测试结果为准。
- RFC 9110:HTTP Semantics,Idempotent Methods
- AWS Builders' Library:Making retries safe with idempotent APIs
- Microsoft:Recommendations for handling transient faults
- Microsoft:Compensating Transaction pattern
- Stripe API:Idempotent requests
今天先选一个会写入外部系统的动作,补齐五个答案:业务意图键是什么;请求超时后怎样查询实际结果;哪类错误可以重试、总预算是多少;审批绑定哪个具体哈希值;已经产生副作用后怎样补偿。任何一项只能回答“平台应该会处理”,就先停在草稿或人工批准,不让自动化直接写入生产系统。