案例的读法:数字是示例,结构才是重点
这一节的三个案例来自游戏服务端最常见的三类事故:配置错误、容量与缓存、权限与幂等。它们不是某家公司的真实事故记录,也不是行业基准;所有人数、时长、金额都是为演示计算而构造的示例值,使用时必须替换成自己的账本与监控数据。
案例的价值不在数字,而在结构:同样的时间线写法、同样的三层根因、同样的“预防/发现/控制”改进映射。把结构学会,换一个业务也能套用;只记住数字,下次遇到不同形态的事故仍然无从下手。
三个案例分别对应本章前两节的方法:7.1 的故障树负责“怎么定位”,7.2 的复盘模板负责“怎么拆根因、怎么闭环”,这一节把两者放进完整叙事里,并补上影响面的可复算口径。
案例一:活动配置多一个零导致奖励超发
某次节日活动上线,运营在后台把“每档奖励数量”从 100 填成了 1000。活动开启后六分钟,奖励发放量告警触发;团队停发、回滚配置、核对账本,从开启到完全止住约 42 分钟。以下时间线与数字均为示例。
| 时间 | 事实 | 证据来源 |
|---|---|---|
| 10:00 | 活动开启,奖励开始发放 | 活动系统日志 |
| 10:06 | 奖励发放量超过基线 8 倍,告警触发 | 监控 |
| 10:09 | 配置 diff 显示数量字段为 1000,预期为 100 | 配置审计日志 |
| 10:12 | 停止活动发奖,冻结相关任务 | 变更记录 |
| 10:30 | 配置回滚,确认不再新增发放 | 配置系统 |
| 10:42 | 账本核对完成,确认超发范围 | 对账报表 |
证据链的核心不是“谁填错了”,而是三件事同时成立:配置审计日志显示字段值;发放流水显示每笔数量与配置一致;对账报表显示超发总量与影响账号数。三条证据的「类型+来源」不同,才足以确认根因,而不是停在“可能是配置问题”。
三层根因如下:
| 层级 | 内容 |
|---|---|
| 直接原因 | 奖励数量配置多了一个零,发放逻辑按配置原样执行 |
| 系统性原因 | 配置没有范围校验;发放前没有总量预算;配置与代码共享无 schema 的 JSON |
| 逃逸原因 | 灰度只按区服切流量,但发奖任务是全局的;上线前没有做总量 dry-run |
架构改进按“预防、发现、控制”三类展开:
| 类别 | 改进项 | 验收标准 |
|---|---|---|
| 预防 | 配置 schema 与范围校验 | 数量超过配置上限时拒绝保存 |
| 预防 | 上线前 dry-run 奖励总量 | 输出预期发放量与预算差异,超阈值阻断 |
| 发现 | 发放量相对配置预期的偏差告警 | 偏差超过 2 倍持续 1 分钟即告警 |
| 控制 | 按区服灰度并支持自动停发 | 单区异常时 30 秒内停止全局发奖 |
| 控制 | 补偿与回滚走幂等命令 | 重复执行不产生二次发放 |
活动配置的治理细节在第 2.1 节已经展开,这里只强调案例层面的教训:配置错误的破坏力,取决于配置有没有预算与熔断,而不取决于填写的人是否仔细。 把“仔细核对”写进检查表,不如把“超预算拒绝保存”写进系统。
案例二:排行榜缓存击穿导致登录雪崩
第二个案例更接近容量问题。登录接口为了展示玩家排名,会同步读取排行榜缓存;某次缓存 key 在同一秒集中过期,回源请求打满数据库,登录接口开始超时。以下数字同样为示例:DAU 12 万,受影响比例 45%,持续 38 分钟。
| 时间 | 事实 | 证据来源 |
|---|---|---|
| 20:00 | 排行榜缓存批量过期,命中率从 96% 降到 9% | 缓存监控 |
| 20:01 | 数据库 CPU 升至 100%,慢查询激增 | 数据库监控 |
| 20:03 | 登录错误率超过 20%,玩家开始集中反馈 | 登录大盘 |
| 20:07 | 值班关闭登录页的排行榜展示 | 降级开关 |
| 20:15 | 回源 QPS 回落,登录恢复 | 监控 |
| 20:38 | 缓存重建完成,排行榜恢复 | 缓存监控 |
证据链的关键是把“登录慢”拆成“缓存失效→回源放大→数据库饱和→登录超时”四段。单看登录错误率,只能知道现象;单看数据库 CPU,只能知道结果;把缓存命中率、回源 QPS、数据库慢查询、登录错误率放在同一时间轴上,才能确认放大路径。
三层根因:
| 层级 | 内容 |
|---|---|
| 直接原因 | 排行榜缓存 key 集中过期,回源请求击穿数据库 |
| 系统性原因 | 登录链路同步依赖非核心的排行榜;缓存无请求合并;容量按平均流量设计 |
| 逃逸原因 | 压测未覆盖缓存集体失效;监控只看平均延迟,不看回源 QPS 与命中率 |
改进映射同样分三类:
| 类别 | 改进项 | 验收标准 |
|---|---|---|
| 预防 | 同 key 并发回源合并 | 单 key 回源并发不超过 1 |
| 预防 | TTL 加随机抖动 | 同一批 key 的过期时间分散在 10 分钟窗口 |
| 发现 | 命中率与回源 QPS 联合告警 | 命中率低于 50% 且回源 QPS 超基线 3 倍时告警 |
| 控制 | 登录链路可关闭排行榜依赖 | 开关生效后登录不读排行榜 |
| 控制 | 入口限流与排队 | 数据库饱和时优先保护登录 |
这个案例最重要的架构判断是:排行榜不是登录的前置条件。 把非核心依赖从核心链路里拆出去,比给缓存加多少层保护都有效。缓存优化能降低概率,依赖隔离才能改变故障形态。可观测性指标的设计在第 5.1 节讨论过,这里补的是“指标要能区分放大路径”,而不是只报最终错误率。
案例三:GM 批量补偿脚本重复执行
第三个案例是权限与幂等。一次邮件附件丢失后,运营用 GM 批量脚本给受影响玩家补发道具。脚本第一次执行到一半超时,操作同学以为失败,重新执行了一次;两次执行都成功,导致部分玩家收到双份补偿。以下数字为示例:涉及 8000 名玩家,重复发放 8000 份道具,六小时后对账才发现。
| 时间 | 事实 | 证据来源 |
|---|---|---|
| 14:00 | 批量补偿脚本第一次执行,处理到 60% 时客户端超时 | GM 审计日志 |
| 14:02 | 操作同学重新执行同一脚本 | GM 审计日志 |
| 14:20 | 两次执行均完成,系统返回成功 | 任务日志 |
| 20:10 | 日常对账发现道具总量高于预期 | 对账报表 |
| 20:30 | 冻结后续补偿,开始逐账号核对 | 变更记录 |
| 次日 10:00 | 完成回收与玩家沟通 | 客服记录 |
这个案例的证据非常直接:GM 审计日志里有两笔相同参数的执行记录,任务日志里两次都返回成功,对账报表显示总量是预期的两倍。三条证据都指向“重复执行”,不需要复杂推理。真正值得复盘的是:为什么系统允许同一命令执行两次都成功?
三层根因:
| 层级 | 内容 |
|---|---|
| 直接原因 | 批量补偿脚本没有幂等键,超时重试导致重复发放 |
| 系统性原因 | GM 工具不记录执行 ID;审批只检查脚本文本,不检查执行次数;补偿后没有自动对账 |
| 逃逸原因 | 监控只告警“执行失败”,不告警“重复成功”;对账是 T+1 的人工动作 |
改进映射:
| 类别 | 改进项 | 验收标准 |
|---|---|---|
| 预防 | 批量命令必须携带幂等键 | 同一幂等键重复提交只产生一次副作用 |
| 预防 | 执行前 dry-run 输出影响行数与金额 | 影响超过阈值需二次确认 |
| 发现 | 补偿后自动对账并告警差异 | 发放总量与预期差异超过 1% 即告警 |
| 控制 | 高风险命令双人审批 | 无第二审批人时命令无法提交 |
| 控制 | 分批执行与单批上限 | 单批失败可终止,不影响已确认批次 |
GM 命令的幂等与补偿机制在第 3.2 节已经详细讨论,权限与审批在第 3.1 节。这个案例在复盘层面的教训是:“重试”是分布式系统的默认行为,任何会产生副作用的命令都必须假设自己会被执行两次。 把幂等键做成工具链的强制字段,比要求操作同学“确认上一次是否成功”可靠得多。
横向对比:三个案例的共同结构
把三个案例放在一起,会发现它们的直接原因完全不同,系统性原因却高度相似:关键路径缺少隔离、高风险动作缺少预算、执行结果缺少对账。
| 案例 | 直接原因 | 系统性原因 | 逃逸原因 | 首要改进 |
|---|---|---|---|---|
| 配置超发 | 数量多一个零 | 无范围校验与预算 | 灰度不覆盖全局任务 | 配置 schema + 总量 dry-run |
| 缓存击穿 | key 集中过期 | 核心链路依赖非核心缓存 | 压测未覆盖集体失效 | 依赖隔离 + 请求合并 |
| 补偿重复 | 超时后重试 | 命令无幂等键 | 只告警失败不告警重复 | 强制幂等键 + 自动对账 |
这张表的用法不是背结论,而是检查自己的系统:配置有没有预算?核心链路有没有非核心依赖?会产生副作用的命令有没有幂等键?三个问题都不需要等到事故才能回答。
另一个共同点是:三个案例的改进项都同时覆盖了预防、发现、控制三类。只做预防,下一次换个触发点仍会坏;只做发现,系统仍然脆弱;只做控制,则是在为已知故障准备止血带,却没有降低发生概率。纵深不是三选一,而是三类都要有最低配置。
影响面怎么算才可复算
复盘报告里的影响面经常是一句“约两万玩家受影响”。这个数字如果不可复算,就无法用于优先级排序,也无法比较两次事故的严重程度。建议把影响面拆成三个可计算字段:
- 受影响玩家数 = DAU × 受影响比例,比例来自监控或抽样,必须写明口径。
- 玩家分钟 = 受影响玩家数 × 影响时长,用于比较不同时长的事故。
- 示例经济损失 = 受影响玩家数 × 人均示例损失 + 超发数量 × 单位示例成本。
以案例一为例(以下全部是示例值):DAU 8 万,受影响比例 35%,时长 42 分钟,人均示例损失 0.02 元,超发 1.2 万份,每份示例成本 1.5 元。计算得到受影响玩家 2.8 万人、玩家分钟 117.6 万、示例经济损失 18560 元。这些数字只用于演示公式,不代表任何真实产品。
可复算的关键是把假设写在公式旁边:比例从哪来、时长怎么界定、单价是谁给的。否则同一个事故,不同的人会算出不同的影响面,复盘会上就会争论数字,而不是讨论改进。
可运行片段:时间线校验、影响计算与改进映射
internal/incidentcase 把案例结构落成可测试的代码。case.go 校验案例身份、事故类别、时间线顺序与必需节点(发现、止血、恢复),并要求直接、系统性、逃逸三层根因齐全;impact.go 用固定公式计算受影响玩家、玩家分钟与示例经济损失,输入不合法时只报一条错误,避免噪声。
mapping.go 按事故类别给出改进模板:配置类、容量类、权限类各自对应预防、发现、控制三类动作,并检查已应用的改进项是否覆盖了这三类。它不判断动作是否有效,只判断有没有形成纵深;有效性要靠 7.2 节的验收标准。
// file: internal/incidentcase/impact.go
// tier: L1
// verify: go test ./internal/incidentcase/
package incidentcase
import (
"errors"
"math"
"time"
)
// ImpactInput 是影响面计算的输入。所有金额与人数都是示例口径,
// 真实使用时必须替换为账本与监控的可复核数据。
type ImpactInput struct {
DAU int
AffectedRatio float64
Duration time.Duration
UnitLoss float64 // 每名受影响玩家的示例损失
ExtraUnits int // 超发或重复发放的示例数量
UnitCost float64 // 每单位示例成本
}
// Impact 是可复算的影响面结果。
type Impact struct {
AffectedPlayers int
PlayerMinutes float64
EconomicLoss float64
}
// ErrInvalidImpact 表示影响面输入不满足计算前提。
var ErrInvalidImpact = errors.New("incidentcase: invalid impact input")
// Compute 计算影响面。比例先按 DAU 折算人数,再乘时长与示例单价。
func (in ImpactInput) Compute() (Impact, error) {
if in.DAU <= 0 || in.AffectedRatio < 0 || in.AffectedRatio > 1 || in.Duration <= 0 {
return Impact{}, ErrInvalidImpact
}
if in.UnitLoss < 0 || in.ExtraUnits < 0 || in.UnitCost < 0 {
return Impact{}, ErrInvalidImpact
}
affected := int(math.Round(float64(in.DAU) * in.AffectedRatio))
if affected > in.DAU {
affected = in.DAU
}
return Impact{
AffectedPlayers: affected,
PlayerMinutes: float64(affected) * in.Duration.Minutes(),
EconomicLoss: float64(affected)*in.UnitLoss + float64(in.ExtraUnits)*in.UnitCost,
}, nil
}
本机验证命令与结果:
cd .local/checks/gsarch-lab
gofmt -l internal/incidentcase
go test -count=1 ./internal/incidentcase/
gofmt -l 无输出;go test 退出码 0,该包 5 个表驱动测试全部通过。测试覆盖示例影响计算、比例与时长边界、时间线乱序与缺节点、改进类别覆盖,以及模板返回值的确定性。
案例复盘的检查表
拆解案例时,可以用下面八个问题自检:
- 时间线是否只写事实与来源,没有混入评价?
- 发现、止血、恢复三个节点是否都有时间戳?
- 直接原因能否解释现象,而不是只描述现象?
- 系统性原因是否回答了“为什么影响这么大”?
- 逃逸原因是否回答了“为什么没有更早发现”?
- 影响面的每个数字是否有口径与来源?
- 改进项是否同时覆盖预防、发现、控制?
- 每条改进项是否有负责人、截止时间、验收标准、复发检测?
第 3 题最容易出错。 “数据库 CPU 100%”不是直接原因,而是结果;“缓存 key 集中过期”才是机制。把结果当原因,改进项就会落在“扩容数据库”上,而不是“消除集中过期与同步依赖”。
三个案例放在一起看
| 维度 | 配置超发 | 缓存击穿 | 补偿重复 |
|---|---|---|---|
| 直接原因 | 发布重试未校验幂等 | 热 key 集中过期 | 脚本无幂等键 |
| 系统性原因 | 缺少总额上限与发布幂等 | 缺少依赖隔离与请求合并 | 缺少执行账本与对账 |
| 逃逸原因 | 只监控错误率,不监控发奖速率 | 只监控成功率,不监控缓存命中 | 只监控脚本退出码 |
| 预防 | schema 校验、dry-run | 依赖隔离、单飞 | 强制幂等键 |
| 发现 | 发奖速率告警 | 命中率与源站 QPS 告警 | 执行记录与账本对账 |
| 控制 | 一键暂停活动 | 降级到静态榜单 | 自动补偿与人工复核 |
三行原因、三类改进,是案例拆解的最小结构。只写直接原因,改进会停留在“加一个判断”;补上系统性与逃逸原因,才会去改隔离、预算与观测。
三个反模式
反模式一:只写时间线,不写证据链。 时间线回答“发生了什么”,证据链回答“怎么知道”。没有证据链的案例,读起来像故事,无法被复用。
反模式二:把示例数字当基准。 本篇的人数和金额都是演示值。直接拿去做容量规划或对外沟通,会得到错误结论。用自己的账本与监控重新计算,是使用案例的最低要求。
反模式三:改进项只做预防。 加校验、加审批当然重要,但如果没有发现与控制,下一次故障仍然会以未知形态扩大。三类改进的最低配置,应该成为案例复盘的硬性要求。
小结
三个案例的直接原因不同,系统性原因却指向同一组问题:关键路径缺少隔离、高风险动作缺少预算、执行结果缺少对账。配置超发靠 schema 与总量 dry-run 防住,缓存击穿靠依赖隔离与请求合并改变故障形态,补偿重复靠强制幂等键与自动对账兜底。
案例拆解的价值不在复述事故,而在提取可迁移的结构:时间线只写事实,证据链要能确认机制,根因拆三层,改进项覆盖预防、发现、控制,影响面必须可复算。把这套结构变成模板与代码检查,案例才会从“故事”变成组织记忆。
下一章讨论如何把复盘结论转成架构规则与治理动作:架构评审、技术债适应度函数、规则自动化。
阅读导航:上一节:7.2 复盘方法与改进闭环 · 下一节:8.1 架构评审与决策权 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。