事故时如何有效诊断问题与修正错误?
2026年7月29日
在软件工程的工作中,难免会遇到各类问题。当遇到问题时,需要有效地排查问题,并做好错误修正。软件工程师的工作核心是解决问题,因此如果能提升自己的解决问题能力,对软件工程师职业发展来说会很有帮助。
很多人会认为,有些人天生比较会 debug,或者 debug 很吃经验;然而,实际上不论天赋与经验多少,都可以通过一些有结构的方式,来加速问题解决。在这篇主题文,我们会来谈在 Google 的 SRE 专书中介绍的有效排查与解决问题 (effective troubleshooting) 方法。
解决问题的四步骤
具体来说,Google 的 SRE 专书在这个章节中,把问题解决拆成两个核心要件,分别是排查问题的方法,以及对系统本身的理解。由于大家平常在工作中面对的系统都有所不同,所以这里不会特别拉出对系统的理解这点来讨论,而是会针对排查问题的方法来深谈。
我们可以把软件工程排查问题的方法,当成一种假设验证的过程 (hypothetico-deductive method)。就像科学家在做实验,会先观察现象、提出假设,通过不同方法来验证假设,然后在过程中不断排除错误假设,借此缩小范围并最终验证假设。
举例来说,假如今天网站突然跑得很慢,如果没有用假设验证法,可能会乱枪打鸟地猜「会不会是因为 Redis 出问题」或者「会不会是昨天合并的 PR 导致的」,当然这样猜也有可能会猜中问题所在,但是往往会比较没效率。
更理想的方式,是先找出哪个地方出错,例如「在 XX API 的监控面板中看到大量的错误,错误类型跟超时有关,P95 的延迟确实大幅上升」。而当定位到某支 API 变慢,也不能直接就乱猜是缓存出问题,或者是数据库出问题,而是需要进一步查看,例如「如果是因为数据库出问题,那数据库的延迟应该会上升,数据库延迟的监控没有显著变化,CPU 的用量也没有明显改变,所以问题应该出在其他地方」。
进一步说,Google 的 SRE 专书把问题解决方法拆成下面这样的流程,以下让我们逐一说明。
分流与检查
在遇到问题时,分流 (triage) 是第一件要做的事。所谓的分流是指判断问题多严重、影响多大,以及该用什么样的事故等级来应对。在实务上可能遇到各种问题,但不是每一个都该用最高规格处理;如果小事也动大工程,很快就会耗尽资源 (备注:推荐回顾 软件工程师该如何做好监控 (monitoring)? 有谈不同的优先等级)。
在分流阶段,重点不是找 bug,而是判断事件的影响,例如影响到多少用户、功能坏掉是只有部分还是整个服务、问题是限制在局部还是逐渐在扩大等等。
在分流阶段有了初步判断后,不要一开始就急着追根因。这个阶段最重要的,是尽可能让系统撑着。在 Google 的 SRE 专书有特别强调要「尽可能让系统在当下的坏情况下持续运作 make the system work as well as it can under the circumstances」。
举例来说,可以把流量从坏掉的集群,切到其他还在运作的集群。假如问题是会扩散的,甚至有时要当机立断舍弃部分流量,不处理请求;这听起来有点反直觉,但如果舍掉部分流量能避免整个系统崩塌,做出这种决策反而是更合理的。
书中用了止血 (stop the bleeding) 一词,这就类似在急救时,如果患者正在大出血,医生不会慢条斯理地研究病患到底为何受伤,而是会先止血,确保问题不会扩大。在面对系统的问题时,也该用同样方式;因为终端用户不会在乎工程团队能不能完美找到根因,只会在乎产品能不能被使用。
在有了初步的分流与处置后,接着会进到检查阶段。理想的状况下,系统要能够让工程团队看见问题,要能做到这样就需要建设完整的监控与可观测性。先前我们在 软件工程师该如何做好监控 (monitoring)? 与 什么是可观测性(Observability)? 为什么需要可观测性? 分别有讨论这两个主题,推荐读者们可回顾。
诊断问题出在哪
在检查问题后,接着要根据看到的证据,判断问题到底比较可能出在哪里。如果想要有效诊断,书中推荐用「简化与缩小范围 simplify and reduce」的方式,把问题拆小来看。
在理想情况下,系统里每个组件,都应该有清楚定义的接口。在接口会定义需要什么输入,会返回什么输出,这些都应该要是可被验证的。因此,要判断该组件有没有出错,最直接的方式,是测试输入与输出的对应。
进一步说,我们可以把这个流程看成一条数据流,在检查的过程中问
- 这一段收到的输入是否正确?
- 这一段吐出的输出是否正确?
- 数据是在哪一段开始变怪的?
- 哪个组件前面还正常,后面就坏了?
以「用户搜索不到商品」为例,比起在整个大系统中大海捞针,可以改成诊断整个链路的数据流,从前端的请求开始检查,一路看前端送的请求是否正确、API 收到的跟前端传来的是否相同、API 背后调用搜索服务这一段有没有结果,这样一路追,就能在过程中诊断出问题在哪。
书中有提到,在比较大的系统,这样线性地追,可能要追很久。因此会推荐分而治之 (dividing and conquering),会把系统做拆分。例如实务上可以分工切成两部分,有人追前端到后端这段,另一个人追后端到其他微服务这段,这样将能更有效率辨别出问题。
在这个过程中,可能会找到几个潜在的问题点,这时我们会把这些视为假设,在有了假设后,接着要基于假设来解决问题。
测试与解决问题
在解决问题时,要同时观察假设是否有被验证。Google 的 SRE 专书有提到两种验证方式,一种是比对实际状况与理论。例如若怀疑是数据库出问题导致 API 变慢,这时应该要有连带的指标变化,例如数据库的 CPU 也该有指标上的波动,如果没有,则很可能问题不在这。
另一种做法是主动对系统做调整,然后观察反应。举例来说,如果假设新版本出问题,可以主动回滚 (rollback) 然后观察系统是否回到稳定状态;或是如果假设是功能开关 (feature flag) 切换导致问题,则可以试着关闭功能开关,然后再观察是否回到稳定状态。
就像医生在看病一样,当看到某个症状,背后可能会有不同的潜在原因,医生不见得能在一开始就精准定位。因此,医生多半会根据症状提出假设,然后做不同的检查得到更多数值来验证,或者给予某种处置后观察病人反应,借此来验证。
在实务上,问题很常会是分成表面问题与深层问题。在实际遇到事故时,应该是以止损为优先,所以先解决表面问题,先不追根究底,反而会是更重要的。举例来说,假如支付系统出问题,导致用户无法正常结账,观测面板发现新的部署版本造成错误率上升,这时应该要立即回滚,以系统恢复为主要目标;而不是追根究底去解决,对终端用户的影响会比较小。
在做假设的测试时,书中有提到一个特别值得留意的观点,就是要测试有互斥的替代可能。换句话说,一个好的测试应该要能清楚地告诉让你知道你「如果结果是 A,就比较支持这个假设;如果结果是 B,就比较支持另一个假设」。
举例来说,如果搜索的 API 返回 502,可能做的假设是对接到前端的 BFF 层坏掉,以及 BFF 层背后调用搜索微服务坏掉。这时如果能够直接对微服务层发请求,如果拿到的结果是坏掉的,就能够很明确地判别问题。而在确定问题所在后,就能够根据问题做相关的处置。
加入 E+ 会员方案
如果你觉得这篇分享有帮助,想阅读更深入的内容,欢迎加入 E+ 会员。除了有会员专属的深度文与影片,探讨前后端开发、AI 工程、职业发展,同时有 Discord 社群一同交流与成长。
许多 E+ 会员通过所任职公司的教育训练补助 (E+ 会开立有统编的发票),不用花自己的钱,也能每周通过深度主题文与社群交流,拓展技术视野、加速职业成长。