第 3 章 生产系统治理、保障与技术债务:韧性架构的动态平衡
生产系统不会因为上线成功就自动可靠。需求、流量、依赖、数据和组织都会变化,系统若没有持续的约束与反馈,就会逐渐变得难以解释、难以变更、难以恢复。本章把生产可靠性定义为四个结果:关键业务可完成、结果正确、失败可恢复、过程可审计。
本章不讨论“永不出错”的理想系统,也不把某个中间件当作可靠性方案。可靠性是一个控制回路:先定义业务结果和可接受的风险,再用保护机制限制故障,用遥测发现偏差,用对账和补偿收敛状态,最后把事故经验固化为治理规则。Google SRE 将 SLO 与错误预算用于平衡可靠性和变更速度,正是这种控制回路的经典表达。[1]
3.1 引言与全景框架
3.1.1 问题边界与共同模型
本章覆盖面向用户的在线服务、异步任务和带有支付、库存、优惠、供应商等外部依赖的交易系统。它关注的是生产中的失效和恢复,不展开容量规划、业务领域建模或安全纵深防御;这些主题只在与本章的故障边界相交处出现。
可靠性至少有四层:
| 层次 | 判断问题 | 不能用什么替代 |
|---|---|---|
| 可用性 | 用户能否完成关键动作? | 只看 CPU、Pod 存活或接口 200 |
| 正确性 | 金额、状态、库存是否符合不变量? | 只看请求成功率 |
| 可恢复性 | 部分失败后能否回到一致状态? | 只写重试脚本 |
| 可解释性 | 能否定位、审计并证明发生了什么? | 只保留一行错误日志 |
生产治理可以抽象为以下反馈环:
业务目标与风险
│ 定义 SLI/SLO、错误预算、数据不变量
▼
设计约束 ──► 保护机制 ──► 运行遥测 ──► 应急止血
▲ │ │ │
│ └──────► 失败记录、对账、补偿 ◄────┘
│ │
└──────────── 复盘、还债、规则固化 ◄────┘
技术债务会提高故障概率和恢复成本;保障机制可以降低爆炸半径,却可能掩盖债务;治理要把事故、演练和数据转化为长期约束。因此,判断一项工作时应同时问:它减少了哪种风险?它增加了什么复杂度?失败后谁负责收敛?
设计时可以先画一张“约束画布”,把抽象的稳定性目标翻译成可评审的工程问题:
| 维度 | 必须明确的约束 | 例子与证据 |
|---|---|---|
| 业务 | 哪些动作必须成功,哪些结果可以延迟? | 支付确认不可重复,推荐结果可以延迟;由用户旅程和损失评估证明 |
| 流量 | 平均、峰值、突发和热点如何分布? | 假设峰值是平时的 5 倍,热点 SKU 占请求的 20%;需要压测或生产测量 |
| 数据 | 谁是权威源,允许多长时间的不一致? | 支付账本是事实,搜索是投影;允许分钟级新鲜度但不允许金额不一致 |
| 资源 | 线程、连接、队列、磁盘和第三方配额的上限是什么? | 每个租户独立并发配额,批任务不能占满在线连接池 |
| 变化 | 什么能灰度、回滚或前向修复? | 代码和配置可回滚,数据库字段先扩展后收缩 |
| 组织 | 谁值班、谁批准高风险动作、谁关闭行动项? | 服务 owner、事故指挥者和数据 owner 分离但职责明确 |
容量数字如果只是方案假设,就必须标明“假设”;上线后要用实际 QPS、P99、队列长度和失败率替换它。稳定性不是把每个指标都做到最大,而是在业务约束下给资源、延迟和恢复能力分配预算。这样才能解释为什么结算链路获得独立连接池、为什么通知链路可以被降级,也能解释为什么一个看似无害的批处理会被暂停。
3.1.2 90 天治理路线图
数字是规划示例,不是通用标准。真正的目标应由业务损失、用户旅程和团队能力校准。
| 阶段 | 重点 | 验收证据 |
|---|---|---|
| 0–30 天:可见 | 识别关键旅程、权威数据源、依赖和债务 | SLI/SLO 草案、服务目录、事故时间线、债务登记表 |
| 31–60 天:可控 | 建立超时、限流、幂等、告警、回滚、DLQ 和对账最小闭环 | 保护矩阵、燃烧率告警、一次演练、一次差异收敛 |
| 61–90 天:可演进 | 将错误预算、变更评审、复盘行动项和还债预算纳入交付节奏 | ADR、灰度指标、行动项关闭率、DORA 趋势 |
DORA 的交付频率、变更前置时间、变更失败率和恢复时间适合观察交付系统的速度与稳定性,但它们是组织级反馈指标,不能直接替代某个业务 SLO。[2]
3.2 技术债务治理:管理摩擦而不是追求零负债
3.2.1 债务分类与利息
技术债务不是所有“不漂亮的代码”,而是为了当前收益接受了未来摩擦的设计或实现选择。Fowler 用“有意/无意 × 明智/鲁莽”描述债务的性质;该模型的价值不在贴标签,而在于帮助团队讨论偿还成本、收益和时机。[3] Kruchten、Nord 和 Ozkaya 则强调要识别债务项、评估影响、决定消除/降低/缓解,并把管理动作纳入发布计划。[4]
| 明智 | 鲁莽 | |
|---|---|---|
| 有意 | 为验证市场先使用单库,记录边界、利息和转向条件 | 明知没有幂等和回滚仍赶着上线,只用“以后补”掩盖风险 |
| 无意 | 需求和流量变化后才发现原设计不合适,属于学习成本 | 不理解约束而复制模式,造成耦合、数据污染和无人负责 |
债务的“本金”是重构或迁移工作量,“利息”是每次变更增加的耗时、事故概率、值班负担和恢复损失。优先级不应由代码观感决定,而应由下面的近似模型决定:
债务优先级 ≈ 影响范围 × 发生概率 × 利息增长率 × 恢复成本
÷ 处理成本
模型只用于排序,不是精确财务计算。一个可操作的债务登记项至少包含:影响旅程、触发条件、当前绕行、owner、利息信号、偿还方案、停止新增债务的门槛和复核日期。没有 owner 和日期的“技术债列表”只是愿望清单。
债务登记还要记录“暂不偿还”的理由。可以暂缓的债务通常满足三个条件:影响范围小、利息没有明显增长、存在可验证的运行护栏;一旦其中一个条件失效,就必须重新评审。例如,一个只读报表仍使用旧查询层,若访问量稳定且有查询超时,可以暂缓;如果它开始被在线结算复用,边界已经变化,就不能继续用原来的绿灯结论。
偿还方式也不只有重写。常见选择包括:
| 方式 | 适用前提 | 主要风险 |
|---|---|---|
| 消除 | 组件或流程已经没有业务价值 | 删除依赖时遗漏隐藏调用 |
| 降低 | 边界明确但实现过于复杂 | 重构只改善局部,没有降低系统耦合 |
| 缓解 | 暂时无法迁移,仍有稳定性护栏 | 护栏失效后债务利息突然暴露 |
| 接受 | 利息稳定且处理成本高于收益 | 业务增长使旧判断失效 |
每个债务项应有一个可观察的“利息指标”,比如构建耗时、变更回滚率、人工补偿量、慢查询比例、告警噪声或新成员上手时间。指标下降不等于债务消失,但能证明偿还动作确实改善了系统,而不是只改变了代码形状。
3.2.2 债务准入与偿还 ADR
允许借债,但必须让它可见、可限额、可回收。准入表可以把“现在能否上线”从口头争论变成证据判断:
| 决策 | 前提 | 必须补上的护栏 | 重新评估条件 |
|---|---|---|---|
| 绿灯 | 影响非核心路径,失败可降级且无数据损坏 | owner、监控、回滚开关 | 流量、金额或依赖数达到阈值 |
| 黄灯 | 影响核心路径,但有人工或自动补偿 | 幂等、审计、对账、值班预案 | 错误预算快速消耗或人工成本上升 |
| 红灯 | 可能重复扣款、丢失权威事实或无法回滚 | 暂停发布,先修复或隔离 | 只有验证完成后才解除 |
关键重构应写成 ADR,而不是只写“推荐方案”。例如把供应商同步从“业务库直接改写搜索表”迁移为“权威库 + Outbox/CDC + 可重放投影”:
| ADR 项 | 内容 |
|---|---|
| 背景 | 多个任务直接写读模型,失败后无法判断哪个版本是真实状态 |
| 候选 | 继续共享库、双写、Outbox/CDC、全量事件溯源 |
| 决策 | 以权威库为唯一写入源,Outbox 或 CDC 驱动投影,投影可重建 |
| 获得 | 事实可追溯、发布可重放、读写扩展独立、差异可对账 |
| 牺牲 | 多一套事件与版本管理;读模型存在延迟,不能宣称即时一致 |
| 已接受风险 | Relay 重复发布、CDC 延迟和历史脏数据迁移 |
| 验证指标 | 投影延迟、差异率、重放成功率、人工介入量 |
| 重评条件 | 事件吞吐、数据保留成本或一致性要求发生变化 |
线上重构优先采用可逆步骤:先建立抽象边界和双读比对,再影子写入或灰度投影;差异可解释后切流,保留回滚窗口,最后删除旧路径。直接停机重写通常把技术风险变成业务不可用风险。
3.3 韧性架构与保护机制
3.3.1 SLI、SLO 与错误预算
SLI 是观测,SLO 是内部目标,SLA 是对外契约。SLO 应围绕用户旅程定义,并同时覆盖速度、正确性、新鲜度和完整性;不要用组件健康替代业务结果。[1]
| 旅程 | SLI 示例 | SLO 示例(假设) | 降级/失败语义 |
|---|---|---|---|
| 浏览 | 基础信息成功率、P99 | 99.9% 在 1 秒内返回 | 推荐、营销标签可为空 |
| 结算 | 价格/库存校验成功率、P99 | 99.5% 在 2 秒内完成 | 非核心权益可延后,价格不能未知 |
| 创单 | 明确成功或失败率、P99 | 99.95% 在 3 秒内结束 | 不返回半成功;超时进入查询/对账 |
| 支付回调 | 合法回调处理延迟、重复率 | 99.99% 在 30 秒内幂等收敛 | 延迟回调必须可重放 |
| 数据发布 | 权威版本到读模型的延迟 | 99% 在 5 分钟内可见 | 旧版本可读,但必须标记水位 |
错误预算是 1 - SLO,但必须先定义排除项、统计窗口和错误分类。预算充足时可以发布和实验;消耗过快时收紧灰度、优先修复高利息债务;预算耗尽时暂停高风险变更,只允许修复和安全变更。[1]
错误预算要落到决策而不是仪表盘。团队可以把预算状态分为绿、黄、红三档,并为每档预先约定动作:
| 预算状态 | 观测信号 | 发布策略 | 治理动作 |
|---|---|---|---|
| 绿 | 短、长窗口燃烧率均低于门槛 | 正常发布,允许小规模实验 | 优先偿还高利息债务,验证新护栏 |
| 黄 | 短窗口明显燃烧或长窗口持续恶化 | 缩小灰度,暂停非必要高风险变更 | 事故指挥者复核依赖、容量和最近变更 |
| 红 | 预算耗尽或业务正确性指标越界 | 只允许止血、修复和安全回滚 | 冻结发布,建立恢复计划,完成复盘后再解冻 |
燃烧率应同时看短窗口和长窗口。短窗口能在突发事故中快速触发止血,长窗口能识别低强度但持续的退化;只看平均值会把尖峰和局部租户的损失稀释掉。SLO 的统计口径也要写清楚:是按请求数、用户旅程、金额、订单还是时间计算;超时、客户端取消、依赖不可用和已知维护是否分别计入;一条请求跨多个服务时,不能把各服务的百分比简单相加。
错误预算不是让团队故意消耗故障额度。它把可靠性投资和交付节奏放在同一张决策表中:预算足够不代表可以跳过幂等和回滚,预算不足也不代表只能停工。若业务正确性受到威胁,即使可用性预算尚未耗尽,也应立即冻结相关动作;若某个 SLO 长期稳定且成本过高,则可以通过 ADR 重新评估目标、采集方式和投入,但要保留历史基线,避免用改变口径制造“改善”。
3.3.2 故障模型与隔离边界
先定义系统会怎样坏,再决定保护措施。常见故障与第一防线如下:
| 故障 | 传播路径 | 第一防线 | 仍需解决的问题 |
|---|---|---|---|
| 慢依赖 | 下游慢 → 线程/连接池耗尽 | 分层超时、舱壁、熔断 | 已发出的请求是否仍占用下游资源 |
| 重试风暴 | 失败 → 同步重试 → 更拥塞 | 指数退避、Full Jitter、预算 | 操作是否幂等,是否有全链路上限 |
| 流量冲击 | 热点/爬虫 → 队列堆积 | 限流、排队上限、热点隔离 | 被拒请求的用户体验和恢复顺序 |
| 部分发布 | 新版本只在部分实例失败 | 灰度、自动回滚、版本标记 | 数据迁移是否向后兼容 |
| 异步不一致 | 事务成功、消息未发或重复 | Outbox、幂等消费、对账 | 权威事实和补偿 owner 是谁 |
| 人工误操作 | 错误配置/补偿扩大影响 | 审批、双人复核、审计、演练 | 紧急操作能否快速撤销 |
故障域不能只按服务名划分,还要按流量、资源、地域和数据分片划分。一个推荐接口变慢,如果和订单接口共享线程池、连接池及缓存实例,推荐故障就会变成交易故障;一个热点商品如果和普通商品共享库存锁,也会把局部竞争扩大成全局排队。故障域的目标不是让每个模块都独立,而是让影响边界可以预测、监控和切断。
| 故障域 | 典型隔离对象 | 设计动作 | 验证问题 |
|---|---|---|---|
| 业务域 | 浏览、结算、支付、履约 | 服务、数据、发布和权限边界 | 非核心域变慢会不会占满核心资源? |
| 流量域 | 普通、活动、爬虫、后台批任务 | 租户配额、独立队列、热点限流 | 一个租户能否耗尽公共配额? |
| 资源域 | 线程池、连接池、缓存、磁盘 | 舱壁、配额、独立实例或分片 | 资源耗尽后核心请求还能否运行? |
| 地域域 | 可用区、城市、跨地域链路 | 就近路由、区域熔断、灾备切换 | 切流是否会把故障带到第二个区域? |
| 数据域 | 租户、SKU、订单分片 | 分片键、热点拆分、读写隔离 | 一个热点分片是否阻塞全局扫描? |
依赖也要分级,否则所有错误都会被当成“服务不可用”:
| 依赖等级 | 失败后的业务语义 | 保护策略 |
|---|---|---|
| 强依赖 | 没有它不能安全推进 | 快速失败、冻结状态、查询或补偿 |
| 弱依赖 | 可以用较低质量结果继续 | 默认值、旧快照、隐藏模块、异步补齐 |
| 后置依赖 | 不影响当前响应 | Outbox、队列、有限重试和 DLQ |
| 观测依赖 | 只影响日志或统计 | 本地缓冲、采样,绝不阻塞主链路 |
例如结算时库存预占和价格快照是强依赖,推荐凑单和营销标签是弱依赖,积分发放和通知是后置依赖,埋点上传是观测依赖。依赖分级必须写入接口契约和 runbook,不能只存在于熟悉系统的工程师脑中。
高可用也不等于“多部署几个实例”。应用层需要无状态和快速替换,数据层需要复制、备份与恢复演练,消息层需要副本、积压水位和重平衡预案;每一种冗余都必须配合切换动作。没有演练过的备用链路,只能称为潜在容量,不能称为已验证的恢复能力。
3.3.3 韧性保护机制矩阵
Nygard 将稳定性模式集中在超时、熔断、舱壁、降级和可恢复变更等防御边界;AWS Reliability Pillar 也强调超时、限流、有限重试、快速失败和紧急开关。[5][6] 机制必须组合使用,单独增加重试往往只会放大故障。
| 机制 | 触发条件 | 防御目标 | 核心参数 | 反模式与补救 |
|---|---|---|---|---|
| 超时 | 超出本层 deadline | 释放调用方资源,避免级联阻塞 | 请求总预算向下游递减 | 全链路固定 5 秒;改为按用户旅程分配预算 |
| 重试 | 可恢复网络/过载错误 | 消除瞬时抖动 | 只重试幂等操作;次数和总时长封顶 | 无差别重试写请求;先用幂等键和错误分类 |
| 限流 | 速率、并发或队列超阈值 | 保护核心资源 | 令牌桶/并发上限/租户配额 | 只在网关限流;在热点和下游边界也设上限 |
| 熔断 | 依赖错误率或延迟持续超标 | 切断故障依赖 | 关闭、打开、半开;探测流量小 | 熔断后无限等待;必须有明确 fallback |
| 舱壁 | 资源池被不同流量共享 | 防止一个租户/依赖拖垮全局 | 线程、连接、队列按风险隔离 | 只隔离线程不隔离连接;需成对配置 |
| 降级 | 非核心依赖不可用或预算不足 | 保住核心业务结果 | 旧数据、默认结果、异步完成 | 把错误伪装成成功;返回状态必须可解释 |
| 幂等 | 超时、重复投递或人工重放 | 防止重复副作用 | 业务键、唯一约束、状态机 | 只在缓存去重;权威状态必须落库 |
| 变更护栏 | 灰度指标越界 | 限制发布爆炸半径 | 小流量、自动暂停、可回滚 | 只看平均值;同时看分位数和业务正确性 |
重试必须有全链路预算。Full Jitter 的一个最小实现如下;attempt、最大次数和总 deadline 需要由调用链共同决定,而不是每个 SDK 各自决定:
import random
def backoff_seconds(attempt, base=0.1, cap=3.0):
limit = min(cap, base * (2 ** attempt))
return random.uniform(0, limit)
AWS 对退避、抖动和最大重试次数的说明指出:仅有指数退避仍可能让大量客户端同时重试,因此要加入随机抖动并设置上限。[7] 对非幂等写操作,正确策略通常是返回可查询的处理中状态,由状态查询、Outbox 或人工队列收敛,而不是再次盲写。
超时必须按端到端预算分配。假设结算初始化总预算为 2 秒,不能让每个下游都使用 2 秒:商品校验可以占 150 毫秒,库存预检查 300 毫秒,计价 500 毫秒,营销试算 300 毫秒,地址运费 300 毫秒,聚合与序列化保留 200 毫秒。预算只是示例,实际值要由 P99、依赖等级和用户体验测量得到;入口 deadline 应向下游传递,子调用只能消耗剩余预算。
超时之后的语义也要区分:读请求可以返回旧快照,弱依赖可以异步补齐,强依赖写请求必须进入可查询的处理中状态。尤其要警惕“客户端超时但服务端已经提交”的不确定结果,此时直接重试可能重复扣款或重复创建订单,正确动作是查询业务状态、等待回调或进入对账。
幂等不是一个缓存键,而是一组持久化约束:
| 动作 | 幂等键 | 持久化约束 | 参数冲突处理 |
|---|---|---|---|
| 创建订单 | user_id + request_id | 唯一索引、请求摘要、状态机 | 相同键不同摘要直接拒绝 |
| 支付回调 | 渠道流水号 | 支付流水唯一、条件状态更新 | 已处理返回同一结果 |
| 消息消费 | event_id | 去重表或消费流水唯一 | 旧版本事件不覆盖新状态 |
| 释放库存 | reservation_id + action | 释放流水唯一、预占状态机 | 已释放视为成功 |
| 人工补偿 | 工单号或补偿批次 | 补偿记录、审批和审计 | 已执行只能查询,不能再次执行 |
中间件参数也属于保护机制。MySQL 连接池的 MaxOpenConns、ConnMaxLifetime、建连/读/写超时要和服务端 max_connections、空闲连接回收时间协调;Redis 的活跃连接上限、获取连接等待时间、读写超时和重试次数要与 maxclients、慢命令和热点键共同评估。评审至少检查三组关系:应用连接池总量小于服务端上限并留有恢复余量;应用连接生命周期不长于代理和服务端的断连时间;获取连接、建连、读、写各阶段都有上限。
| 现象 | 不能直接下的结论 | 应检查的证据 |
|---|---|---|
| MySQL 连接池打满 | 不一定是连接数太小 | 慢查询、长事务、锁等待、连接泄漏和请求排队 |
| Redis 获取连接超时 | 不一定是 Redis 宕机 | 慢命令、大 key、连接未归还、客户端池配置 |
| 偶发连接断开 | 不一定是网络随机抖动 | wait_timeout、代理空闲超时、连接生命周期和重试放大 |
| 队列持续增长 | 不一定是消费者数量不足 | 单条处理时长、毒消息、下游限流和分区热点 |
参数优化不能只盯吞吐。把连接池放大、重试次数提高、队列上限调高,短期可能让成功率变好,长期却可能把压力转移到数据库、消息代理或外部供应商。每个参数变更都要记录目标、影响边界、回滚方式和验证指标。
3.3.4 变更与演练
一次发布就是一次故障注入机会。生产准入至少检查:依赖是否有超时、核心写是否幂等、配置是否可审计、数据库变更是否向后兼容、灰度是否能自动停止、回滚是否不会覆盖新数据。混沌演练不追求制造戏剧性事故,而是验证“发现—止血—恢复—复盘”链路和 owner 是否真实存在。
变更风险应按影响范围而不是按代码行数分级。只改文案的变更可能是低风险,只改一行限流配置也可能让全站不可用。评审可以使用以下护栏:
| 变更类型 | 主要风险 | 必须具备的护栏 | 放量与停止条件 |
|---|---|---|---|
| 只读代码或展示层 | 延迟、渲染和客户端兼容性 | 版本标记、回滚包、基础指标 | 小流量灰度,错误率或 P99 越界即停 |
| 核心写逻辑 | 重复副作用、状态错迁移 | 幂等键、状态机、影子比对、前向修复方案 | 先内部流量,再按租户或分片放量 |
| 数据库结构 | 锁表、兼容性和回填压力 | 扩展后迁移、双读/双写窗口、回填限速 | 观察锁等待、延迟和数据差异 |
| 配置与规则 | 突发限流、计价或权限错误 | 配置版本、审批、自动校验、快速禁用 | 先小范围生效,业务正确性优先于吞吐 |
| 基础设施与依赖 | 区域级不可用、连接风暴 | 容量余量、切流预案、恢复演练 | 分区或可用区逐步执行,失败自动暂停 |
每次发布至少保留四条证据:变更前基线、变更时间线、灰度期间的业务对账结果、变更后的验证结论。回滚也不是无条件安全的:如果新版本已经写入新格式数据,直接回滚旧代码可能无法读取;如果新版本已经发送新事件,回滚消费者可能重复或丢弃事件。因此数据库迁移和事件协议要优先采用向后兼容,无法兼容时设计一次性的前向修复。
混沌实验可以从低危、可恢复的故障开始。实验单应写明假设、爆炸半径、注入方式、停止开关、预期信号、恢复动作和验收证据。例如“支付查询延迟升高时,订单服务应在总预算耗尽后进入处理中,不得重复发起扣款;五分钟内限流开关生效,恢复后差异项全部进入对账状态机”。实验失败不等于系统失败,真正失败的是无法观测、无法停止或没有 owner。阿里云的混沌工程实践同样把故障注入与稳定性验证、应急预案和持续改进结合起来。[19]
3.4 可观测性与 1-5-10 应急响应
3.4.1 从图表到可回答的问题
可观测性不是把所有日志搬进平台,而是能从输出推断内部状态,并回答“谁受影响、哪条路径变慢、是否发生错误副作用、恢复是否收敛”。OpenTelemetry 将 traces、metrics、logs 和上下文传播定义为可组合的遥测信号;Dapper 论文则说明了分布式追踪在跨服务定位因果链上的价值。[8][9]
基础监控可用 RED 和 USE 组织,但交易系统必须再加业务正确性:
| 视角 | 指标 | 用途 |
|---|---|---|
| RED | Rate、Errors、Duration | 看服务对请求的外部表现;优先用分位数而非平均值 |
| USE | Utilization、Saturation、Errors | 看 CPU、线程池、连接池、队列、磁盘和网络的资源压力 |
| 业务 | 成功/失败、状态迁移拒绝、金额差异、库存差异、延迟和新鲜度 | 识别“接口成功但业务错误” |
| 事件 | 发布、配置、扩容、熔断、补偿、人工操作 | 把变化与指标异常放到同一时间线 |
高基数维度要有边界。trace_id 适合放在日志和 Trace,不适合直接作为长期指标标签;租户、商品、错误码、版本等维度应按查询需要分层采样、聚合或保留短期明细。统一字段至少包括 request_id、trace_id、业务单号、版本、水位、错误分类和脱敏后的操作者;禁止把支付凭证、密钥或完整个人信息写入日志。
观测体系应按“入口—服务—资源—数据—事件”分层,而不是按工具分组。入口层回答用户旅程是否成功;服务层回答哪一个接口、版本或依赖变慢;资源层回答线程、连接、队列和磁盘是否饱和;数据层回答状态、金额、库存和水位是否收敛;事件层把发布、配置、扩容、熔断、补偿和人工操作串到同一条时间线上。这样发生事故时,值班人可以从业务影响下钻到技术原因,再反向验证恢复结果。
指标命名和标签必须服务于查询。稳定维度如服务名、接口、区域、状态码和版本可以长期保留;订单号、用户号和追踪号应进入日志或短期明细索引。对高基数数据至少设置三道控制:采集端采样,存储端保留期限,查询端限制扫描范围。采样不能覆盖所有信号:错误、慢请求、状态迁移拒绝和资损差异应保留更多样本,成功请求则可以按比例采样。OpenTelemetry 的信号模型强调上下文传播和统一语义,这使得日志、指标与 Trace 能够围绕同一请求关联,但采集标准本身并不替团队决定保留多久、哪些字段可以公开。[14][20]
告警应满足四个条件:有明确影响、有责任人、有动作、有去重。CPU 高并不一定是事故,创单成功率下降、支付差异增长或队列预计超过数据新鲜度预算才是更接近用户结果的信号。每条告警都要注明当前值、基线、窗口、影响对象和建议动作;重复告警合并成事件,恢复通知必须带上验证结果。对长期无动作的告警要降级、改造或删除,告警数量本身不能作为可观测性成熟度指标。
容量与恢复验证也属于可观测性。容量估算至少区分稳定流量、峰值流量和故障转移后的流量,并把请求数、请求体大小、下游放大倍数、并发连接和数据增长一起计算。压测不能只看吞吐,还要观察 P99、错误分类、队列积压、锁等待、GC、连接池和恢复速度;压测数据必须标注是实验结果还是容量假设。备份则要按 RPO 和 RTO 设计:有备份不代表能恢复,必须定期恢复到隔离环境,校验表数量、关键账本、索引、权限和应用可读性。恢复演练中还要验证 DNS、密钥、配置、消息位点和外部依赖,避免只恢复数据库却无法启动完整业务。
3.4.2 1-5-10 应急时钟
“1-5-10”是团队约定的响应节奏,不是行业标准:1 分钟确认信号,5 分钟完成止血,10 分钟建立可协作的恢复计划。它的目的不是要求十分钟修复所有事故,而是防止团队在争论根因时继续扩大损失。
| 时间 | 责任 | 必须产出 | 禁止动作 |
|---|---|---|---|
| 0–1 分钟 | 值班人 | 确认告警真实、标记事故级别、通知 on-call | 反复刷新看板却不建立事件 |
| 1–5 分钟 | 事故指挥者 | 停止发布、限流/降级/切流、记录影响范围 | 未知副作用下大面积重启或改库 |
| 5–10 分钟 | 指挥者 + 领域 owner | 时间线、假设、下一动作、回滚/补偿负责人 | 多人并行执行同一不可逆操作 |
| 10 分钟后 | 恢复小组 | 按证据定位,持续更新影响和预算 | 把“指标恢复”误判为“数据已收敛” |
燃烧率告警要同时配置短窗口和长窗口:短窗口用于快速止血,长窗口用于发现慢性退化。告警输出必须能直接连接到动作,例如“创单 SLO 燃烧、库存补偿 backlog 增长、最近版本为 X、建议暂停发布”,而不是只发送一个红色仪表盘。
3.5 数据一致性、对账与资损防控
3.5.1 权威事实与异步传播
跨服务系统不应追求所有表同时更新,而应先回答:谁拥有业务不变量,哪条记录是权威事实,哪些是可重建投影,多久的延迟可接受。DDIA 将可靠性、可扩展性、可维护性以及数据系统的权衡放在同一框架中;中文《凤凰架构》也把分布式事务的核心问题归结为跨边界状态如何达成可接受的一致性与可恢复性。本章据此采用“权威事实 + 可重放派生状态 + 对账收敛”的模型。[10][16]
当一次本地事务既更新业务实体又发布事件时,Transactional Outbox 将事件写入同一数据库事务,再由 Relay 投递;它避免了数据库提交和消息发送之间的双写窗口,但 Relay 仍可能重复发布,因此消费者必须幂等。[11][17]
命令
│ 本地事务:业务表 + outbox_event
▼
权威库 ──► Outbox/CDC ──► Broker ──► 幂等消费者 ──► 读模型/补偿队列
│ │ │ │
└─────────────┴──────────────┴──────► 对账与水位
Outbox 至少记录 event_id、聚合 ID、聚合版本、事件类型、payload 引用、创建时间、投递状态和最近错误。大 payload 放对象存储并保存哈希,不要把不可查询的大对象塞进主表。消费者用 event_id 或业务幂等键建立唯一约束,同时校验版本,拒绝旧事件覆盖新状态。
3.5.2 三层对账与统一状态机
对账不是简单地比较两张表,而是按时间、粒度和权威口径分层:
| 层次 | 频率 | 比较对象 | 目标 | 处理方式 |
|---|---|---|---|---|
| 实时流水 | 分钟级 | 事件、支付回调、库存动作 | 发现单笔缺失、重复或乱序 | 自动重试、查询状态、进入差异表 |
| 准实时汇总 | 小时级 | 服务汇总、渠道汇总、版本水位 | 发现分片或批次偏差 | 暂停发布、重放分片、生成工单 |
| 离线总账 | 日/日切 | 权威账本、外部账单、库存快照 | 发现长期漏记和资损 | 人工复核、调账、审计归档 |
差异项统一进入状态机,而不是由不同业务各写一套脚本:
DETECTED → CLASSIFIED → RETRYING → VERIFIED
│ │ │ │
│ │ └─失败────┘
│ └─需人工──► MANUAL_REVIEW → COMPENSATED
└──────────────────────────────► IGNORED(必须有理由)
不变量是:任何差异都有来源引用、分类、影响范围、owner、下一动作和审计记录;COMPENSATED 必须有验证证据,IGNORED 必须有业务豁免和过期时间。补偿任务要保存 attempt、next_run_at、错误分类、payload 引用和 idempotency_key。超过次数不代表成功或失败,而是转入人工复核。
DLQ 不是垃圾桶。可以进入 DLQ 的是暂时无法自动处理、但仍有明确业务语义和原始凭证的记录;格式损坏、敏感信息泄露或无法识别权威对象的记录应进入隔离区。重新投递必须支持按租户、批次、错误类型和时间范围选择,设置速率上限,禁止无限自动重放。
对账表本身也要设计成可运营的数据产品。差异记录至少包含权威来源、对端来源、比较批次、业务主键、双方状态、金额或数量、首次发现时间、最近重试时间、错误分类、影响等级和处理人。比较逻辑要区分“未到水位”“暂时不可见”“确定缺失”“重复副作用”“金额不一致”和“已人工豁免”,否则不同性质的问题会被混成一个数量。水位要有明确语义,例如事件时间、提交时间、消费位点或外部账单日切时间,不能把查询时刻随意当成业务截止时间。
自动补偿必须遵循小步、可停、可验证。先根据幂等键查询当前事实,再执行最小必要动作,动作完成后重新读取权威状态,最后将证据写回差异项。重试按错误分类处理:网络超时可以延迟重试,版本冲突需要重新读取,金额不一致只能冻结并人工审批,权限或数据格式错误应立即隔离。补偿 worker 要限制并发、租户和总金额,设置熔断与暂停开关;当补偿量突然超过基线时,优先暂停而不是扩大自动化范围。
人工接管也必须工程化。工单不能只写“请处理”,应绑定业务主键、当前状态、推荐动作、禁止动作、审批角色和验证查询。高风险动作采用双人复核,执行者不能同时批准自己的调账;每次操作记录前值、后值、原因、规则版本和关联事故。人工不是自动化失败后的黑洞,而是状态机中的一个可审计节点,处理结果仍需回到实时或日切对账中验证。
3.5.3 端到端案例:支付成功但订单未更新
假设支付渠道已返回成功,订单服务因网络超时没有完成状态推进。正确目标不是“再次扣款”,而是让订单、支付账本和用户可见状态最终收敛。
- 正常链路:订单服务创建
PENDING_PAYMENT,支付服务记录支付单;支付成功与payment_succeeded事件在本地事务中写入支付账本和 Outbox;Relay 投递事件,订单消费者以payment_id幂等推进到PAID。 - 故障发生:消费者处理后返回前崩溃,Broker 重投;唯一键拒绝重复副作用,状态机把重复事件视为已处理。若支付服务提交成功但 Relay 未发出,Outbox 扫描发现未投递记录。
- 实时收敛:订单查询支付服务或接收渠道回调;若支付账本为成功而订单仍为待支付,创建差异项,重放事件或执行只推进状态的补偿命令。
- 准实时对账:按
payment_id、订单号和渠道流水号联查,校验金额、币种、商户和版本;金额不一致时禁止自动补偿,转人工冻结。 - 最终验证:订单状态、支付账本、库存动作和通知记录均有证据;用户看到“已支付/处理中”的状态,客服可查询时间线,财务可在日切对账中复核。
这里的关键不变量是:支付成功只允许订单从 PENDING_PAYMENT 向前推进,不允许回退覆盖;同一支付单只能产生一次订单支付事实;任何未知状态都必须可查询,不能用超时直接展示失败。Saga 可以协调跨服务步骤,但补偿是业务动作,不是数据库回滚;库存释放、退款和订单关闭各自需要明确前提、权限和审计。[12]
| 异常分支 | 不能做什么 | 允许的恢复动作 | 收敛证据 |
|---|---|---|---|
| 支付请求超时,结果未知 | 立即再次扣款 | 查询渠道、等待回调、以支付单号去重 | 渠道流水、支付账本和订单状态一致 |
| 回调重复到达 | 重复推进订单或重复发券 | 依据渠道流水和事件键返回已处理结果 | 重复计数不增加业务副作用 |
| 金额或币种不一致 | 自动标记支付成功 | 冻结订单和支付单,进入人工复核 | 差异原因、审批和最终处置记录 |
| 支付成功、库存失败 | 直接取消支付且不通知用户 | 重试预占、切换可接受库存策略或退款 | 库存流水、退款流水和用户状态可查询 |
| 订单已关闭、晚到成功回调 | 让旧事件覆盖关闭状态 | 依据业务规则退款或转人工,不回退状态 | 订单、支付和退款三方对账通过 |
| 补偿任务反复失败 | 无限重放 | 分类进入 DLQ,限制金额和速率后人工接管 | 重试次数、失败分类和处理结论完整 |
这个案例说明,最终一致性不是“过一会儿就会好”,而是有状态、有水位、有证据和有停止条件的恢复过程。对外展示也应区分“失败”“处理中”“成功”三种语义:在支付结果未知时展示处理中,既避免用户重复操作,也避免系统把未知事实伪装成失败。美团的对账体系实践把实时、准实时和离线核对结合起来,目的正是将单笔异常、批次偏差和长期账务问题放进不同处理节奏中。[18]
3.5.4 资损三道防线
资损包括错误计价、重复扣款、重复退款、超卖、少卖、优惠预算穿透、结算错账和人工调账失控。最小治理模型是“三道防线”:
| 防线 | 典型控制 | 失败后动作 |
|---|---|---|
| 事前防错 | 金额整数化、版本化规则、状态机、权限分级、唯一约束、影子比对 | 阻断发布或拒绝非法命令 |
| 事中拦截 | 金额/库存不变量、风险阈值、重复检测、限额、人工二次确认 | 冻结高风险请求,保留凭证 |
| 事后收敛 | 实时流水、日切总账、差异工单、补偿、调账和审计 | 先止血,再核算范围,最后补偿 |
任何金额和库存操作都应能解释“前值、动作、后值、来源、规则版本、操作者”。规则服务可以拦截风险,但不能成为新的无审计黑箱;硬编码阈值必须有配置版本、生效时间和回滚路径。
不同业务的高风险不变量不同,不能只用一个“成功率”覆盖:
| 业务对象 | 关键不变量 | 常见故障 | 优先控制 |
|---|---|---|---|
| 价格与优惠 | 应用规则版本明确,成交价可由原始输入重算 | 规则灰度不一致、优惠叠加、零元或负价 | 版本锁定、影子计算、上下限和异常订单冻结 |
| 支付与退款 | 同一支付或退款流水只能产生一次资金事实 | 超时重试、重复回调、退款超过实付 | 渠道流水唯一、金额状态机、日切对账 |
| 库存 | 可售数、预占数、释放数满足守恒关系 | 并发超卖、重复释放、库存回填覆盖新值 | 条件更新、预占流水、分片对账和人工冻结 |
| 结算与供应商 | 结算批次、费率和账期可追溯 | 批次重复、费率版本错误、外部账单延迟 | 批次锁、版本化费率、双方汇总和差异工单 |
| 运营与调账 | 每次人工改变都有审批和可逆路径 | 权限过宽、脚本误执行、理由缺失 | 最小权限、双人复核、命令白名单和审计 |
资损事件的处理顺序应是“止血—定界—保全—收敛—复盘”。止血可以暂停优惠、冻结退款、关闭异常库存动作或限制某个租户,但要记录开关生效范围;定界要按时间、版本、规则和业务主键计算影响,而不是只统计报错数;保全要保留原始请求、事件、账单和配置版本;收敛要使用幂等补偿或人工调账;复盘则把缺失的不变量转成校验、告警和发布门禁。若先调账后定界,可能把问题掩盖并制造第二次差异。
3.6 组织文化与生产红线
3.6.1 无指责复盘模板
无指责不是免除责任,而是让参与者能如实描述当时看到的信号、采用的假设和采取的动作。Etsy 的实践强调从系统条件、决策过程和失败机制学习,而不是寻找一个“坏人”作为根因。[13]
复盘文档控制在一页也可以有效,但必须包含:
| 字段 | 要回答的问题 |
|---|---|
| 影响 | 哪些用户、订单、金额、数据和 SLO 受影响? |
| 时间线 | 告警、发现、止血、恢复、验证分别何时发生? |
| 机制 | 哪个不变量被破坏?故障如何传播? |
| 当时判断 | 参与者看到什么、假设什么、为什么选择该动作? |
| 做得好的地方 | 哪个监控、开关、预案或协作缩小了损失? |
| 行动项 | owner、截止日期、验收证据、优先级和依赖是什么? |
| 复发门槛 | 什么信号出现时必须重新打开问题? |
行动项不能只写“加强监控”。应写成“为 payment_id 建立唯一约束和重复率告警,灰度验证重复事件不产生第二次订单推进,owner 为 X,截止日期为 Y”。复盘结束不等于治理完成,只有护栏上线并验证,经验才算进入系统。
事故协作应把角色和决策分开。事故指挥者负责优先级、风险和对外节奏,不亲自执行所有命令;执行负责人负责回滚、限流、切流或补偿;通信负责人负责影响范围、用户公告和内部时间线;记录负责人保存证据、命令和假设;领域 owner 负责判断业务不变量是否恢复。小事故可以一人兼任多个角色,但必须显式记录。任何高风险动作都要先说清“预期改变什么、如果没有改变怎么办、如何撤销”,避免多人凭直觉同时改动生产。
行动项按风险排序,而不是按会议上谁发言最积极排序。立即项修复正在扩大损失的护栏缺口;短期项补齐幂等、告警、回滚和对账;长期项减少架构耦合或替换高风险流程。每一项都要有验收查询或演练证据,连续两个周期没有进展就重新升级。DORA 的变更失败率和恢复时间可以作为组织层趋势指标,但它们不能替代订单、金额、库存等领域正确性指标;指标改善若伴随人工调账增加,不能判定治理成功。[3]
3.6.2 生产环境五条红线
- 不允许没有超时、幂等语义和回滚/查询路径的远程写调用进入核心链路。
- 不允许直接改权威账本、订单状态或库存余额;紧急操作也必须经过受控命令、审批和审计。
- 不允许用“接口返回成功”替代业务正确性验证;支付、库存、价格必须有独立不变量和对账。
- 不允许无限重试、无限堆积和无限自动重放;每条路径都要有预算、上限和人工接管点。
- 不允许发布、配置和补偿绕过灰度、观测和可逆性;无法回滚时必须先设计前向修复。
3.7 本章小结与延伸思考
生产可靠性可以压缩成五个判断句:
- 先定义业务结果,再定义服务指标;正确性和可恢复性不能被可用性掩盖。
- 先划定故障域,再组合超时、限流、重试、熔断、降级和舱壁;保护机制必须说明副作用。
- 先确认权威事实,再设计事件、读模型、对账和补偿;最终一致不等于无边界等待。
- 先建立 1-5-10 的止血节奏,再定位根因;指标恢复后仍要验证数据收敛。
- 先把债务、事故和行动项变成可验证的约束,再谈组织成熟度。
上线前可用以下清单做快速评审:关键旅程是否有 SLO 和错误预算?每个远程调用是否有 deadline?写操作是否有幂等键和状态机?异步事件是否可重放?权威数据是否可对账?告警是否连接到动作?发布、配置和补偿是否可审计、可暂停、可回滚?
延伸思考:当业务规模扩大十倍时,最先失效的是哪个假设?如果不能使用消息队列,如何用本地事务、扫描和对账保持收敛?如果错误预算持续耗尽,应该降低目标、增加容量,还是停止某类功能?
参考资料
[1] Beyer B, Jones C, Petoff J, Murphy N R, eds. Site Reliability Engineering:Service Level Objectives;Service Best Practices. O’Reilly Media, 2016.
[2] Google Cloud. Use Four Keys metrics like change failure rate to measure your DevOps performance. 2020. DORA 指标的工程化说明;不将行业分层阈值直接当作团队目标。
[3] Fowler M. Technical Debt Quadrant. 2009.
[4] Kruchten P, Nord R, Ozkaya I. Managing Technical Debt: Reducing Friction in Software Development. Addison-Wesley Professional, 2019.
[5] Nygard M T. Release It! Second Edition: Design and Deploy Production-Ready Software. Pragmatic Bookshelf, 2018.
[6] Amazon Web Services. Reliability Pillar: Design interactions in a distributed system. AWS Well-Architected Framework.
[7] Amazon Web Services. REL05-BP03 Control and limit retry calls. AWS Well-Architected Framework.
[8] Sigelman B H, Barroso L A, Burrows M, et al. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure. Google Research, 2010.
[9] OpenTelemetry. Signals;Observability primer. CNCF OpenTelemetry documentation.
[10] Kleppmann M. Designing Data-Intensive Applications. O’Reilly Media, 2017.
[11] Richardson C. Pattern: Transactional outbox. Microservices Patterns companion site.
[12] Richardson C. Pattern: Saga. Microservices Patterns companion site.
[13] Etsy Engineering. Blameless PostMortems and a Just Culture;Allspaw J. Debriefing Facilitation Guide.
[14] Majors C, Fong-Jones L, Miranda G. Observability Engineering, 2nd ed.. O’Reilly Media, 2026.
[15] Amazon Web Services. Incident lifecycle in Incident Manager. AWS Systems Manager documentation.
[16] 周志明. 《凤凰架构:构建可靠的大型分布式系统》:分布式事务. 凤凰架构,2022。
[17] Amazon Web Services. 事务性发件箱模式. AWS Prescriptive Guidance 中文文档。
[18] 美团技术团队. 美团配送资金安全治理之对账体系建设. 美团技术团队,2018。
[19] 阿里云开发者社区. 云上混沌工程:从故障注入到稳定性治理. 阿里云开发者社区,2022。
[20] OpenTelemetry 社区. 可观测性入门. OpenTelemetry 中文文档。