《游戏服务端架构·平台化与架构治理》7.3 真实事故案例拆解

三个复合案例:活动配置多一个零导致奖励超发、排行榜缓存击穿拖垮登录、GM 批量补偿脚本重试造成重复发放。所有数字均为示例值,仅用于演示影响面计算与改进映射。每个案例给出时间线、证据、直接与系统性根因,以及预防、发现、控制三类架构改进,并附可运行 Go 影响计算代码。

案例的读法:数字是示例,结构才是重点

这一节的三个案例来自游戏服务端最常见的三类事故:配置错误、容量与缓存、权限与幂等。它们不是某家公司的真实事故记录,也不是行业基准;所有人数、时长、金额都是为演示计算而构造的示例值,使用时必须替换成自己的账本与监控数据。

案例的价值不在数字,而在结构:同样的时间线写法、同样的三层根因、同样的“预防/发现/控制”改进映射。把结构学会,换一个业务也能套用;只记住数字,下次遇到不同形态的事故仍然无从下手。

三个案例分别对应本章前两节的方法: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 个表驱动测试全部通过。测试覆盖示例影响计算、比例与时长边界、时间线乱序与缺节点、改进类别覆盖,以及模板返回值的确定性。

案例复盘的检查表

拆解案例时,可以用下面八个问题自检:

  1. 时间线是否只写事实与来源,没有混入评价?
  2. 发现、止血、恢复三个节点是否都有时间戳?
  3. 直接原因能否解释现象,而不是只描述现象?
  4. 系统性原因是否回答了“为什么影响这么大”?
  5. 逃逸原因是否回答了“为什么没有更早发现”?
  6. 影响面的每个数字是否有口径与来源?
  7. 改进项是否同时覆盖预防、发现、控制?
  8. 每条改进项是否有负责人、截止时间、验收标准、复发检测?

第 3 题最容易出错。 “数据库 CPU 100%”不是直接原因,而是结果;“缓存 key 集中过期”才是机制。把结果当原因,改进项就会落在“扩容数据库”上,而不是“消除集中过期与同步依赖”。

三个案例放在一起看

维度配置超发缓存击穿补偿重复
直接原因发布重试未校验幂等热 key 集中过期脚本无幂等键
系统性原因缺少总额上限与发布幂等缺少依赖隔离与请求合并缺少执行账本与对账
逃逸原因只监控错误率,不监控发奖速率只监控成功率,不监控缓存命中只监控脚本退出码
预防schema 校验、dry-run依赖隔离、单飞强制幂等键
发现发奖速率告警命中率与源站 QPS 告警执行记录与账本对账
控制一键暂停活动降级到静态榜单自动补偿与人工复核

三行原因、三类改进,是案例拆解的最小结构。只写直接原因,改进会停留在“加一个判断”;补上系统性与逃逸原因,才会去改隔离、预算与观测。

三个反模式

反模式一:只写时间线,不写证据链。 时间线回答“发生了什么”,证据链回答“怎么知道”。没有证据链的案例,读起来像故事,无法被复用。

反模式二:把示例数字当基准。 本篇的人数和金额都是演示值。直接拿去做容量规划或对外沟通,会得到错误结论。用自己的账本与监控重新计算,是使用案例的最低要求。

反模式三:改进项只做预防。 加校验、加审批当然重要,但如果没有发现与控制,下一次故障仍然会以未知形态扩大。三类改进的最低配置,应该成为案例复盘的硬性要求。

小结

三个案例的直接原因不同,系统性原因却指向同一组问题:关键路径缺少隔离、高风险动作缺少预算、执行结果缺少对账。配置超发靠 schema 与总量 dry-run 防住,缓存击穿靠依赖隔离与请求合并改变故障形态,补偿重复靠强制幂等键与自动对账兜底。

案例拆解的价值不在复述事故,而在提取可迁移的结构:时间线只写事实,证据链要能确认机制,根因拆三层,改进项覆盖预防、发现、控制,影响面必须可复算。把这套结构变成模板与代码检查,案例才会从“故事”变成组织记忆。

下一章讨论如何把复盘结论转成架构规则与治理动作:架构评审、技术债适应度函数、规则自动化。

阅读导航:上一节:7.2 复盘方法与改进闭环 · 下一节:8.1 架构评审与决策权 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查