3128 字
约 10 分钟
18
S15-A19 · 兜底逻辑的边界:降级到什么程度算可用

大促当天上游超时,降级开关按预案打开了。监控里错误率从 12% 掉到 0.3%,值班群里松了口气。半小时后发现客服工单暴增:页面能打开,商品列表也能显示,但点"加入购物车"没有任何反应——降级逻辑返回了一个空列表,前端把它渲染成了"当前无可购买商品"。

降级被当成"别让页面崩",可用户要的不是不崩的页面,是能完成的动作。这篇把兜底拆成三个问题:兜到什么程度算可用、兜底本身怎么被验证、兜底什么时候会悄悄变成主路径。


1. 判据不是"没崩",而是核心动作能不能完成

"服务可用"在事故现场常被偷换成"HTTP 200 比例高"。这两个指标之间隔着用户的全部意图。

判定口径 上升时说明什么 掩盖了什么
HTTP 200 比例 网关没挂 200 里可能是空壳页面
错误率 没有返回 5xx 降级返回的默认值也是 200
P99 延迟 响应够快 快可能是因为什么都没查
核心动作完成率 用户真的做成了事 基本不掩盖,但需要埋点
核心动作时长 完成得是否可忍受 慢但可完成也算可用

可用的判定标准必须能一行写成:某类用户在某个入口,完成某个动作的成功率不低于某个阈值。写不出来,说明降级本身没有目标,只是在延迟一次报错。

// 可执行的可用性判据,而不是形容词
availability_criteria:
  entry: "商品详情页"
  core_action: "加入购物车"
  success_event: "cart_add_success"
  min_success_rate: 0.90        // 低于此值视为不可用
  p95_max_ms: 3000
  window: 5m

这份判据的意义在于:降级方案评审时,可以直接问"这个降级路径下,cart_add_success 还能不能打点"。打不了点,就等于这个降级路径无法被判定为可用。


2. 兜底的三层,与三层的用户感受

兜底不是一件事,是三种完全不同的东西。它们的用户感受差得很远,运维成本也差得很远。

层级 做法 用户感受 兑现的承诺 主要风险
L1 旧数据 返回缓存或上一次成功结果 "有点旧,但能用" 可完成核心动作 数据新鲜度失控
L2 裁剪结果 丢掉部分字段/模块,保留主干 "少了点东西" 可完成核心动作 裁剪规则与页面耦合
L3 明确失败 返回可读的失败态与重试入口 "现在不行,一会儿再来" 诚实 转化直接归零

关键判断:L1 和 L2 兑现的是"能做事",L3 兑现的是"不骗人"。三者没有优劣,只有适用条件。

  • 核心动作依赖上游某字段 → L2 不能裁掉它,只能让位给 L3;
  • 核心动作只读不写 → L1 通常够用;
  • 核心动作是写且需实时校验 → L3 是唯一诚实的选择。

把 L3 做成"看起来像 L2"的页面是最坏组合:用户以为能点,点了没反应。


3. 兜底路径平时不运行,所以它是最少被验证的代码

降级分支在生产上执行次数接近零。它没被压测过、没被集成测试覆盖过、没经过真实流量,甚至连日志格式都可能与主路径不一致。等到真要用它的那天,你验证的是它的第一次执行。

验证手段 覆盖什么 成本 频率
用例里强制抛异常 分支能否进入、返回值结构 低 每次提交
契约测试对齐主路径 响应字段与前端约定一致 中 每次提交
定时演练自动开降级 真实链路、超时、开关联动 高 按季度
故障注入 上游超时、半死、慢速三种形态 中高 按月
演练后核对核心动作 判据是否真的可达成 低 每次演练

最容易漏的是第三行与第五行的组合:开关能打开,不代表降级后核心动作能完成。演练验收不是"看到降级生效",而是"降级期间完成率仍达标"。

─── 演练脚本的骨架:开降级、打流量、核判据、必关
just_open_degrade "$MODULE" "$REASON_ID"     // 带工单号,禁止裸开
sleep "${DRILL_WINDOW:-300}"                 // 观察窗口
curl -s "$METRIC_API/core_action_rate" | jq '.rate'
just_close_degrade "$MODULE"                 // 无论成功失败都要关

4. 兜底需要被监控,否则它会悄悄变成主路径

这是兜底逻辑最隐蔽的失效方式:它不报错、不告警、不占事故编号,只是执行次数从 0 缓慢爬升到 30%,然后稳定在那里。理论上系统全程"可用",实际上所有用户都在读过期数据。

必须埋的四个数:

指标 用来回答 异常信号
兜底路径执行次数 走了多少次 非零且单调上升
走到了哪一层 L1/L2/L3 的分布 L1 长期为主 = 上游从没真恢复
兜底持续时间 已经降级多久 超过阈值无人处理
掩盖故障数 有多少真实故障被降级吃掉 上游故障复盘里找不到对应记录

第二项最容易被忽略。只统计"触发降级次数"的系统,看不到某条链路其实已经从 L1 变成常态——因为 L1 的成功率太高,永远不会触发阈值告警。

// 兜底里打点,不是在主路径里推断
if (fallbackResult.isPresent()) {
    // 层级标签是排查的关键:L1 旧数据 / L2 裁剪 / L3 明确失败
    Metrics.counter("fallback.execute",
        "layer", fallbackResult.get().getLayer(),
        "module", moduleName).increment();
}

还有一条纪律:兜底返回不能伪装成正常结果。响应带 degraded: true 或等价标记,前端才能显示"数据可能有延迟";没有标记,前端只能猜。


5. 触发与恢复:自动容易误触发,人工容易错过窗口

触发方式 反应速度 主要失效 适用
全自动(阈值驱动) 秒级 抖动导致降级/恢复反复切换 高频链路、影响面明确
全人工(值班决定) 分钟到十分钟 错过最佳窗口,或不敢开 影响面大、副作用不明确
半自动(自动建议+人工确认) 分钟级 停在建议页没人看 前后端耦合强的模块

自动触发的坑在抖动:错误率在阈值上下浮动,开关被反复切换,页面忽好忽坏,比稳定降级更糟。必要措施是滞回(降级阈值高于恢复阈值)加最短保持时间。

恢复的判断是同一个问题的另一半,而且更容易出错:上游恢复了吗? 靠探测还是靠告警?

恢复信号 可靠性 说明
上游健康检查通过 中 健康但慢,仍会继续超时
错误率回落 中高 需要足够长的观察窗口
告警自动清除 低 告警清除条件常比真实恢复宽松
主动试探真实请求 高 成本是要做灰度恢复

推荐渐进恢复:按比例放量,每步看完成率。一次恢复到底,等于自己制造瞬时流量尖峰。

// 带滞回与最短保持时间的触发规则
trigger:
  open_error_rate: 0.05          // 5% 触发降级
  close_error_rate: 0.01         // 1% 才允许恢复,滞回防抖
  min_hold: 10m                  // 最短保持,禁止反复切换
recover:
  ramp: [1, 10, 50, 100]         // 按比例渐进放量
  step_hold: 5m                  // 每档观察时长

6. 兜底数据的保质期、开关的归属,以及一条不能越的线

旧数据有保质期。"降级返回上次成功结果"这句话没说清"上次"是多久以前。一小时前是对的,一天前的价格可能已经作废。

数据类型 陈旧容忍度(典型量级) 超期后的正确做法
商品库存 极低 不得降级,宁可明确失败
商品价格 低 展示但禁止下单,走 L3
商品标题/图片 高 可长时降级
推荐列表 高 可长时降级
用户账户余额 无 严禁降级返回旧值

"降级开关"本身也需要被管理:谁有权开、开了多久、怎么关。开关没有归属人,就会变成永久状态——没人敢关,因为没人能确认关了会怎样。

// 开关登记项:没有归属人与到期时间的开关不应该存在
{
  "switch": "order.preview.fallback",
  "owner": "ORDER_TEAM",
  "opened_by": "ONCALL_ID",
  "opened_at": "TIMESTAMP",
  "auto_close_after": "PT4H",
  "reason_ticket": "INCIDENT_ID"
}

最后是一条不可越过的边界:兜底不得绕过安全控制。降级可以少查数据、可以返回旧值、可以拒绝写入,但不能跳过鉴权、不能跳过租户隔离、不能跳过脱敏。以"提升可用性"为由在降级分支里放宽权限,是把一次可恢复的可用性事故换成一个不可逆的数据事故。

团队层面的度量与边界:

度量 采集方式 关注点
兜底执行总次数 埋点计数 是否在上升
兜底期间核心动作完成率 判据事件 降级是否真的"可用"
兜底持续时间 开关日志 是否无人处理
被降级掩盖的故障数 与上游复盘交叉比对 有多少事故从未被登记

不该投入完整降级体系:非核心功能(推荐位、运营位)、纯内部系统(影响面只有同事)、影响面极小且上游自带重试、以及数据实时性本身就是产品价值的场景——那里降级等于交付错误答案。


7. 排错

现象 常见原因 处理方向
降级后页面能开但无法交互 判据缺位,只对齐了状态码 补核心动作埋点,重定判据
降级开关反复开关 无滞回、无最短保持时间 加阈值间隔与保持窗口
上游早已恢复,降级没关 恢复信号依赖告警清除 改渐进恢复 + 主动试探
用户投诉数据不对但监控全绿 兜底返回未标记 响应带 degraded 标记
降级期间鉴权报错 降级分支缺上下文 明确禁止降级跨安全边界

排错顺序:先确认走的是哪一层,再确认该层的判据是否可达成,最后才怀疑开关与触发逻辑。多数"降级没用"的问题出在第一层——根本没记录走了哪条路。


8. 自测

  1. 为什么"HTTP 200 比例高"不能作为可用的判定口径?
  2. L1、L2、L3 三层兜底分别兑现了什么承诺,各自的失效条件是什么?
  3. 为什么降级分支是系统里最少被验证的代码?
  4. 只统计"触发降级次数"的监控,会漏掉哪种形态的故障?
  5. 自动降级为什么要带滞回和最短保持时间?
  6. 降级绝对不该越过哪一条边界?
S15-A19 · 兜底逻辑的边界:降级到什么程度算可用
https://www.clxhxhhr.top/posts/6369/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。