《游戏服务端架构·平台化与架构治理》7.1 故障树与定位路径

《群岛纪元》在赛季首日出现登录失败,团队从全服大盘一路排查到渠道回调,却差点被一条变更记录带偏。本节按登录失败、掉线、卡顿、结算丢失、商城异常五类症状给出排查树,强调先确认影响面、再按客户端到第三方分层、最后用独立证据验证假设,并附可运行的 Go 路由与证据判定代码。

症状不是原因

《群岛纪元》赛季首日 10:00,登录失败率从 0.4% 跳到 31%。值班同学看到网关大量 502,又发现十二分钟前有一次登录服务发布,于是立即回滚。回滚后失败率没有下降,反而涨到 35%。真正的根因是第三方渠道登录回调超时,登录服务的 HTTP 客户端没有超时上限,工作协程被逐个占满,最终连本地账号登录也拿不到协程。

这次误判的代价不是回滚本身,而是把最宝贵的二十分钟花在了错误的分层上。发布记录与故障时间相邻,但相邻不等于因果;网关 502 是下游超时后的表象,不是根因。症状是玩家能描述的现象,原因是系统里可验证的机制,两者之间隔着一棵排查树。 以下时间、比例与人数均为叙事示例,不是真实运营数据。

这一节讨论三件事:影响面怎么先画出来,五类高频症状分别从哪里切入,以及什么样的证据才配叫“确认”。第 6.3 节已经定义了事故分级与响应组织,这里不重复;本节只回答“从哪一层开始查、查到什么程度算查清”。

先画影响面,再选排查路径

故障发生后,第一反应往往是打开日志搜索,这是最低效的起点。日志只能回答“某次请求发生了什么”,不能回答“有多少人受影响、影响是否在扩大”。正确的第一步是用三个维度画影响面:

维度要回答的问题典型数据源
空间全服、单区、单渠道还是单机型登录/在线大盘按维度拆分
时间从什么时候开始,是否与变更窗口重合1/5/15 分钟窗口对比
趋势扩大、持平还是收敛同一指标的环比与同比

影响面决定的是策略,不是细节。如果失败率仍在扩大、且已经影响登录或支付这类核心路径,应先止血再定位:回滚、限流、降级、切流,都是合法动作。如果影响面稳定在一个小区服,且核心路径可用,则可以继续收集证据,避免为了一个局部问题触发全服回滚。

这里有一个容易被忽略的判断:影响面本身也是证据。 只有特定机型失败,指向客户端;只有特定渠道失败,指向第三方;所有区服同时失败但老玩家正常,指向新会话链路。把影响面写成一句话,再进入分层,比直接扎进日志更快。

五类症状的排查树

不同症状的第一问完全不同。下面五类覆盖了游戏服务端最常见的玩家反馈,每一类都给出第一问、常见误判和首个高价值证据。

症状第一问常见误判首个高价值证据
登录失败握手失败还是鉴权失败看到网关 502 就查网关按版本/渠道拆分的失败率
掉线心跳断开还是业务主动断开一律归因运营商网关心跳失败率与连接数
卡顿客户端帧率低还是服务端 tick 慢先怪玩家手机tick p99 与 GC 停顿
结算丢失请求没到还是到了没落账直接补发请求 ID 与账本流水
商城异常展示错、扣款错还是发货错先回滚整个商店订单状态机与渠道回调

以登录失败为例,排查顺序是:先按版本、渠道、机型拆分失败率,确认是否集中;再看网关握手成功率与限流拒绝率,区分“连不上”和“连上了被拒”;然后看登录服务的 RPC 错误码与会话写入,区分“业务失败”和“依赖超时”;最后才看账号库、缓存与第三方回调。这个顺序的价值在于每一步都能排除一层,而不是在日志里碰运气。

掉线与卡顿常被混为一谈。掉线是连接生命周期问题,卡顿是单帧或单次 tick 的时延问题。掉线要看心跳与连接数,卡顿要看 tick 耗时分位与 GC;两者的证据源不同,排查树也不该共用。

结算丢失与商城异常则必须先把“请求是否到达”与“业务是否完成”分开。很多补发事故的起点,是值班同学看到玩家截图就直接补发,没有先查请求 ID 是否已经在账本里落过一次。第 3.2 节讨论过 GM 命令的幂等与补偿,这里只强调定位阶段的事实顺序:先确认请求轨迹,再决定是否补偿。

分层定位:每一层只回答一个问题

分层不是把系统画成架构图,而是给每一层分配一个只能由该层回答的问题。问题分配错了,指标就会互相污染。

层只回答的问题边界指标
客户端是否只有特定版本、机型或网络类型客户端错误码、崩溃率
网关连接是否建立,是否被限流或摘除握手成功率、限流拒绝率
逻辑业务处理是否完成,卡在哪个依赖RPC 错误率、tick p99、协程数
存储读写是否及时且一致延迟、主从延迟、热 key、连接池
第三方外部依赖是否按约定履约回调成功率、对账差异、状态页

分层的判据是边界指标:跨过边界时,请求的形态或责任主体发生了变化。网关到逻辑的边界是“连接已建立、请求已解码”;逻辑到存储的边界是“业务已决定读写什么”。如果一个问题在边界两侧都能解释,说明指标选得不对,应该换一个更能区分责任的指标。

《群岛纪元》的误判就发生在边界上:网关 502 看似是网关问题,但网关的握手成功率正常、限流拒绝率为零,真正异常的是网关到登录逻辑的上游超时。只要先看“握手成功率”这个边界指标,就能把网关排除,而不是被 502 的状态码带走。

走查:把《群岛纪元》的定位过程摊开

把这次事故按排查树重放一遍,可以看到每一步只回答一个问题,而且每一步都有退出条件。

步骤检查看到的证据结论
1影响面拆分全服失败率 31%,渠道 A 为 78%,本地账号为 3%集中在渠道 A 登录
2网关边界握手成功率 99.8%,限流拒绝率为 0排除网关容量与证书
3登录逻辑登录 RPC 超时,服务协程数持续上涨卡在登录逻辑内部
4链路追踪请求在渠道回调 HTTP 调用处停留超过 30 秒指向第三方回调
5第三方状态渠道回调成功率降至 12%,状态页公告异常确认外部依赖故障

第 1 步的结论是“集中在渠道 A”,不是“渠道 A 有问题”。这两句话差别很大:前者是事实,后者是假设。第 4 步的追踪与第 5 步的状态页属于不同来源的直接证据,加上第 3 步的协程指标,才满足“确认”的门槛。

这次走查还暴露了一个架构问题:登录服务对渠道回调没有设置超时,导致外部依赖的延迟被放大成协程耗尽。即使第 5 步已经确认根因,改进项也不能只写“等渠道恢复”,而要写“为外部调用设置超时与熔断”。定位回答“哪里坏了”,改进回答“为什么这里坏了会拖垮全局”。

止血与定位的切换点

定位和止血不是先后关系,而是两条并行的线。判断何时切换,可以看四个条件:

条件倾向止血倾向继续定位
影响路径登录、支付、结算等核心路径展示、排行等非核心路径
趋势仍在扩大已持平或收敛
可逆性回滚、降级成本低且可快速验证动作不可逆或会破坏证据
信息量当前假设无法在十分钟内验证再采一组证据就能确认

需要止血时,动作要尽量保留证据:重启前先抓协程栈与堆快照,摘除节点前先保留失败实例,回滚前先导出配置与变更记录。很多团队在恢复后才发现现场没了,复盘只能靠回忆补时间线。

反过来,不需要止血时也不要为了“做点什么”而全员上线。无目标的重启、扩容、切流会改变现场,让原本清晰的证据链变得不可解释。定位阶段最贵的资源不是人力,而是未被污染的现象。

证据分级:什么才算“确认”

定位过程中会收集到很多材料,但材料不等于证据。至少要把它们分成五类,并明确每类能证明什么、不能证明什么。

证据能证明不能证明
指标现象规模、起始时间与趋势具体代码路径
日志单次请求发生了什么全局影响面
追踪跨服务耗时与失败点未被采样的请求
变更时间上的相关性因果关系
玩家反馈主观体验与个例普遍性

一条结论要被称为“确认”,至少需要两条**「类型+来源」组合不同**的直接证据。例如“登录逻辑被渠道回调阻塞”可以由两条证据确认:登录服务协程数指标(来源:监控)持续上涨,以及追踪里登录请求在渠道 HTTP 调用处停留超过 30 秒(来源:链路系统)。如果只有一条直接证据,其余都是间接相关,那么结论仍然是假设。

这条规则刻意不把“最近有变更”当作确认依据。发布、配置、活动、DDL 都可能与故障时间相邻,但相邻只是候选。变更要成为证据,必须能解释故障的机制:哪个参数变了、经过哪条代码路径、为什么会导致当前现象。解释不了机制的变更,只能留在候选列表里。

同样,玩家反馈不能单独确认普遍性。一个玩家的截图能证明“存在这个现象”,不能证明“很多人遇到”。要把反馈变成证据,必须回到大盘或日志里找到对应的规模。

定位顺序的三个失败路径

失败路径一:最近变更谬误。 故障前动过什么,就先怀疑什么。这是合理的启发式,但不能跳过机制验证。《群岛纪元》的回滚之所以无效,是因为发布只是时间上相邻,真正的阻塞点在第三方回调。正确的做法是:把变更列为候选,同时用边界指标验证它能否解释现象。

失败路径二:单玩家样本外推。 客服转来一张截图,团队据此认定“全服都这样”。样本外推的成本很低,代价是可能为一个局部问题触发全服动作。先按维度拆大盘,再决定样本是否具有代表性。

失败路径三:过早收敛。 找到第一个能解释现象的原因就停止排查。复杂故障常有多个 contributing factor:缓存击穿是触发因素,缺少降级是放大因素,告警延迟是恢复慢的原因。只修第一个,下一次还会以另一种形态出现。定位阶段至少要回答:直接原因是什么、为什么系统没有兜住、为什么没有更早发现。

检查表:十分钟内该做完什么

事故的前十分钟决定后面两小时。下面这张检查表可以直接贴在值班手册里:

  1. 用空间、时间、趋势三个维度写出影响面,一句话即可。
  2. 判断是否需要先止血;需要就执行,不要等根因。
  3. 对齐故障起点前后 30 分钟的变更,列成候选而不是结论。
  4. 按客户端、网关、逻辑、存储、第三方逐层看边界指标,每层只回答一个问题。
  5. 为当前最重要的假设写下否证条件:看到什么就放弃它。
  6. 收集至少两条「类型+来源」不同的直接证据,再宣布确认。
  7. 把已排除的层记录下来,避免换班后重复排查。

第 5 条最容易被跳过。没有否证条件的假设会自我强化:支持它的证据被记住,反证被解释掉。写下否证条件,等于提前约定“什么情况下我们承认错了”。

可运行片段:症状路由与证据判定

internal/faulttree 把上面的规则落成两个可测试的部分。tree.go 维护五类症状的检查表:每条检查有分层、问题、所需证据,以及“命中/未命中”后的下一步;Route 先放三条影响面检查,再按客户端到第三方排序,保证同样症状每次得到同样的排查路径。

evidence.go 负责判定假设。规则只有三条:任意一条直接反证即可排除;至少两条「类型+来源」不同的直接支持证据才算确认;其余一律是假设。这样“最近有变更”“玩家都在骂”“单条日志报错”都不会被误判成确认。

// file: internal/faulttree/evidence.go
// tier: L1
// verify: go test ./internal/faulttree/
package faulttree

// Kind 是证据类型。「类型+来源」组合不同,才算两条独立证据。
type Kind string

const (
	KindMetric       Kind = "metric"
	KindLog          Kind = "log"
	KindTrace        Kind = "trace"
	KindChange       Kind = "change"
	KindPlayerReport Kind = "player_report"
)

// Evidence 是一条证据。Direct 表示它直接指向或排除假设,而不是间接相关。
// Supports 为 false 时是反证:它排除假设,而不是支持假设。
type Evidence struct {
	Kind     Kind
	Source   string
	Direct   bool
	Supports bool
}

// Hypothesis 是待验证的结论。没有足够独立直接证据时,它只是假设。
type Hypothesis struct {
	Claim    string
	Evidence []Evidence
}

// Verdict 是假设的判定结果。
type Verdict string

const (
	VerdictConfirmed  Verdict = "confirmed"
	VerdictRefuted    Verdict = "refuted"
	VerdictAssumption Verdict = "assumption"
)

// Verdict 的规则:
//  1. 任意一条直接反证即可排除假设;
//  2. 至少两条「类型+来源」组合不同的直接支持证据,才算确认;
//  3. 其余情况一律是假设,包括只有变更记录、只有玩家反馈、只有间接指标。
//
// 这条规则刻意不把“最近有变更”当作确认依据,因为相关性不是因果。
func (h Hypothesis) Verdict() Verdict {
	type origin struct {
		kind   Kind
		source string
	}
	confirmed := make(map[origin]bool)
	for _, e := range h.Evidence {
		if !e.Direct {
			continue
		}
		if !e.Supports {
			return VerdictRefuted
		}
		confirmed[origin{e.Kind, e.Source}] = true
	}
	if len(confirmed) >= 2 {
		return VerdictConfirmed
	}
	return VerdictAssumption
}

两段合起来,排查过程就从“凭经验翻日志”变成了“按固定路径收集证据、按固定规则判定结论”。tree.go 与 evidence.go 同包,测试覆盖路由完整性、分层顺序、检查项唯一性、路由确定性,以及证据判定的确认、排除与假设三种结果。

本机验证命令与结果:

cd .local/checks/gsarch-lab
gofmt -l internal/faulttree
go test -count=1 ./internal/faulttree/

gofmt -l 无输出;go test 退出码 0,该包 5 个表驱动测试全部通过。测试只使用标准库,不依赖外部服务。

一次排查的记录长什么样

排查结束后,至少留下下面这张表。它比“已定位”三个字有用得多:

字段例子作用
影响面华东 3 服,约 8000 人无法登录决定止血优先级
首问登录握手是否成功选择第一条排查路径
分层结论网关正常、鉴权超时、账号库连接耗尽缩小到具体层
证据鉴权 P99 从 40ms 升到 3s;连接池等待 120支撑结论
否证条件若连接池空闲仍超时,则排除连接耗尽防止假设固化
结论级别已确认区分确认与假设
止血动作扩容鉴权实例并重启连接池恢复服务
待办连接池上限与慢查询治理进入复盘

这张表可以在事故频道里边排查边填,不需要等复盘再补。它的价值在于:换班的人能接着查,复盘的人不用重新问一遍,管理层能看到影响面而不是一堆术语。

记录时有两个细节容易忽略:一是时间戳要统一时区,跨区事故尤其如此;二是“已排除”的结论也要留证据,否则下一班可能把同样的路径再走一遍。

三个反模式

反模式一:从日志开始定位。 日志适合回答“这一次请求发生了什么”,不适合回答“影响面有多大”。先看大盘,再决定要不要钻日志,能省掉大量无效搜索。

反模式二:把相关性当因果。 变更、告警、玩家反馈都只是候选。不能解释机制的候选,不该进入根因栏,只能留在假设栏。

反模式三:只找直接原因。 直接原因回答“怎么坏的”,系统性原因回答“为什么系统没兜住”,逃逸原因回答“为什么没早发现”。只修直接原因,同类问题会换一个入口再来。

小结

故障定位的起点是影响面,不是日志。用空间、时间、趋势三个维度画出一句话的影响面,再决定先止血还是继续定位。五类高频症状各有第一问:登录看握手与鉴权,掉线看心跳与连接,卡顿看帧率与 tick,结算看请求轨迹与账本,商城看订单状态机与渠道回调。分层定位的关键是给每层分配一个只能由该层回答的问题,并用边界指标排除,而不是用状态码猜测。

证据要分级:指标证明规模,日志证明单次请求,追踪证明跨服务路径,变更只证明相关性,玩家反馈只证明个例。两条「类型+来源」不同的直接证据才算确认,其余都是假设。把否证条件提前写下来,是防止假设自我强化最便宜的办法。

下一节讨论事故之后:时间线怎么记、根因怎么拆到系统性层、改进项如何用负责人、截止时间、验收标准与复发检测真正闭环。

阅读导航:上一节:6.3 事故分级与响应 · 下一节:7.2 复盘方法与改进闭环 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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