周六凌晨3点
你的手机震动起来。PagerDuty。警报显示”API响应时间 > 5秒,错误率 > 15%”。你半梦半醒,眯着眼看着手机,第一反应是检查这是否是误报。不是。生产环境出问题了。
你在接下来30分钟内的行动将决定这只是一次小故障还是一场职业生涯级别的灾难。而且大多数工程师——即使是经验丰富的——在这个时刻处理得很糟糕。不是因为他们缺乏技术技能,而是因为他们缺乏一个系统性的方法。
在花了十二年的时间从10人初创公司到财富500强企业调试生产事故,并进行了200多次事后回顾后,我将真正有效的方法提炼成了一个可重复的框架。这不是理论。这是一本在凌晨3点当一切都毫无意义且一切已损坏时经过压力测试的行动手册。
分类框架:前15分钟
事故发生的前15分钟是最关键的,也最容易被浪费。工程师们直接跳到假设——”可能是数据库”,”我敢打赌有人部署了什么”——在了解问题的实际范围之前就开始追逐理论。
步骤1:确定影响范围(0-5分钟)
在修复任何问题之前,你需要知道什么坏了。不是你认为坏了的,而是根据数据测量实际坏了什么。
打开你的主要仪表板。如果你使用的是Grafana、Datadog或New Relic,你应该有一个事故概览仪表板,显示:
- 所有服务的错误率
- 响应时间百分位数(p50、p95、p99)
- 请求吞吐量
- 活跃用户数或会话数
- 数据库连接池利用率
- 队列深度(如适用)
目标是在五分钟或更短时间内回答三个问题:
- 受影响的用户比例是多少? 所有用户、特定地区、特定用户群体?
- 业务影响是什么? 交易是否失败?数据是否丢失?或者只是”变慢”?
- 什么时候开始的? 这对于关联分析至关重要。
# 快速Datadog查询以评估影响范围
# 显示过去30分钟按服务划分的错误率
sum:trace.http.request.errors{env:production} by {service}.as_rate()
/ sum:trace.http.request.hits{env:production} by {service}.as_rate()
步骤2:检查明显问题(5-10分钟)
在你开始追踪微服务调用图之前,先检查导致70%生产事故的三件事:
1. 最近的部署。 在你的部署仓库中运行 git log --oneline --since="2 hours ago",或者检查你的 CD 管道历史。如果有人在过去两小时内进行了部署,那就是你的主要嫌疑对象。不要让任何人告诉你”我的变更不可能导致这个问题”——我已经听到这句话大约 500 次,其中大约 400 次是错误的。
# 检查各服务的最近部署
kubectl get events --sort-by=.metadata.creationTimestamp
--field-selector reason=Pulling
-A --since=2h
# 或通过你的部署工具
argocd app list --output json |
jq '.[] | select(.status.operationState.finishedAt > "2026-03-31T") | {name, syncStatus: .status.sync.status}'
2. 基础设施变更。 是否有人修改了安全组?轮换了证书?缩减了节点组?检查你的基础设施变更日志——Terraform Cloud 审计日志、AWS CloudTrail,或你的团队使用的任何工具。
3. 外部依赖。 你的支付服务提供商是否宕机?第三方 API 是否超时?检查你的关键依赖的状态页面,并查看出站 HTTP 调用是否存在延迟增加的情况。
步骤 3:沟通(第 10-15 分钟)
这是大多数工程师失败的地方。你已经盯着仪表板看了 10 分钟,对问题有了大致了解,但还没有告诉任何人。与此同时,你的工程副总裁正在收到愤怒客户的邮件,而支持团队正在向用户说”我们正在调查”,但没有任何上下文。
在你的事件频道发布状态更新。这不一定是完美的。类似这样的内容:
事件更新 — 03:12 UTC
影响:约 30% 的 API 请求返回 500 错误,主要影响结账流程。
开始时间:大约 02:45 UTC。
当前理论:调查与 02:38 UTC 的 order-service 部署的相关性。
下次更新:15 分钟后。
这只需要 60 秒编写,但可以节省下游数小时的混乱。
调查:系统化日志关联
一旦你完成分类和沟通,就该实际找到根本原因了。这时,拥有适当的可观测性工具会带来百倍回报。
三大支柱的实践
你已经听说过”可观测性的三大支柱”——日志、指标和追踪。理论上,它们能给你提供完整的可见性。实际上,大多数团队的情况是:
- 指标显示某些问题,但不是什么问题
- 日志要么过于冗长,要么缺少关键信息
- 追踪覆盖了 60% 的服务,但采样方式似乎总是错过失败的请求
以下是在事件期间如何有效使用每一种方法。
从指标开始
指标告诉你在哪里寻找问题。它们是预聚合的,查询速度快,并且善于揭示模式。将 RED 方法(速率、错误、持续时间)应用于请求路径中的每个服务,将迅速缩小你的关注范围。
# Prometheus 查询:找出错误率增长最快的服务
# 将当前错误率与同一时间昨天的错误率进行比较
(
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service)
)
/
(
sum(rate(http_requests_total{status=~"5.."}[5m] offset 1d)) by (service)
/ sum(rate(http_requests_total[5m] offset 1d)) by (service)
)
此查询将每个服务的当前错误率除以昨天的错误率。值为 10 表示该服务的错误率是正常水平的 10 倍。按此值排序,问题服务几乎总是在最前面。
转向追踪
一旦你知道哪个服务行为异常,追踪会告诉你为什么。单个分布式追踪会向你展示请求通过系统的完整旅程,包括哪个服务调用缓慢或失败。
在 Jaeger、Tempo 或 Datadog APM 中,搜索受影响服务在受影响时间窗口内带有错误状态的追踪。查看 span 瀑布视图。你正在寻找:
- 带有错误标签的 spans
- 异常长的 spans(与相同 span 的典型持续时间相比)
- 缺失的 spans(应该在追踪中出现但没有出现的服务)
- 重试模式(同一下游调用多次出现)
使用日志获取详细信息
日志填补了指标和追踪无法覆盖的空白。一旦你确定了有问题的服务和失败的操作,就在受影响的时间窗口内搜索该服务的日志。
这里的关键技术是使用追踪 ID 进行日志关联。如果你的服务将追踪 ID 传播到其日志输出中(它们应该这样做——对于任何分布式系统来说,这是不可协商的),你可以从失败的追踪中获取一个追踪 ID,并在所有服务的日志中搜索它。
# 搜索与特定追踪相关的所有日志
# 使用 Loki 和 LogQL
{namespace="production"} |= "trace_id=abc123def456"
# 使用 Elasticsearch/OpenSearch
GET /logs-*/_search
{
"query": {
"bool": {
"must": [
{ "match": { "trace_id": "abc123def456" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"sort": [{ "@timestamp": "asc" }]
}
这为你提供了关于该特定请求所发生事情的完整故事,按时间顺序排列,涵盖了它接触的每个服务。这是你在分布式生产系统中能获得的 closest thing to a debugger breakpoint(最接近调试器断点的东西)。
实践中的”5个为什么”与理论
大家都知道”5个为什么”技术。通过连续问五次”为什么”来找到根本原因。理论上,这种方法很优雅。但实际上,通常是这样的:
为什么结账失败了? 订单服务返回了500错误。
为什么订单服务返回了500? 数据库查询超时了。
为什么数据库查询会超时? 表有5000万行且缺少索引。
为什么缺少索引? 三个月前应该添加索引的迁移脚本静默失败了。
为什么迁移脚本会静默失败? 我们的迁移工具没有检查迁移是否实际成功应用。
这是教科书式的版本。而在大多数团队中,实际发生的情况是这样的:
为什么结账失败了? 订单服务返回了500错误。
为什么? (…检查日志30分钟后…)看起来数据库连接池耗尽了。
为什么? 因为查询很慢。
为什么查询很慢? (DBA和应用程序开发者之间爆发争论,讨论是查询语句有问题还是数据库需要更多资源。)
为什么? (没人再问了,因为已经有人修复了问题,事件被”解决了”。)
5个为什么的问题不在于技术本身——而在于团队过早停止、被指责分散注意力,或者接受表面答案。
让5个为什么真正发挥作用
规则1:持续提问,直到找到系统性故障,而不是人为失误。 “因为开发者忘记添加索引”不是根本原因。”因为我们的迁移流程没有验证成功完成”才是。你正在寻找的是有问题的流程、缺失的安全措施、缺失的自动化检查——而不是犯错的人。
规则2:分支提问,而不仅仅是深入挖掘。 在每个”为什么”环节,可能存在多个促成因素。数据库变慢是因为缺少索引,也是因为营销活动导致流量增加了3倍,还因为连接池对于实例类型来说太小了。这三个问题都需要解决。
规则3:用数据验证每个答案。 不要接受”我认为是X”——检查确认X实际发生的日志、指标和追踪。我见过太多事后复盘,大家接受的”根本原因”后来被证明是错误的,而实际问题两周后又再次出现。
实战故事:真实的事件模式
经过多年的事件响应,某些模式反复出现。以下是我最常看到的几种模式,以及如何识别和处理它们。
模式1:级联超时
服务 A 调用服务 B 时设置了 5 秒超时。服务 B 调用服务 C 时也设置了 5 秒超时。服务 C 响应缓慢,导致 B 等待 C 时超时。但 B 在等待期间仍然保持与 A 的连接。最终,A 的连接池因等待 B 而耗尽,现在整个系统因为一个下游服务缓慢而崩溃。
如何识别:你会看到延迟在服务图中向上游传播,连接池耗尽错误出现在本身没有做任何错误操作的服务中。
解决方案:实施适当的超时预算(每个下游调用获得总请求预算的一部分)、断路器(停止调用失败的服务)和舱壁模式(隔离连接池,使一个缓慢的依赖项不会耗尽所有连接)。
// Resilience4j 断路器配置
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.build();
CircuitBreaker breaker = CircuitBreaker.of("serviceC", config);
Supplier<Response> decorated = CircuitBreaker
.decorateSupplier(breaker, () -> serviceC.call(request));
模式 2:周四下午 2 点的性能缓慢
每周大约在同一时间,性能会下降。程度不足以触发警报,但用户会注意到。几小时后它会自行恢复。这让团队感到抓狂,因为当你调查时,问题已经消失了,很难调试。
如何识别:查看按周粒度度量的指标仪表板。你会看到与定时任务、批处理、周报、数据库维护任务甚至特定客户群体的流量模式相关的规律性模式。
解决方案:检查在该时间运行的定时任务。罪魁祸首几乎总是一个随时间增长而变大的每周批处理作业,现在它与生产流量竞争数据库连接或 CPU。将其移动到只读副本、安排在非工作时间运行,或为其分配专用资源。
模式 3:需要三天才会显现的内存泄漏
你的服务在部署后运行良好。通过所有测试。运行 48-72 小时后表现优异。然后响应时间开始增加,GC 暂停变长,最终服务因 OOM 被重启,并由 Kubernetes 重新启动。循环重复。
如何识别:容器内存图上的锯齿状内存使用模式。每次重启都会重置到基线,然后内存在几天内线性上升。
解决方案: 你需要一个已运行一天或两天的实例的堆转储。在 Java 中:
# 在运行中的容器上触发堆转储
kubectl exec -it order-service-7d4f8b9-x2k4n --
jmap -dump:live,format=b,file=/tmp/heap.hprof $(pgrep java)
# 复制到本地进行分析
kubectl cp order-service-7d4f8b9-x2k4n:/tmp/heap.hprof ./heap.hprof
# 在 Eclipse MAT 或 IntelliJ 分析器中打开
# 查找保留大小异常高的对象
常见原因:已注册但从未移除的事件监听器、没有淘汰策略的缓存、在线程池中累积的线程局部变量,以及日志代码中无限制增长的 StringBuilder 对象。
模式4:”什么都没变”事件
每个人都发誓什么都没变。没有部署,没有配置更改,没有基础设施更新。但系统行为却不同了。这是最令人沮丧的模式,因为它感觉系统在欺骗你。
现实情况: 总是发生了某些变化。常见的隐藏变化:
- SSL 证书自动续订,新证书有不同的中间 CA,验证速度更慢
- 依赖项更新了其 API 行为(即使没有版本变更 — SaaS API 经常这样做)
- DNS TTL 过期,现在解析指向不同的端点
- 云提供商维护事件将你的实例迁移到不同的硬件
- 由于营销活动、节假日或工程团队无人知晓的病毒式社交媒体帖子,流量模式发生了变化
解决方案: 你需要全面变更跟踪。除了部署日志外,还要跟踪 DNS 变更、证书轮换、云提供商事件(AWS Health Dashboard、GCP Status)、依赖版本锁定,甚至像营销活动这样的业务事件。建立”可能影响生产环境的事项”共享日历的团队,每季度可以节省数小时的调试时间。
真正有效的事件后审查
大多数事件后审查(也称为事后分析或回顾)都是浪费时间。它们遵循模板,有人填写空白,团队在会议上点头同意,创建行动项目,但实际上什么都没改变。两个月后,同类事件再次发生。
这是一个我见过能产生实际效果的模板,因为它专注于系统性改进而非叙事性描述。
五部分审查
第一部分:时间线(最多1页)
带有时间戳的事件按时间顺序列表。从第一个促成因素开始(例如引入错误的部署),而不是从警报触发时开始。包括谁做了什么,他们观察到了什么,以及他们做了什么决定。这是事实性的 — 没有分析,没有指责。
第2节:影响(3-5个要点)
量化的影响。不是”一些用户受到了影响”,而是”在47分钟内有14,200次结账尝试失败,导致估计损失83,000美元收入”。包括面向用户的影响、数据完整性的影响和SLA的影响。
第3节:出了什么问题及原因(实际分析)
这里是5 Whys分析所在的地方。多个促成因素,每个都追溯到其系统性根本原因。以无指责的语气编写——”部署管道没有针对暂存数据库运行集成测试”,而不是”开发者没有测试他们的迁移”。
第4节:哪些方面做得好
这一节经常被跳过,但它很重要。警报是否在预期时间内触发?团队是否有效沟通?运行手册是否有帮助?强化有效的内容与修复无效的内容同样重要。
第5节:行动项目(负责人和截止日期)
这是评审成功或失败的地方。每个行动项目必须有:
- 一个明确的负责人(不是团队,而是一个人)
- 具体的交付成果(不是”改进监控”,而是”为订单服务添加连接池利用率警报,阈值为80%”)
- 截止日期(不是”下一个冲刺”,而是”2026年4月15日”)
- 优先级:P0(防止此确切事件再次发生)、P1(防止类似事件)、P2(一般性改进)
跟踪行动项目的完成情况。如果你的团队在规定的截止日期内完成了不到70%的事件后行动项目,那么你的评审过程只是在做表面文章,而不是真正的改进。
行动项目跟踪模板
## 事件后行动项目 — INC-2026-0892
| ID | 描述 | 负责人 | 优先级 | 截止日期 | 状态 |
|----|-------------|-------|----------|----------|--------|
| 1 | 为order-service添加连接池利用率警报(>80%) | @jchen | P0 | 2026-04-08 | 开放 |
| 2 | 在CI管道中添加迁移验证步骤 | @mpark | P0 | 2026-04-15 | 开放 |
| 3 | 在订单→库存调用路径上实现断路器 | @alee | P1 | 2026-04-22 | 开放 |
| 4 | 为结账流程添加针对暂存数据库的集成测试 | @jchen | P1 | 2026-04-22 | 开放 |
| 5 | 编写数据库连接池耗尽的运行手册 | @srao | P2 | 2026-04-30 | 开放 |
构建你的可观测性栈:实用建议
如果你正在从头开始设置可观测性或彻底改造现有设置,以下是我根据部署规模在2026年给出的建议。
小型团队(< 20名工程师,< 10个服务)
使用单一供应商。Datadog、Grafana Cloud 或 New Relic。一站式方法可以消除集成难题,并为您提供开箱即用的关联指标、日志和追踪。是的,其单位成本比自托管更高,但运行自己的 Prometheus + Loki + Tempo 堆栈的运营成本也不容小觑。
中型团队(20-100 名工程师,10-50 个服务)
这是自托管与托管方案权衡变得有趣的地方。一个合理的折中方案:使用 Grafana Cloud 进行仪表盘和告警,配合自托管收集器(OpenTelemetry Collector),为您提供日后切换后端的灵活性。在您的应用程序代码中使用 OpenTelemetry SDK 仪器化 — 它是供应商中立的,并且在 2026 年前已获得所有主要语言的良好支持。
# 中型设置的 OpenTelemetry Collector 配置
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1000
tail_sampling:
policies:
- name: error-sampling
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-sampling
type: latency
latency: {threshold_ms: 1000}
- name: baseline-sampling
type: probabilistic
probabilistic: {sampling_percentage: 10}
exporters:
otlphttp/grafana:
endpoint: https://otlp-gateway-prod-us-central-0.grafana.net/otlp
headers:
Authorization: "Basic ${GRAFANA_CLOUD_TOKEN}"
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlphttp/grafana]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/grafana]
大型团队(100+ 名工程师,50+ 个服务)
在这个规模上,您可能需要一个专门的可观测性团队和混合解决方案。自托管的 ClickHouse 或 Apache Druid 用于高基数指标。Elasticsearch 或 Grafana Loki 用于日志(取决于您的查询模式)。Jaeger 或 Grafana Tempo 用于追踪。这里的关键投资在于关联层 — 确保您能够从指标异常无缝跳转到相关追踪再到相关日志。
心理博弈
抛开技术技能,调试生产问题是一项心理挑战。凌晨 3 点,在压力之下,利益相关者每五分钟就要一次更新,您的认知能力会下降。以下是一些有帮助的做法:
事件期间结对工作。 两位工程师一起处理事件 — 一位主导(输入命令、检查仪表盘)和一位导航(保持大局观、做笔记、质疑假设) — 总是比单独一位工程师表现更好。导航者能捕捉到隧道视野,当你盯着日志输出看了 20 分钟后,这种视野不可避免地会出现。
在测试假设前先写下来。这听起来可能有些教条,但能避免你陷入循环。打开一个笔记,写下”假设:数据库变慢是因为 orders.customer_id 缺少索引”,然后测试它并记录结果。当你调试一小时后,有人问”你检查过 X 吗?”,你可以查看笔记而不是试图回忆。
为当前方法设置计时器。如果你已经追寻一个理论15分钟却没有进展,停下来重新评估。你查看的数据是否正确?你的假设是否仍然与你看到的现象一致?最常见的调试反模式是沉没成本——因为你已经投入了时间,所以继续沿着死胡同走下去。
知道何时升级问题。如果你处理一个事件已经30分钟却仍未找到根本原因,就引入更多人。这不是软弱的表现,而是良好的事件管理。新的视角和不同的专业知识能加速解决过程。
总结
生产环境调试不是要成为房间里最聪明的人或对你的系统有百科全书式的了解。而是要有一个能在压力下重复执行的流程:
- 快速分类:确定影响范围,检查明显问题,尽早沟通。
- 系统化调查:使用指标找出问题位置,使用追踪找出原因,使用日志获取细节。
- 不断问”为什么”,直到找到系统性故障而非人为错误。
- 编写具体、有负责人、可跟踪的行动项目。
- 在需要之前就投资可观测性工具。
擅长事件响应的工程师不是那些从未遇到过事件的工程师,而是那些能够快速解决问题、彻底从中学习并构建随时间推移故障更少的系统的工程师。目标不是完美,而是可靠性的持续、可衡量的改进。
下次你的手机在凌晨3点响起时,你会有处理它的系统。至于你是否会对此感到高兴,那就是另一个问题了。
