提供系统性的问题排查方法,避免"随机试错"式 debug,快速定位根因并修复,同时将经验沉淀为知识。
- 程序报错/崩溃
- 功能行为不符合预期
- 性能突然下降
- 环境相关问题(本地正常/生产异常)
- 间歇性/难以复现的问题
先判断问题类型,选择对应排查路径:
| 类型 | 特征 | 典型原因 |
|---|---|---|
| 崩溃/异常 | 程序直接报错退出 | 空指针、类型错误、未处理异常 |
| 逻辑错误 | 运行正常但结果不对 | 条件判断错、边界遗漏、算法错误 |
| 性能问题 | 慢、卡、超时 | N+1 查询、内存泄漏、死循环 |
| 环境问题 | 本地好/线上坏 | 配置差异、版本不同、权限问题 |
| 并发问题 | 时好时坏 | 竞态条件、死锁、资源争用 |
| 集成问题 | 调用外部服务出错 | 网络、认证、格式不匹配 |
复现是 debug 的前提,不能复现就不能确认修复。
- 记录错误信息(完整堆栈/日志/截图)
- 确定复现步骤:
- 什么操作触发的?
- 需要什么前置数据/状态?
- 是否 100% 复现?还是概率性?
- 尝试在本地复现
- 如果无法复现:
- 增加日志/监控
- 检查环境差异
- 查看是否是数据相关问题
复现记录格式:
环境:[OS/浏览器/Node版本/...]
步骤:1. ... 2. ... 3. ...
期望:[应该发生什么]
实际:[实际发生了什么]
频率:[必现 / 偶现(约X次中Y次)]
缩小范围,确定问题在哪一层:
前端 UI → API 请求 → 后端逻辑 → 数据库 → 外部服务
隔离方法:
- 二分法:在中间点检查,确定问题在前半段还是后半段
- 最小复现:剥离无关代码,构建最小复现用例
- 替换法:用已知正常的组件替换怀疑组件
- 对比法:对比正常情况和异常情况的差异
检查点:
- 请求是否正确发出?(Network tab)
- 后端是否收到请求?(日志)
- 逻辑处理是否正确?(断点/日志)
- 数据库查询是否正确?(SQL 日志)
- 响应是否正确返回?(Response)
常用定位手段:
| 手段 | 适用场景 |
|---|---|
| 断点调试 | 逻辑错误,需要逐步跟踪 |
| 日志输出 | 生产环境、异步流程 |
| 堆栈追踪 | 异常/崩溃 |
| 网络面板 | 前后端交互问题 |
| 数据库日志 | 查询性能/结果问题 |
| Git bisect | 回归问题(哪个提交引入的) |
| 性能分析器 | 性能瓶颈 |
Git bisect(定位引入问题的提交):
git bisect start
git bisect bad # 当前版本有 bug
git bisect good v1.0.0 # 这个版本是好的
# Git 会自动 checkout 中间提交,你测试后标记 good/bad
git bisect reset # 结束提问框架(5 Whys):
为什么报错?→ 因为 user 是 null
为什么 user 是 null?→ 因为查询没找到记录
为什么没找到?→ 因为 user_id 传了错误的值
为什么值错误?→ 因为前端传了 string 而非 number
为什么类型不对?→ 因为 API 没有做类型验证
→ 根因:API 缺少输入验证
修复原则:
- 修复根因,不是症状(不要只处理报错,要解决根本问题)
- 最小改动原则(不要顺手重构)
- 先写失败测试,再修复(TDD 思路)
- 考虑修复是否引入新问题
修复检查:
- 修复是否针对根因?
- 是否有更多类似问题需要一并修复?
- 是否需要添加防御性代码?
- 是否需要补充测试?
- 原始复现步骤不再触发问题
- 相关功能回归测试通过
- 边界条件测试通过
- 如果是性能问题,确认指标恢复
- 全量测试套件通过
问题记录格式:
## [问题标题]
- 日期:
- 影响范围:
- 根因:
- 修复方案:
- 耗时:
- 教训:[下次如何避免/更快定位]
- 相关提交:[commit hash]- 问题已分类(崩溃/逻辑/性能/环境/并发)
- 已记录完整错误信息和复现步骤
- 问题已在本地复现(或有明确复现路径)
- 已隔离到具体层/模块
- 根因已确定(不是只修症状)
- 修复前有对应的失败测试
- 修复后原始问题不再复现
- 回归测试通过
- 问题已记录(根因/修复/教训)
- 必要时补充了防御性代码/测试
| 输出物 | 格式 | 存放位置 |
|---|---|---|
| Bug 修复代码 | 提交 | fix/ 分支 |
| 回归测试 | 测试代码 | tests/ |
| 问题记录 | Markdown | docs/issues/ 或知识库 |
| 误区 | 正确做法 |
|---|---|
| 不复现就开始改 | 先复现,确认问题存在 |
| 随机改代码碰运气 | 系统性隔离和定位 |
| 只修症状不修根因 | 用 5 Whys 追到根本原因 |
| 修完不测试 | 必须有验证步骤 |
| 修完就忘了 | 记录下来,避免重复踩坑 |
| 一次改太多 | 最小改动,一次修一个问题 |
| 忽略"偶现"问题 | 偶现往往是并发/竞态,更严重 |
| 根据上游 issue 的相似结论直接下结论 | 那只是候选假设;按其修改后仍失败就应立即放弃并回到堆栈证据,同时清除被证伪的修改与其误导性注释 |
| 性能问题只测自己的服务就断定服务端慢 | 必须做对照实验(同一客户端测一个已知高速的第三方源)以排除客户端/链路因素;对照时注意重定向(curl 需 -L)否则只测到几十字节的 301/302 响应,得出虚假数字 |
| 用探测请求查看缓存命中状态并据此下结论 | 探测本身可能改变被观测对象(如小 Range 请求会使该分片被缓存,之后总显示 HIT);应以终端指标(如大范围实际下载速率)作为判据 |
| 报错信息在代码库零匹配仍继续深挖代码逻辑 | 先核对运行的构建与源码是否同版本(版本号、构建产物、UI 形态);源码中不存在的错误串只可能来自旧构建,可沉淀为知识卡(见 knowledge-card-template) |