32-错误处理与容错机制——重试熔断降级缓存四重防护
第 1 章 文档定位与阅读指引
1.1 本篇在 55 篇中的位置
行动层系列(30–34)的第三篇。30 篇讲了定位与组织、31 篇讲了七大执行器——本篇承接源文档 7.4 错误处理与容错机制(源 2961–3023),解决行动层最关键的工程问题:执行失败时怎么办?
真实世界里,API 会超时、数据库会抖动、代码会报错、网络会断。没有容错机制,一次小故障就能让整个 Agent 任务失败。本篇讲重试、熔断、降级、缓存四重防护。
1.2 本篇要回答的核心问题
- 执行错误有哪几类?如何分类处理?(错误分类)
- 重试策略怎么设计?为什么用指数退避?(重试)
- 熔断器如何防止雪崩?(熔断)
- 降级方案有哪些?(降级)
- 结果缓存怎么设计?(缓存)
- 四重防护如何协同?
1.3 读者对象与前置
- 读者:行动层/执行器工程师、关注"AI 可靠性"的架构师、SRE。
- 前置:30 篇(行动层组织)、31 篇(执行器)。
1.4 阅读方法建议
- 想快速理解框架:读第 2 章(错误分类)。
- 想深入重试:读第 3 章。
- 想深入熔断:读第 4 章。
- 想深入降级与缓存:读第 5、6 章。
- 想理解协同:读第 7 章。
- 想避坑:读第 8 章。
- 想直观感受:读第 9 章。
- 想验收:读第 10 章。
1.6 容错全景图
1.7 为什么不用"简单重试"了事
简单重试在单体小系统够用,但在 Agent 多执行器、多 Server 场景下会暴露三大问题:
- 风暴:并发重试放大下游压力。
- 雪崩:下游故障时全员重试拖垮全局。
- 盲飞:重试/降级不可观测,故障无预警。
四重防护正是为这三问题而生。
1.8 容错与可靠性的关系
容错 ≠ 100% 成功,而是"失败可预期、可恢复、可降级"。目标是把不可控的硬失败,转化为可控的软降级。
1.9 本篇与源文档的承接
本篇 100% 承接源文档 7.4:错误处理流程图(2.1 原样继承)、五类错误分类(2.2)、熔断器状态机(4.2 原样继承)。在此基础上做工程化深拆与案例扩展。
1.11 本篇与源文档映射表
为核"100% 承接源 7.4",下表逐节标注本篇内容与源文档的对应关系,便于溯源与对照。
| 源 7.4 段落 | 源主题 | 本篇落点 | 承接方式 |
|---|---|---|---|
| 7.4.1 | 错误处理流程图 | 2.1 | 原样继承+扩展 |
| 7.4.2 | 五类错误 | 2.2–2.6 | 逐类深拆 |
| 7.4.3 | 重试机制 | 第 3 章全章 | 三要素/退避/幂等 |
| 7.4.4 | 熔断机制 | 第 4 章全章 | 源状态机 4.2 原样继承 |
| 7.4.5 | 降级机制 | 第 5 章全章 | 四方案+决策树 |
| 7.4.6 | 缓存机制 | 第 6 章全章 | 键/TTL/失效/层级 |
| 7.4.7 | 协同与总结 | 第 7–10 章 | 实战扩展 |
| 7.4 案例 | 错误处理示例 | 第 9 章 | 五推演+三综合案例 |
承接原则:源文档已有的图表(错误处理流程图、熔断器状态机)一律原样保留,本篇在其之上补充判定树、参数表、代码、挑战、反例与实战案例,做到"源内容不丢、扩展不注水"。若读者想看原始表述,可直接对照源文档 7.4 节。
1.10 本章小结(终版)
容错是行动层"靠得住"的保障:重试应对瞬时故障、熔断防止雪崩、降级提供替代、缓存减少重复。本篇承接源 7.4,建立容错全景(1.6),解释为何简单重试不够(1.7),厘清容错与可靠性关系(1.8)。四重防护环环相扣,让执行层在真实世界的各种意外面前依然可靠。
第 2 章 错误处理总览
2.1 错误处理流程图(源文档原样承接)
源文档 7.4 给出完整错误处理流程:
2.2 五类错误的分类
源文档把执行错误分为五类:
| 错误类型 | 例子 | 处理方向 |
|---|---|---|
| 成功 | 正常返回 | 返回结果 |
| 超时 | 30s 无响应 | 重试→降级 |
| 网络错误 | 连接断开 | 重试→降级 |
| 权限错误 | 403 | 修复→人工 |
| 业务错误 | 参数不合法 | 分类→认知层处理 |
2.3 超时错误的处理链
超时 → 重试次数未超限 → 指数退避重试 → 超限 → 返回超时错误 → 触发降级。
超时是"没收到回应",不代表"没执行"——重试前需确认操作是否幂等(3.5)。常见超时来源:下游慢查询、网络拥塞、死锁。超时阈值应分级(2.16)。
2.4 网络错误的处理链
网络错误 → 重试 3 次 → 恢复则成功 → 未恢复 → 返回网络错误 → 触发降级。
网络错误与超时的区别:超时是"等不到",网络错误是"连接直接断"。两者都属瞬时故障,可重试。但需注意:网络错误重试前应释放旧连接,避免连接泄漏(31 篇 2.x)。
2.5 权限错误的处理链
权限错误 → 检查是否可自动修复 → 可修复则修复后重试 → 不可修复 → 请求人工干预。
权限错误的典型修复路径:token 过期 → 自动刷新 → 重试(见 9.5 微案例)。不可修复的(如权限不足、配额耗尽)必须上报,盲目重试无意义。
2.6 业务错误的处理链
业务错误 → 错误分类 → 返回结构化错误 → 认知层处理(可能调整策略)。
业务错误是"请求本身有问题"(参数不合法、状态不对)。这类错误重试无意义,应返回结构化错误(2.12)让认知层重新规划。例如"订单已发货不可取消"——Agent 应改走"申请退货"而非反复取消。
2.7 降级的统一入口
超时/网络错误的最终去向都是降级处理:有替代工具 → 切换;无替代 → 返回失败并通知认知层重新规划。
降级是错误处理的"最后一道闸"。无论前面重试多少次,最终都会汇聚到降级决策树(5.8),保证"失败有出口、任务不卡死"。
2.8 错误分类的代码示例
2.8 错误处理的设计原则
先自救、再替代、最后求助——避免过早放弃也避免盲目纠缠。
2.10 错误处理的完整生命周期
2.11 错误分类的依据
分类依据三个维度:
- 可重试性:瞬时(超时/网络)vs 永久(参数错)。
- 可修复性:权限/凭证可否自动修复。
- 严重性:影响范围(单工具 vs 全局)。
2.12 错误的结构化表示
{
"code": "TIMEOUT",
"message": "请求超过30秒无响应",
"category": "transient",
"retryable": true,
"fallback": "cache"
}
2.13 错误上报的链路
2.14 错误处理的成本
每次错误处理都有成本(重试占用时间、降级降低质量)。目标:用最小成本恢复,恢复不了再升级。
2.16 超时阈值的设计
超时不是越短越好:太短误杀慢请求,太长拖垮链路。建议:
| 操作类型 | 超时建议 | 说明 |
|---|---|---|
| 本地 API | 5s | 内网低延迟 |
| 第三方 API | 30s | 受网络影响 |
| 代码执行 | 60s | 计算耗时 |
| 数据库查询 | 10s | 防慢查询 |
2.17 错误分类与重试策略映射
2.18 错误传播的边界
错误在行动层内部尽量自愈(重试/降级),只有"无法自愈"的错误才上报认知层。过早上报 = 浪费 LLM 推理;过晚上报 = 任务卡死。
2.19 错误处理的可观测
错误率、重试率、降级率、平均恢复时间,是错误处理健康的四指标。缺了它们,错误就是"黑盒"。
2.21 错误分类的实战判断树
错误分类(2.2–2.6)决定"能不能重试、要不要熔断、能否降级"。工程里最易混淆的是 HTTP 状态码与超时,下面给出可直接落地的判断树。
易错点对照表:
| 场景 | 直觉 | 正确做法 | 原因 |
|---|---|---|---|
| 429 限流 | 立即重试 | 退避且尊重 Retry-After | 硬撞更易被封 |
| 503 不可用 | 重试 | 重试+熔断 | 可能持续不可用 |
| 404 | 重试 | 不重试 上报 | 资源真的不在 |
| 400 | 重试 | 不重试 修请求 | 参数问题重来也错 |
| 超时但已处理 | 重试 | 幂等校验后跳过 | 防重复副作用 |
分类落码示例(伪代码):
def classify(err):
if err.is_timeout: return TRANSIENT # 超时
if err.code == 429: return TRANSIENT # 限流
if err.code in (500, 502, 503): return TRANSIENT
if err.code == 404: return PERMANENT # 资源缺失
if err.code in (400, 422): return PERMANENT # 请求错误
if err.code in (401, 403): return AUTH # 权限
return BUSINESS # 业务校验
注意:分类结果会被重试(3 章)、熔断(4 章)、降级(5 章)共用,必须"一次分类,全局复用",避免各防护重复判断、口径不一。
2.22 错误处理的可测试性设计
容错逻辑"看起来对"不等于"真的对"。必须把它变成可测试的。
测试金字塔:
| 测试层 | 目标 | 手段 | 覆盖率要求 |
|---|---|---|---|
| 单元 | 退避/幂等键/分类 | Mock 错误 | 100% 分支 |
| 集成 | 熔断三状态转换 | 模拟失败率 | 状态全走通 |
| 混沌 | 重试风暴/雪崩 | 随机杀依赖 | 关键路径 |
| 演练 | 真实故障恢复 | 计划内演练 | 年度≥2 次 |
可测试设计原则:
- 依赖可注入:执行器依赖通过接口注入,测试可替换为"必然失败/必然超时"的桩。
- 时间可控制:退避/熔断计时用可注入时钟,测试不真等 30 秒。
- 状态可观测:熔断状态、重试计数暴露为测试可读指标。
- 故障可注入:提供"让第 N 次调用失败"的钩子,验证重试确实触发。
示例——验证熔断器打开:
cb = CircuitBreaker(failure_rate=0.5, window=10)
for _ in range(10):
with inject_failure(): # 注入失败
cb.call(make_request) # 全部失败
assert cb.state == OPEN # 达到阈值即打开
可测试性是容错"可信"的前提。没有测试的容错代码,上线即是一颗定时炸弹。
2.20 本章小结(终版)
源文档错误处理流程图 100% 承接:五类错误各有处理链,降级统一兜底。深挖覆盖完整生命周期、分类依据、结构化表示、上报链路、成本权衡——先自救再替代最后上报。
第 3 章 重试机制深拆
3.1 为什么要重试
很多执行失败是瞬时故障:网络抖动、服务短暂不可用、限流。这类故障重试一次可能就好了,直接放弃太可惜。
3.2 重试策略三要素
- 最大次数:避免无限重试。
- 退避间隔:避免同时重试(惊群)。
- 抖动(jitter):避免固定间隔同步。
retry:
max_attempts: 3
backoff: [1s, 2s, 4s]
jitter: 0.2
3.3 指数退避
源文档给出退避序列:1s → 2s → 4s → 8s(指数增长)。
3.4 什么错误值得重试
- 可重试:超时、网络、429、5xx。
- 不可重试:4xx(参数错)、认证失败(需人工)。
3.5 重试的幂等性
重试的前提是幂等:重复执行不产生副作用。
- 读操作天然幂等。
- 写操作需幂等键。
- 非幂等写操作禁止自动重试。
3.6 重试风暴的防护
多个 Agent 同时重试 → 压垮服务。防护:
- 退避 + 抖动(错开时间)。
- 熔断器(错误率高时停止重试)。
- 限流(重试也占配额)。
3.7 重试与熔断的配合
3.8 重试参数的配置
| 场景 | max | backoff | jitter |
|---|---|---|---|
| API | 3 | 1-4s | 0.2 |
| 数据库 | 2 | 0.5-2s | 0.1 |
| 代码 | 1 | — | — |
3.9 重试的观测
记录:重试次数、重试成功率、平均额外延迟。重试率过高说明依赖不稳定,需查根因。
3.11 重试的完整实现
def retry_with_backoff(fn, policy):
for attempt in range(policy.max_attempts):
try:
return fn()
except RetryableError as e:
if attempt == policy.max_attempts - 1:
raise
delay = policy.backoff[attempt] * (1 + random.uniform(0, policy.jitter))
time.sleep(delay)
raise FinalFailure()
3.12 重试与幂等键
{
"method": "POST",
"url": "...",
"headers": { "Idempotency-Key": "task-123:step-2" }
}
重试时携带相同幂等键,Server 去重。
3.13 重试的上下文
- 重试次数记录在 trace。
- 重试不改变请求语义。
- 重试间可更新凭证(token 刷新)。
3.14 重试的监控指标
| 指标 | 意义 |
|---|---|
| 重试率 | 依赖稳定性 |
| 重试成功率 | 重试有效性 |
| 平均额外延迟 | 重试代价 |
| 放弃率 | 最终失败 |
3.15 重试的常见反模式
| 反模式 | 修正 |
|---|---|
| 无限重试 | 上限 |
| 固定间隔 | 退避+抖动 |
| 重试写操作 | 幂等键/不重试 |
| 忽略熔断 | 熔断优先 |
3.16 重试的退避算法实现
指数退避(带抖动)伪代码:
3.17 重试的退避系数选择
| 系数 | 行为 | 适用 |
|---|---|---|
| 固定 | 每次同间隔 | 调试 |
| 线性 | 1,2,3… | 轻负载 |
| 指数 | 1,2,4,8… | 通用 |
| 指数+上限 | 封顶 | 防过长等待 |
3.18 重试与批量操作的冲突
批量任务中单条失败不应重试整批——应逐条重试失败的条目,避免"一条失败、全批重跑"。
3.19 重试的超时嵌套
重试本身的累计等待可能超过上游超时。需计算:单次超时 × 重试次数 + 退避总和 < 上游容忍度。
3.20 重试的并发控制
高并发下大量请求同时重试 → 风暴。用信号量限制并发重试数,配合舱壁(30 篇 7.x)。
3.21 重试的日志与追踪
每次重试都应记录:第几次、等待时长、错误类型。链路追踪(trace)关联重试链,便于排障。
3.23 重试与幂等键的实现
3.24 重试在代码执行器中的特殊考量
代码执行(31 篇)天然耗时且副作用强。重试代码执行必须:幂等(同输入同输出)、可取消(超时不留僵尸进程)、沙箱隔离(失败不影响外界)。
3.25 重试与缓存的协同
重试失败后降级用缓存(stale)——此时重试已证明下游不可用,缓存是最后的体面。重试成功则回填缓存(6.x)。
3.26 重试与业务 SLA
重试延迟要算进业务 SLA:3 次指数退避累计可能 7s+。若业务要求 5s 响应,需调小退避或减次数。
3.27 重试的跨调用链传递
重试状态应写入 trace context,下游可在日志中看到"这是第 N 次重试",便于定位"重试风暴"来源。
3.28 重试的测试
强制注入瞬时故障(网络延迟、5xx),验证重试次数、退避间隔、最终成功率。混沌工程(25 篇 9.x)常态化。
3.29 重试的默认值建议
| 场景 | 次数 | 退避 | 抖动 |
|---|---|---|---|
| 本地调用 | 2 | 固定 100ms | 0 |
| 第三方 API | 3 | 指数 1s | 30% |
| 数据库 | 2 | 指数 500ms | 20% |
| 代码执行 | 1 | 不重试 | — |
3.31 重试与消息队列的协同
当执行器通过消息队列(MQ)异步调用依赖,重试逻辑要搬到"队列侧",而非调用侧。
同步调用 vs 异步 MQ 重试对比:
| 维度 | 同步重试 | MQ 重试 |
|---|---|---|
| 重试位置 | 调用方进程内 | Broker / 消费方 |
| 失败处理 | 立即退避重试 | 进入死信队列(DLQ) |
| 幂等要求 | 同进程内保证 | 跨进程必须幂等 |
| 适用 | 实时链路 | 解耦长任务 |
MQ 重试标准模式:
关键设计点:
- 消费幂等:同一条消息可能被投递多次,消费逻辑必须幂等(用消息 ID 去重,仿 3.4)。
- 重试上限 + 退避:MQ 自带"指数退避重投"能力,避免自写轮询。
- 死信队列(DLQ):超过上限的消息进 DLQ,由人工或补偿任务处理,不阻塞主队列。
- 不阻塞:MQ 重试是异步的,不会像同步重试那样"卡住调用线程"。
与行动层衔接:Agent 把"发通知""写审计"等非核心动作投到 MQ,其重试由队列托管;核心动作(支付、扣减)仍走同步重试+幂等。两类重试在 7.8 协同框架里统一监控。
反模式:在 MQ 消费里"无限同步重试 + 不 Ack"——消息不确认会重复投递,消费方被同一失败消息压垮。正确做法:快速失败 + 交给队列退避重投 + 超限进 DLQ。
3.32 重试速记口诀与常见误区清单
为便于记忆与代码评审,把重试要点浓缩成口诀,并列出评审必查项。
口诀:
瞬时才试,幂等必配;退避错峰,抖动防堆;次数有限,超时相随;不可重试,直接退。
代码评审检查单:
- 重试前是否先判断"可重试"(分类 2.21)?
- 是否有幂等键,避免重复副作用?
- 退避是否指数 + 抖动,而非固定间隔?
- 最大次数是否受限(≤5)?
- 是否尊重上游超时,避免空等?
- 重试计数/失败原因是否打点可观测?
- 是否避免对 4xx/权限/业务错重试?
任何一项不达标,评审应打回。重试是"看起来简单、做错代价大"的典型,靠口诀+检查单把经验固化。
3.30 本章小结(终版)
重试机制应对瞬时故障:三要素控制节奏,指数退避错开时间,幂等保证安全,熔断防止风暴。深挖覆盖完整实现、幂等键、上下文、监控、反模式、退避算法、系数选择、批量冲突、超时嵌套、并发控制、日志追踪、幂等实现、代码执行考量、与缓存协同、SLA、跨链传递、测试、默认值——可重试与不可重试严格区分。
第 4 章 熔断器深拆
4.1 为什么要熔断
当某个工具连续失败时,熔断器会暂时"熔断"对该工具的调用,避免雪崩式失败。雪崩:一个服务挂掉 → 重试风暴 → 更多服务挂掉。
4.2 熔断器状态机(源文档原样承接)
源文档 7.4 给出熔断器状态机:
熔断器状态机:
关闭状态(正常)
│
│ 连续失败次数 ≥ 阈值(如5次)
↓
打开状态(熔断中)
│ 所有调用直接返回错误,不真正执行
│
│ 等待恢复时间(如30秒)
↓
半开状态(试探)
│ 放行1个请求试探
│
├── 成功 → 关闭状态(恢复正常)
└── 失败 → 打开状态(继续熔断)
4.3 三状态详解
| 状态 | 行为 | 进入条件 |
|---|---|---|
| 关闭 | 正常调用 | 初始/半开成功 |
| 打开 | 直接返回错误 | 连续失败≥5 |
| 半开 | 放行1个试探 | 等待30s后 |
4.4 状态转换图
4.5 熔断的触发条件
- 连续失败次数 ≥ 阈值(如 5 次)。
- 或滑动窗口错误率 ≥ 50%(窗口 60s)。
- 或最小请求数 ≥ 10 且错误率高。
4.6 熔断的恢复
- 等待恢复时间(如 30s)。
- 半开状态放行 1 个请求试探。
- 成功 → 关闭;失败 → 继续打开。
4.7 熔断器的粒度
粒度越细,隔离越好(一个工具熔断不影响其他)。
4.8 熔断器的参数
circuit_breaker:
failure_threshold: 5
reset_timeout: 30s
half_open_requests: 1
sliding_window: 60s
4.9 熔断与降级的配合
熔断打开时,调用直接走降级:缓存结果、替代工具、或明确失败。
4.10 熔断的可观测
记录:熔断次数、熔断时长、恢复次数。频繁熔断说明依赖不稳定,需查根因。
4.12 熔断器的完整实现
class CircuitBreaker:
def __init__(self, threshold=5, reset=30, half_open=1):
self.threshold = threshold
self.reset = reset
self.half_open = half_open
self.state = "closed"
self.failures = 0
self.opened_at = 0
def call(self, fn):
if self.state == "open":
if time.time() - self.opened_at > self.reset:
self.state = "half_open"
else:
raise BreakerOpen()
try:
result = fn()
self.on_success()
return result
except Exception as e:
self.on_failure()
raise
def on_success(self):
self.failures = 0
if self.state == "half_open":
self.state = "closed"
def on_failure(self):
self.failures += 1
if self.state == "half_open" or self.failures >= self.threshold:
self.state = "open"
self.opened_at = time.time()
4.13 熔断的滑动窗口
基于错误率的熔断用滑动窗口:
sliding_window:
window_seconds: 60
min_requests: 10
error_rate_threshold: 0.5
- 窗口内请求数 ≥ 10 且错误率 ≥ 50% → 熔断。
4.14 熔断的粒度实践
| 粒度 | 适用 | 优点 |
|---|---|---|
| 按工具 | 工具独立 | 隔离最细 |
| 按 Server | Server 维度 | 管理简单 |
| 按执行器 | 执行器维度 | 统一 |
| 按主机 | 依赖主机 | 防单点 |
4.15 熔断的恢复试探
半开状态放行 1 个请求(可配置 N 个):
- 成功 → 关闭(恢复正常)。
- 失败 → 打开(继续熔断 30s)。
4.16 熔断与重试的优先级
熔断优先于重试:
- 熔断打开 → 直接失败(不重试)。
- 熔断关闭 → 允许重试。
- 重试失败累积 → 触发熔断。
4.17 熔断的可观测
记录:熔断次数、熔断时长、半开试探结果、恢复次数。看板展示各工具熔断状态。
4.18 熔断的常见反模式
| 反模式 | 修正 |
|---|---|
| 无熔断 | 加熔断 |
| 阈值太小 | 误熔断 |
| 阈值太大 | 兜不住 |
| 无半开 | 无法恢复 |
4.19 熔断的滑动窗口实现
4.20 熔断的阈值设计
| 参数 | 建议 | 说明 |
|---|---|---|
| 失败率阈值 | 50% | 过高兜不住 |
| 窗口大小 | 20 次 | 过小误触 |
| 休眠窗口 | 30s | 过长恢复慢 |
| 半开放行 | 1-3 次 | 试探量 |
4.21 熔断与限流的协同
熔断防"下游故障传播",限流防"自身被压垮"。二者互补:限流在入口(25 篇 3.15),熔断在调用侧。
4.22 熔断的跨进程共享
多 Agent 实例共用一个下游时,熔断状态应跨进程共享(Redis),避免每个实例各自熔断导致"局部熔断、全局未防"。
4.23 熔断的误熔断防护
偶发抖动不应触发熔断。用"最小请求数"门槛(窗口内至少 N 次请求才评估),避免低流量时误熔。
4.24 熔断的监控看板
熔断状态、触发次数、平均打开时长、半开成功率,是熔断健康的四指标。看板缺失 = 雪崩无预警。
4.26 熔断与重试的协同时序
4.27 熔断的半开探测策略
半开时不应只放 1 个请求——放太少可能误判(偶发成功),放太多可能再次压垮。建议按窗口规模的 5% 或 1-3 个之间取大值。
4.28 熔断的人工干预
极端故障下可手动"打开"熔断(提前保护下游),或手动"强制关闭"(确认下游恢复后)。运维控制台应暴露熔断状态与手动开关。
4.29 熔断与舱壁的关系
舱壁(30 篇 7.x)限制单个下游的最大并发,熔断限制失败率。两者配合:舱壁防"单点占满资源",熔断防"单点拖垮全局"。
4.30 熔断在不同执行器的粒度
| 执行器 | 熔断粒度 | 理由 |
|---|---|---|
| API | 按 host | 同一 host 共命运 |
| 数据库 | 按实例 | 实例独立 |
| 代码 | 按沙箱池 | 池级隔离 |
| 搜索 | 按引擎 | 引擎独立 |
4.31 熔断的演进方向
自适应熔断(如基于延迟百分位而非固定阈值)能更精准地识别"变慢但未挂"的亚健康状态,比二值熔断更早介入。
4.33 熔断与限流的协同实战时序
熔断(保护链路)与限流(保护自身)常被混淆,但两者协同才能既"不压垮别人"又"不被别人拖垮"。下面是一次完整调用的协同时序。
分工口诀:
- 限流在前:先问"我有没有资格发这个请求"(保护自身资源)。
- 熔断在后:再问"对方还活不活"(保护链路不雪崩)。
- 二者都触发时:限流拒绝优先(因为连令牌都没有,没必要谈熔断)。
配置联动:限流阈值参考熔断打开频率——若熔断频繁打开,说明下游已不稳,可临时收紧限流减少无效请求;下游恢复后逐步放开。这种"限流随熔断自适应"是 7.22 动态调参的雏形。
错误计数归属:被限流拒绝的请求不应计入熔断错误率(它不是依赖的错,是自身的错),否则限流会"误触发"熔断。这一点在 4.5 粒度设计与 2.21 分类判断树里都要体现:限流拒绝 = PERMANENT 类(不重试、不计入熔断)。
4.32 本章小结(终版)
熔断器防止雪崩:关闭→打开→半开三状态。深挖覆盖完整实现、滑动窗口、粒度、恢复试探、优先级、可观测、反模式、窗口实现、阈值设计、与限流协同、跨进程共享、误熔断防护、监控看板、协同时序、半开探测、人工干预、舱壁关系、不同执行器粒度、自适应演进——连续失败触发、等待恢复试探、成功回关失败再熔,源状态机 100% 承接。
第 5 章 降级机制深拆
5.1 为什么要降级
工具不可用时,任务不一定失败——可以用替代方案"降级"完成,或降级失败后明确上报。
5.2 降级方案优先级
5.3 四类降级方案
| 方案 | 场景 | 例子 |
|---|---|---|
| 替代工具 | 有同功能工具 | 邮件→钉钉 |
| 缓存结果 | 只读场景 | 用上次价格 |
| 简化执行 | 可降级 | 详情→摘要 |
| 明确失败 | 无路可走 | 上报重规划 |
5.4 降级的透明性
降级后要在结果中标注:
{ "status": "success", "data": "...", "meta": { "degraded": true, "reason": "primary_unavailable", "fallback": "cache" } }
5.5 降级的业务影响
降级有代价(数据旧、精度低),认知层要知情:
- 缓存结果:标注"可能过期"。
- 简化执行:标注"已简化"。
- 替代工具:标注"来自替代源"。
5.6 降级与重试的顺序
失败处理优先级:
- 重试(瞬时故障)。
- 降级(替代方案)。
- 上报(认知层重新规划)。
5.7 降级的演练
定期演练降级路径:模拟主工具不可用,验证降级方案可用。
5.8 降级决策树
降级不是"随便换个工具",而是按可替代性分级决策:
5.9 降级的触发条件细分
| 触发场景 | 降级方案 | 风险 |
|---|---|---|
| 工具超时 | 替代工具 / 缓存 | 结果可能过时 |
| Server 熔断 | 备用 Server | 数据来源变化 |
| 权限被拒 | 简化执行 | 功能缺失 |
| 业务错误不可恢复 | 明确失败 | 任务中断 |
| 网络分区 | 缓存 / 离线模式 | 一致性弱化 |
5.10 降级与重试的边界
常见错误是把"一切失败"都先重试。正确边界:
- 可瞬时恢复的(网络抖动、限流)→ 重试。
- 结构性失败的(权限、业务校验)→ 直接降级或上报。
- 不确定是否恢复的(超时)→ 重试 N 次后降级。
5.11 降级的代价与监控
降级后必须可观测:降级次数、降级原因分布、降级成功率、降级后任务完成率。监控缺失会导致"悄悄降级、悄悄出错"。
5.12 降级的安全边界
降级方案本身也可能引入风险:用缓存可能返回敏感旧数据,简化执行可能跳过校验。降级需遵守最小权限与脱敏基线,与 30 篇安全隔离一致。
5.13 降级在代码审查 Agent 中的体现
回顾 29 篇案例:GitHub Server 不可用时,Agent 降级为"仅读取本地缓存的 diff",并通知认知层调整计划——这就是降级的真实落地。
5.14 降级的配置化
降级策略应可配置:哪些工具允许降级、降级优先级、降级超时。配置中心(30 篇 3.x)统一管理,避免硬编码。
5.15 降级的测试
降级路径同样需要测试:强制注入工具失败,验证降级是否触发、降级结果是否可用。混沌工程(25 篇 9.x)可常态化验证降级链路。
5.17 替代工具的选择标准
选择替代工具需评估:功能覆盖度(能完成同样任务吗)、数据来源(是否同源)、延迟(是否可接受)、成本(是否更贵)、可靠性(是否更稳)。优先级:同源替代 > 跨源替代 > 简化执行。
5.18 简化执行的边界
简化执行(只取核心字段、跳过非关键步骤)必须保证"语义不偏离"。例如"查订单详情"降级为"查订单状态"——用户得到状态而非详情,但任务未完全失败。
5.19 明确失败的艺术
当所有降级都不可行,应"明确失败"而非"假装成功"。失败信息要结构化(2.12),让认知层能精准重规划。模糊失败(返回空)比明确失败更危险。
5.20 降级的回滚与补偿
降级产出的"简化结果"可能与最终真实结果不一致。需记录降级轨迹,待主路径恢复后补偿(如补发完整通知、回填完整数据)。
5.21 降级的人工确认点
高风险降级(如"跳过校验直接放行")应设人工确认,而非自动执行。降级自动化程度应与风险等级匹配。
5.22 降级与用户体验
对终端用户,降级应"无感或知会":无感(自动缓存)、知会(提示"数据可能延迟")。绝不可"静默错误"。
5.23 降级在多 Agent 协作中的传播
一个 Agent 降级 → 结果质量下降 → 下游 Agent 受影响。降级信号应随任务上下文传递(29 篇协作机制),让下游提前调整预期。
5.25 降级标注规范与文案实战
降级"透明"不能只停留在理念,要落成可执行的标注规范。本节给出可直接抄用的标注模板与反例。
标注四要素(缺一不可):
- 发生了什么:简明说明降级原因(依赖名 + 故障类型)。
- 用了什么替代:替代工具 / 缓存 / 简化逻辑。
- 数据时效:实时 / 缓存于 HH:MM / 简化结果。
- 用户可做的:重试入口 / 稍后再试 / 影响范围。
标准标注模板:
| 场景 | 推荐文案 | 标注级别 |
|---|---|---|
| 缓存兜底 | “当前展示为 5 分钟前缓存数据,点此刷新最新” | 黄色角标 |
| 替代工具 | “主服务繁忙,已切换备用通道,结果已验证” | 蓝色角标 |
| 简化结果 | “部分数据暂不可用,已展示核心摘要” | 橙色角标 |
| 明确失败 | “该功能暂时不可用,已记录,恢复后通知您” | 红色提示 |
反例(禁用文案):
- ❌ “加载失败”(不说明原因、无出路)——用户只能干等。
- ❌ “系统异常,请稍后再试”(掩盖降级、假装没降级)——不诚实。
- ❌ 静默返回旧数据无标注——信任陷阱(见 8.27 事故三)。
文案设计原则:
- 用"人话"而非错误码;用户不懂 503。
- 给"下一步"而非只报"坏消息";每条降级提示都应有出口。
- 标注位置贴近受影响内容,而非全局弹窗(避免恐慌)。
- 提供"重试加载最新"按钮,让用户有掌控感。
工程落地:标注文案与降级代码同仓管理,纳入 UI 评审;国际化场景需多语言文案。降级标注率应作为质量门禁(目标 100%),未标注的降级在代码评审中一票否决。
5.24 本章小结(终版)
降级机制提供"备用出路":替代工具、缓存结果、简化执行、明确失败四类方案按优先级选择,由降级决策树驱动。深挖覆盖触发条件、与重试边界、代价监控、安全边界、案例落地、配置化、测试、替代选择标准、简化边界、失败艺术、回滚补偿、人工确认、用户体验、多 Agent 传播——透明(标注原因、stale 标记)并让认知层知情,先重试再降级最后上报,与重试、熔断、缓存共同构成行动层韧性底座。
第 6 章 结果缓存深拆
6.1 为什么要缓存
相同参数的调用复用结果,减少重复执行:
- API 调用省费用。
- 数据库查询省资源。
- 重复任务省时间。
6.2 缓存的适用场景
6.3 缓存键设计
缓存键 = 执行器 + 参数哈希:
cache_key = hash(executor + json.dumps(arguments))
6.4 缓存 TTL 策略
| 场景 | TTL |
|---|---|
| 稳定数据 | 300s |
| 动态数据 | 30s |
| 敏感数据 | 不缓存 |
| 大结果 | 不缓存 |
6.5 缓存失效
- 写操作后清理相关缓存。
- 变更通知触发失效。
- TTL 到期。
6.6 缓存的命中标注
{ "meta": { "cached": true, "cache_ttl": 60 } }
6.7 缓存的成本收益
- 命中率越高越省。
- 读多写少场景收益最大。
- 统计类查询缓存收益大。
6.9 缓存键的构造细节
缓存键必须能唯一标识一次"相同请求":
| 要素 | 是否入键 | 说明 |
|---|---|---|
| 工具名 | 是 | 不同工具结果不可混 |
| 参数序列化 | 是 | 参数不同结果不同 |
| Server 标识 | 是 | 多 Server 同名工具 |
| 会话上下文 | 可选 | 租户隔离需带租户 ID |
| 时间戳 | 否 | 由 TTL 控制新鲜度 |
6.10 缓存的层级结构
- 本地内存缓存:最快,进程内,适合单实例。
- 分布式缓存(Redis):多实例共享,适合水平扩展(30 篇 3.x)。
- 语义缓存:按"问题语义"命中,适合 LLM 调用(与 18 篇推理协同)。
6.11 缓存与一致性的权衡
缓存的代价是"可能读到旧数据"。权衡策略:
- 只读数据:长 TTL。
- 弱一致数据:中 TTL + 命中标注 stale。
- 强一致数据:不缓存或写后强失效。
6.12 缓存的命中率优化
命中率低 = 缓存白做。优化手段:预热(启动时加载热点)、参数归一化(消除无意义差异)、合理 TTL(避免过早失效)。
6.13 缓存的失效风暴
批量写后批量失效,可能造成"缓存击穿"——大量请求同时穿透到 Server。解法:失效错峰、请求合并、单飞(singleflight)。
6.14 缓存与降级的协同
缓存既是"加速器"也是"降级源":正常时加速,Server 不可用时降级提供 stale 结果(5.x 已述)。二者共用同一份存储。
6.15 缓存的安全考量
缓存可能泄露跨租户数据(键未带租户 ID)、缓存敏感结果。缓存需遵守脱敏基线(30 篇 5.x),并对敏感结果缩短 TTL 或不缓存。
6.16 缓存的可观测
缓存命中率、失效率、平均节省时间、击穿次数,是缓存健康的四大指标。缺失会导致"缓存 silently 失效"。
6.18 缓存的预热策略
预热避免"冷启动"期全部穿透。适合已知高频的元数据查询(工具清单、Schema)。
6.19 缓存的压缩与序列化
大结果(如整表)缓存时压缩(gzip)+ 二进制序列化,降低内存与网络开销。但压缩有 CPU 成本,小结果不值得。
6.20 缓存的逐出策略
| 策略 | 行为 | 适用 |
|---|---|---|
| LRU | 淘汰最久未用 | 通用 |
| LFU | 淘汰最少用 | 热点稳定 |
| TTL | 到期即删 | 时效数据 |
| FIFO | 先进先出 | 简单 |
6.21 缓存与并发读
高并发缓存未命中时,多个请求同时回源(缓存击穿)。singleflight(同键合并)让一次回源服务所有等待者(6.13 已述)。
6.22 缓存的命名空间隔离
多租户场景用 namespace 前缀隔离缓存,避免租户间命中错乱(2.12 键带租户 ID 的呼应)。
6.23 缓存的容量规划
缓存不是无限大。容量 = 热点数据量 × 副本数。超额触发逐出(6.20),可能误删热点。需监控命中率与逐出率。
6.24 缓存的故障降级
缓存服务(Redis)本身也可能挂。此时应降级为"不缓存、直接回源",而非阻塞。缓存故障不应拖垮主链路。
6.26 缓存与数据库一致性深度权衡
缓存的本质是"用一致性换性能"。Agent 行动层写数据(如更新配置、落库)时,缓存与库如何不打架,是工程里最易出 bug 的地方。
三种经典策略:
| 策略 | 做法 | 一致性 | 风险 |
|---|---|---|---|
| 写后失效 | 写库后删缓存 | 最终一致 | 删失败→脏读 |
| 写后更新 | 写库后更缓存 | 较强 | 并发写乱序 |
| 写穿透 | 直接写缓存+异步落库 | 弱 | 丢库风险 |
推荐:写后失效 + 延迟双删(防并发脏读):
并发写乱序问题:两个写 W1、W2 先后到,但缓存更新顺序反了 → 缓存是旧值。解法:缓存更新带版本号/时间戳,旧版本不覆盖新版本。
降级场景的取舍:当"强一致"不可得,降级可接受"最终一致 + 标注"(见 5.25)。例如展示"数据可能有秒级延迟",比"卡住等强一致"体验更好。一致性不是越高越好,要按业务定级:
- 金融余额:强一致,不缓存或短 TTL + 写后强失效。
- 商品库存:最终一致,标注"库存可能实时变动"。
- 用户画像:弱一致,长 TTL 即可。
一致性监控:对比缓存值与库值抽样,差异率超阈值报警;发现脏缓存可一键批量失效。一致性漂移(8.10)往往源于"只写库不失效"或"失效失败未重试"。
结论:缓存一致性没有银弹,靠"写后失效 + 版本防护 + 降级标注 + 监控兜底"四件套把风险压到可接受。
6.25 本章小结(终版)
结果缓存减少重复调用:只读+稳定场景缓存、参数哈希做键、TTL 控制新鲜度、写后失效保一致。深挖覆盖键构造、层级、一致性权衡、命中率优化、失效风暴、与降级协同、安全考量、可观测、预热、压缩、逐出、并发读、命名空间、容量规划、故障降级——命中标注透明,读多写少场景收益最大,是行动层性能与韧性的双重基石。
第 7 章 四重防护的协同
7.1 四重防护的定位
7.2 协同时序
7.3 各司其职
| 防护 | 解决 | 触发 |
|---|---|---|
| 重试 | 瞬时故障 | 超时/网络 |
| 熔断 | 雪崩 | 连续失败 |
| 降级 | 不可用 | 熔断/重试耗尽 |
| 缓存 | 重复调用 | 相同参数 |
7.4 参数调优
resilience:
retry: { max: 3, backoff: [1s,2s,4s] }
breaker: { threshold: 5, reset: 30s }
fallback: { prefer_cache: true }
cache: { ttl: 60s, read_only: true }
7.6 四重防护的优先级链
当一次执行失败时,四重防护按固定顺序介入:
7.7 重试与熔断的配合
- 重试在熔断"关闭"时生效;熔断"打开"时重试被短路。
- 熔断统计的是"重试后仍失败"的次数,避免重试放大故障。
- 半开状态允许少量请求探测恢复,避免长期误熔断。
7.8 降级与缓存的配合
- 缓存是降级的首选数据源(stale 结果比无结果好)。
- 降级写入的"简化结果"也可回写缓存,供后续复用。
- 二者共享存储,避免双写不一致。
7.9 四重防护的全局配置
参数应集中管理(配置中心):
| 防护 | 关键参数 | 默认值建议 |
|---|---|---|
| 重试 | maxAttempts / backoff / jitter | 3 / 指数 / 30% |
| 熔断 | errorThreshold / sleepWindow | 50% / 30s |
| 降级 | 优先级 / 超时 | 替代→缓存→简化 |
| 缓存 | TTL / maxSize | 60s / 1000 |
7.10 四重防护与认知层的联动
行动层处理失败后会向认知层上报(降级原因、是否成功),认知层据此调整计划——这是 18 篇"推理"与 22 篇"反思"所需的失败信号来源。
7.11 四重防护的测试策略
- 重试:注入瞬时故障,验证退避与次数。
- 熔断:注入连续失败,验证状态机切换。
- 降级:注入 Server 不可用,验证降级触发。
- 缓存:注入重复请求,验证命中与失效。
7.12 四重防护的可观测
统一打点:重试次数、熔断状态、降级率、缓存命中率,聚合到行动层监控面板(30 篇 3.x)。缺可观测 = 韧性别名"盲飞"。
7.14 协同的配置模板
resilience:
retry:
max_attempts: 3
backoff: exponential
base_ms: 1000
jitter: 0.3
circuit_breaker:
failure_threshold: 0.5
window_size: 20
sleep_window_ms: 30000
half_open_requests: 2
fallback:
order: [alternative, cache, simplify, fail]
annotate: true
cache:
ttl_seconds: 60
max_size: 1000
invalidate_on_write: true
7.15 协同的端到端时序
7.16 协同的故障传播模型
执行失败
├─ 可重试 → 重试(熔断统计) → 成功/降级
└─ 不可重试 → 直接降级/上报
降级失败 → 上报认知层 → 重新规划
7.17 协同的代价平衡
四重防护不是越多越好:重试增加延迟、熔断减少可用、降级降低质量、缓存占用内存。平衡点在"故障频率 × 影响"——高频小故障重重试,低频大故障重熔断降级。
7.18 协同与水平扩展
多 Agent 实例下,熔断与缓存状态需跨进程共享(4.22、6.10),否则每个实例各自决策,全局防护失效。
7.19 协同的灰度发布
新容错参数应先灰度(1% 流量)验证再全量。参数错误(如阈值过小)可能引发误熔断风暴。
7.21 四重防护的演进路线
四重防护不是一次性到位,应随系统规模演进。下面给出"从小到超大"的推荐演进路径。
| 阶段 | 系统特征 | 推荐防护 | 理由 |
|---|---|---|---|
| 1 | 单体、依赖少 | 重试 | 解决偶发抖动 |
| 2 | 依赖变多 | +熔断 | 防单点拖垮 |
| 3 | 读多写少 | +降级+缓存 | 保核心+省钱 |
| 4 | 多团队协作 | +可观测 | 故障可定位 |
| 5 | 超大规模 | +配置中心 | 参数统一治理 |
| 6 | 极致高可用 | +自适应 | 动态调优 |
切忌"阶段 1 就上全套"——复杂度会拖慢交付,且多数防护在低风险场景收益有限。按痛点驱动演进,每一级都有明确 ROI。
7.22 配置中心化实战与动态调参
当防护参数散落在几十个服务里,一次"熔断阈值调整"要改 N 处、发 N 次版。配置中心化是必选项。
集中配置结构(与 10.11 模板同源):
resilience_defaults: # 全局默认
retry: {max: 3, base_ms: 200, mult: 2.0, jitter: full}
breaker: {rate: 0.5, window: 20, open_s: 30, half: 3}
cache: {ttl_s: 300, key: param_hash}
overrides: # 按依赖特例
payment_gateway:
breaker: {rate: 0.3, open_s: 10} # 支付更敏感
recommendation:
cache: {ttl_s: 60} # 推荐时效短
动态调参三原则:
- 热更新:参数变更秒级生效,无需重启(避免"改配置=发版")。
- 灰度推送:新参数先推 5% 节点验证,再全量(见 10.24)。
- 版本回退:参数带版本号,异常一键回退旧版。
自适应调参(L5)前瞻:基于实时错误率、延迟自动调整退避与阈值。例如延迟升高自动加大退避、错误率突增自动收紧熔断。但自适应需防"误判放大"——自动调参本身要有护栏(最大/最小边界),不能无限漂移。
配置中心化的终局:防护策略从"代码里写死"变成"平台上治理",运维可在故障期间临时收紧熔断而不碰代码。这是容错从"开发负担"转为"平台能力"的关键一跃。
7.20 本章小结(终版)
四重防护各司其职又环环相扣:重试治瞬时、熔断防雪崩、降级给出路、缓存省重复。由优先级链驱动,重试与熔断配合、降级与缓存配合、全局配置集中、与认知层联动、可测试可观测。深挖覆盖配置模板、端到端时序、故障传播模型、代价平衡、水平扩展、灰度发布——协同保障行动层"失败可救、慢了能快"。
第 8 章 技术挑战与解决方案
8.1 挑战总览
8.2 挑战:重试风暴
- 退避+抖动错开。
- 熔断器上限。
- 限流重试。
8.3 挑战:雪崩
- 熔断器。
- 舱壁隔离。
- 限流。
8.4 挑战:缓存过期
- TTL 合理。
- 写后失效。
- 变更通知。
8.5 挑战:降级不透明
- meta 标注。
- 认知层知情。
- 日志记录。
8.6 挑战:误重试写操作
- 幂等键。
- 写操作不自动重试。
- 人工确认。
8.7 挑战:错误掩盖
重试/降级可能掩盖真正的问题。解法:
- 重试/降级计数看板。
- 频繁降级告警。
- 根因分析。
8.8 排障决策树
8.10 重试风暴的根治
重试风暴(thundering herd)指失败瞬间大量重试同时打到 Server:
解法:全链路退避 + 抖动 + 请求合并 + 熔断前置。
8.11 错误掩盖的识别
"所有失败都被降级吞掉"是比失败更危险的状态。识别手段:降级率异常升高、任务完成率与成功率背离、日志中降级路径占比过高。
8.12 误重试的代价
对不可重试错误(4xx、业务校验失败)重试 = 浪费资源 + 放大日志噪音。要求错误分类先行(2.x),用幂等键约束写操作。
8.13 缓存过期与雪崩
缓存集中过期会造成"缓存雪崩"(同刻大量穿透)。解法:TTL 加随机抖动、热点永不过期+后台刷新、多级缓存。
8.14 降级不透明的问题
降级结果未标注,认知层会把 stale 数据当最新数据决策。要求降级结果必须携带"来源标注"(stale/替代/简化)。
8.15 容错机制的排障手册
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 大量超时 | Server 过载/网络 | 看 Server 指标、限流 |
| 熔断频繁 | 下游持续故障 | 看错误率、恢复探测 |
| 降级率飙升 | 主路径全挂 | 看根因、切换备用 |
| 缓存命中率低 | 键设计问题 | 看参数归一化 |
| 重试风暴 | 退避缺失 | 看重试策略配置 |
8.16 容错机制的混沌演练
定期注入故障验证韧性:杀掉一个 Server 进程、拔掉网络、注入高延迟——验证四重防护是否按预期工作(25 篇 9.x 方法复用)。
8.18 挑战:降级循环
降级 → 简化结果 → 下游基于简化结果再请求 → 再降级 → 循环。识别:降级率持续高位且任务无法收敛。解法:降级次数上限 + 最终明确失败。
8.19 挑战:重试放大下游负载
高 QPS 下并发重试,即使单实例退避,总量仍可能压垮下游。解法:全局限流(25 篇 3.15)+ 请求合并 + 熔断前置。
8.20 挑战:缓存与降级的数据一致性漂移
缓存作为降级源,若长期不更新,降级结果越来越旧,导致"静默错误"。解法:stale 标记 + 最大陈旧度 + 恢复后强制刷新。
8.21 挑战:熔断误判(抖动)
低流量下偶发 1 次失败即达阈值 → 误熔断。解法:最小请求数门槛 + 更长窗口(4.23)。
8.22 挑战:可观测缺失导致盲飞
四重防护都在跑,但没有指标 → 故障无预警、优化无依据。解法:统一打点(重试/熔断/降级/缓存四类指标)+ 看板 + 告警。
8.23 挑战:配置错误引发二次故障
阈值过小 → 频繁熔断;TTL 过长 → 陈旧;退避过大 → 拖慢。解法:配置评审 + 灰度(7.19)+ 混沌演练验证。
8.24 挑战:多租户容错隔离
某租户触发熔断不应影响其他租户。解法:按租户分桶熔断 + 命名空间缓存(6.22)+ 舱壁配额(30 篇 7.x)。
8.25 容错机制的成熟度模型
| 级别 | 特征 |
|---|---|
| L1 | 仅简单重试 |
| L2 | 重试+熔断 |
| L3 | 重试+熔断+降级 |
| L4 | 四重防护+可观测 |
| L5 | 四重防护+自适应+混沌演练 |
8.27 真实事故复盘(三则行业教训)
容错设计的价值,往往在事故后才被真正理解。下面三则源于公开技术复盘(已脱敏抽象),对应本篇三道防护的"反面教材"。
事故一:重试风暴压垮支付网关
- 现象:大促开始 3 分钟,支付成功率从 99.9% 跌到 40%,下游支付网关 CPU 100%。
- 根因:客户端对支付超时做了"固定间隔 100ms 重试 ×5",且所有客户端时钟同步,重试在同一毫秒共振,瞬间流量放大 5 倍。
- 缺失的防护:退避无抖动(3.4)+ 无熔断(4 章)+ 无舱壁(30 篇 7.x)。
- 修复:改为指数退避+全抖动,叠加熔断器(失败率 50% 即打开),单依赖并发限流。
- 教训:重试是双刃剑,没有抖动的重试等于组织化的 DDOS。
事故二:缓存雪崩拖垮商品库
- 现象:某日凌晨缓存集群重启,大量商品缓存键 TTL 相同,重启后集体失效,瞬间所有请求穿透到数据库,数据库连接池耗尽。
- 根因:TTL 设置完全相同(6.4 失效风暴),且无空值缓存/布隆过滤(6.19),单飞未启用(6.20)。
- 修复:TTL 加随机抖动避免同时失效;热点键永不过期+后台刷新;启用单飞合并并发回源。
- 教训:缓存集体失效比缓存没有更危险,TTL 抖动是低成本高收益的基础防护。
事故三:静默降级引发信任危机
- 现象:推荐服务故障,系统静默返回"默认热门列表",用户持续看到重复内容,投诉"App 坏了但不报错"。
- 根因:降级未标注(5.3 透明性缺失),用户无从判断数据真实性。
- 修复:所有降级结果带角标"备用数据,可能非最新",并提供"重试加载最新"入口。
- 教训:降级不标注,本质是对用户的不诚实;透明降级反而增强信任。
三则事故共同指向:四重防护不是"可选项",每一道缺失都会在特定的故障场景下变成事故。
8.28 容错实施的成本账与 ROI
团队常问:"上容错要花多少功夫,值不值?"用一张量化账回答。
| 投入项 | 一次性成本 | 持续成本 | 收益 |
|---|---|---|---|
| 重试改造 | 中(幂等键设计) | 低 | 瞬时故障恢复,省人工排查 |
| 熔断接入 | 低(用库) | 低 | 防雪崩,避免全站宕机(估损百万级) |
| 降级预案 | 高(需备方案) | 中(维护) | 核心链路保活,体验不崩 |
| 缓存建设 | 中 | 中(内存) | 省下游成本 30%–70% |
| 可观测 | 中 | 中(存储) | 故障早发现,MTTR 降 80% |
ROI 排序(投入产出比从高到低):
- 熔断:低成本高收益,首推。
- 缓存:直接省钱,次推。
- 可观测:让其他防护"看得见",必投。
- 降级:保核心,按业务优先级排。
- 重试精细化:需幂等改造,最后做。
经验法则:先上"低成本高收益"的熔断与可观测,再逐步补缓存与降级。不要一上来就做全套,避免团队被复杂度劝退。
8.29 容错与 SRE 的协作接口
容错代码写完后,真正的"韧性"靠 SRE 接手运维。二者需有明确接口,避免"开发以为有容错、SRE 以为没告警"的错位。
| 交接项 | 开发交付 | SRE 接手 |
|---|---|---|
| 指标 | 暴露重试/熔断/降级/命中率 | 配看板+报警阈值 |
| 配置 | 参数可热更新 | 故障期临时调参 |
| 演练 | 提供故障注入点 | 计划内混沌演练 |
| 降级 UX | 标注文案定稿 | 监控标注率达标 |
| 预案 | 明确降级路径 | 编写事故 runbook |
协作闭环:
常见错位与修正:
- 开发没暴露"降级率"指标 → SRE 无法判断降级是否频发 → 要求补打点(7.12)。
- SRE 不知降级含义 → 误判"功能坏了" → 开发需对接降级 UX 与标注规范(5.25)。
- 演练只测开发环境 → 生产未验证 → 约定每季度生产影子演练(8.25)。
容错是"开发写能力、SRE 保运行"的共担责任,交接清晰才能长治久安。
8.26 本章小结(终版)
容错机制的挑战覆盖重试风暴、雪崩、缓存过期、降级不透明、误重试、错误掩盖、降级循环、负载放大、一致性漂移、误判、盲飞、配置错误、多租户隔离,深挖风暴根治、掩盖识别、误重试代价、缓存雪崩、降级透明、排障手册、混沌演练、降级循环、负载放大、漂移、误判、盲飞、配置、租户——解法:退避+熔断、舱壁、TTL+失效、标注、幂等键、看板告警、根因分析、分桶、灰度、自适应,全部可落地可验证。成熟度模型(L1-L5)给出演进路线。
第 9 章 通俗案例深度解析
9.1 案例:API 超时的完整处理
9.2 案例:熔断器救场
解读:邮件 API 故障期间,若每个 Agent 都无限重试,会把本就脆弱的 API 彻底压垮。熔断器在连续 5 次失败后"打开",让后续调用快速失败(不再打向 API),转而走降级(写本地队列)。30 秒后"半开"试探一次,成功则恢复、失败则继续熔断。这是防雪崩的关键。
9.3 案例:缓存省费用
相同报表查询第二次命中缓存,耗时 500ms → 5ms,API 费用省 50%。
解读:数据分析 Agent 常对同一指标反复查询(不同维度)。首次查询落库缓存(TTL 60s),后续查询命中缓存,既不花 API 费用、也不占下游负载。缓存的收益在"读多写少、参数稳定"的场景最大。
9.4 案例:降级不丢任务
Slack 不可用 → 通知降级为写文件 + 稍后补发,审查任务不中断。
解读:代码审查 Agent(29 篇)最后要把结论通知团队。若 Slack 宕机,直接失败会导致"审查做了、团队不知道"。降级为"写本地文件 + 队列稍后补发",审查任务本身不中断,故障恢复后补发到位。降级保的是"任务不丢",而非"完美执行"。
9.5 微案例:权限自动修复
token 过期 → 自动刷新 → 重试成功,用户无感。
解读:权限错误中"可修复"的一类最常见就是凭证过期。容错层捕获 401/AUTH 错误后,先尝试刷新 token(不惊动认知层),刷新成功则重试原请求。用户/认知层全程无感,体验最佳。不可修复的权限错误(如配额耗尽)才上报。
9.6 反例:无限重试
无上限重试 → 打爆 API → 触发限流 → 更慢。退避+上限。
后果:下游只是慢了 1 秒,却被重试 100 次打进限流黑名单,恢复时间从 1 秒变成 10 分钟。重试必须设上限(通常 3 次)+ 退避,否则自愈变自残。
9.7 反例:无熔断
一个服务挂 → 全员重试 → 雪崩。熔断器。
后果:10 个 Agent 实例同时调用一个挂掉的 API,每个重试 3 次 = 30 次无效请求,把重启中的 API 再次压垮,形成"重启→被打挂→重启"死循环。熔断器的"打开"状态让请求快速失败,给下游喘息空间。
9.8 反例:缓存不失效
数据更新了缓存还是旧值 → 用户看到过期数据。写后失效。
后果:Agent 基于旧缓存做了"用户已付款"的错误判断,重复扣款。缓存必须"写后失效"(任何写操作触发相关键删除),否则缓存从"加速器"变成"错误源"。
9.9 案例总结
容错机制的真实运转:重试治瞬时、熔断防雪崩、降级给出路、缓存省重复——四重防护协同,让"做不成"变成"换个方式做成"。
通用教训:先分类(2.x)、再重试(3.x)、配熔断(4.x)、可降级(5.x)、有缓存(6.x)、全标注(5.12)。缺任何一环,都会在真实故障中暴露短板。
9.11 推演:限流场景下的完整应对
Agent 调用第三方 API 被限流(429):
9.12 推演:数据库慢查询降级
SELECT 超时 → 重试一次仍慢 → 熔断计数 → 降级为"查询元数据表"(简化执行)→ 标注后返回。认知层得到"表存在但数据未取到"的部分结果。
9.13 推演:多执行器链路上的容错
数据流水线(31 篇案例):DB 查询成功 → 代码分析超时 → 重试成功 → API 发送被拒(权限)→ 不重试、降级为"记录到队列"→ 通知认知层异步补偿。
9.14 推演:缓存击穿修复
热点统计查询集中过期 → 击穿 → 启用 singleflight(同参数请求合并为一个)→ 回源一次 → 回填缓存 → 后续全部命中。
9.15 推演:熔断半开恢复
熔断打开 30s 后进入半开 → 放行 1 个探测请求 → 成功 → 关闭熔断 → 恢复全量。失败 → 重新打开,等待下一次探测。
9.16 反例:重试幂等缺失
写操作无幂等键重试 → 重复扣款/重复建单。幂等键(29 篇 5.x)是写操作重试的前提。
9.17 反例:降级无标注
降级返回旧缓存未标注 → 认知层按最新数据处理 → 决策错误。任何降级必须带来源标注。
9.18 反例:全局熔断粒度过粗
一个 Server 熔断导致所有调用被熔 → 舱壁隔离(按工具/租户分桶),避免单点拖垮全局。
9.19 反例:缓存敏感数据
缓存了含凭证的结果且未脱敏 → 泄露风险。缓存遵守脱敏基线,敏感结果不缓存。
9.21 推演:跨区域容灾降级
主区域 API 不可用 → 熔断 → 降级为调用备区域 API(替代工具)→ 标注"跨区域"。备区域延迟高但可用,任务不中断。
9.22 推演:凭证轮换期间的重试
凭证即将过期 → 调用偶发 401 → 容错层刷新凭证 → 重试 → 成功。全程无感,认知层未介入。
9.23 推演:大结果分页的容错
大结果返回超时被截断 → 重试仍超时 → 降级为分页拉取(简化执行)→ 合并分页结果 → 标注"分块获取"。
9.24 推演:数据库主从切换
主库挂 → 写操作熔断 → 降级为写从库(若允许)或排队 → 主库恢复后补偿。读操作直接切从库。
9.25 反例:降级链过长
降级 → 替代工具也挂 → 再降级 → 再替代 → 无限降级链。每次降级都应收敛,最终到"明确失败"。
9.26 反例:熔断恢复后未清缓存
熔断恢复后,旧 stale 缓存仍被返回,用户看到过期数据。恢复后应强制刷新(6.20)。
9.27 反例:重试吞掉业务错误
把 4xx 当瞬时错误重试 → 浪费重试次数 + 掩盖真实问题。错误分类(2.11)是重试的前提。
9.28 反例:缓存命中误判
相似但不相同的请求命中缓存 → 返回错误结果。缓存键必须严格区分参数(6.3)。
9.29 反例:可观测缺失的故障
四重防护都在跑但无指标 → 故障发生 1 小时才发现 → 损失扩大。指标是容错的"眼睛"。
9.31 综合案例一:电商大促「秒杀下单」全链路容错推演
秒杀是容错设计的"压力考场":瞬时高并发 + 下游脆弱 + 不能超卖。本案例把四重防护一次性串起来。
场景链路:接收下单指令 → 库存校验(API)→ 库存扣减(数据库)→ 支付(第三方)→ 发通知(消息队列)。
四重防护落点:
| 步骤 | 风险 | 防护 | 配置 |
|---|---|---|---|
| 库存校验 | 服务抖动 | 重试 3 次+退避 | base 200ms ×2 |
| 库存扣减 | 超卖 | 幂等+数据库事务 | 唯一订单键 |
| 支付 | 网关超时 | 重试+熔断 | 熔断 50%/20次 |
| 通知 | MQ 拥堵 | 降级(本地落库稍后补) | 标注"通知延迟" |
关键决策:
- 下单指令带幂等键,重复提交不重复扣减(解决超卖)。
- 支付网关超时先重试(幂等),连续失败触发熔断,熔断期间新请求直接走"稍后支付"降级页,标注"支付通道繁忙,订单已锁定"。
- 通知非核心,MQ 拥堵时降级为本地落库+定时补偿,不影响主下单。
复盘教训:秒杀最怕"重试放大超卖"。扣减必须幂等且原子,重试只重试"未确认"的支付,绝不重试已成功的扣减。四重防护在这里是"保不超卖 + 保不崩 + 保核心下单"。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/lza_csdn2019/article/details/165750038



