第三章 问题排查的六阶段模型
阶段0:镇定与复现 —— 稳定重现是前提
镇定:不要着急修改代码。仓促的修改往往引入新问题。
收集信息:错误日志、截图、用户操作步骤、环境信息(OS、版本、依赖)。
尝试复现:按照用户步骤操作。如果无法复现,询问更多细节(使用什么浏览器?网络环境?数据内容?)。如果可以,尽可能简化步骤得到最小复现用例。
复现技巧:
使用二分法减少输入:如果一个长 JSON 触发 bug,每次删除一半字段,直到找到必要的最小 JSON。
使用自动化脚本重复执行操作,增加复现概率(尤其对并发问题)。
# 自动压力测试脚本示例
import threading
import requests
def worker():
for _ in range(100):
requests.post('http://localhost:8080/api', json={'key': 'value'})
threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
阶段1:定位 —— 缩小错误发生的范围
方法:
打印探针:在怀疑的关键路径插入 print("1")、print("2"),观察输出序列,找到第一个未打印的位置。
异常捕获:在顶层 try-except 中打印完整堆栈,确定错误行。
注释法:暂时注释掉一半代码,看错误是否消失,反复二分。
案例: 一个 2000 行的模块崩溃。注释掉后半部分,错误消失 → 错误在后半部分。再在后半部分中注释一半,以此类推,5次二分就能定位到具体函数。
阶段2:隔离 —— 排除外部干扰
将问题从复杂的系统环境中剥离出来:
关闭无关插件/中间件
使用测试替身(mock、stub)代替真实数据库/API
在其他机器/环境运行相同代码
mock 示例(Python unittest.mock):
from unittest.mock import Mock
# 代替真实的外部 API 调用
mock_api = Mock()
mock_api.get.return_value = {'status': 'ok'}
def process_data(api_client):
data = api_client.get('/data')
return data['status']
assert process_data(mock_api) == 'ok'
阶段3:提出假设 —— 基于证据的猜测
假设必须具体、可验证。不好假设:“可能是内存问题”。好假设:“第 88 行分配的 10MB 缓冲区没有释放,累积 100 次后导致内存溢出”。
假设来源:
经验知识(常见错误模式)
阅读文档(某些函数有副作用)
代码审查(明显逻辑缺陷)
阶段4:实验验证 —— 用最小的成本检验假设
实验设计原则:
一次只改变一个变量。
尽量不修改原始代码(使用断点、日志、外部监控)。
记录实验结果(通过/失败/部分通过)。
验证手段:
添加断言并运行单元测试。
临时修改代码并重新部署(开发环境)。
使用调试器观察变量值。
阶段5:修复与回归测试
理解根因后,修复代码(不要只治标)。
添加针对该 bug 的单元测试,确保不会再次出现。
运行现有所有测试,确保没有引入回归。
阶段6:复盘与预防
记录 bug 报告(现象、根因、修复方案、预防措施)。
思考:如何设计可以避免这类错误?(例如添加类型注解、改进 API 设计、增加静态检查)
分享给团队,避免他人踩坑。
第四章 错误分类与针对性排查手册
4.1 语法错误 —— 编译器/解释器直接指路
常见类型:
缺少括号、引号、运算符
缩进错误(Python)
未声明变量(Java、C)
使用了保留字作为标识符
排查步骤:
阅读错误信息的文件名和行号。
检查该行和上一行的语法。
如果错误在行尾,可能上一行缺少闭合符号。
使用 IDE 的实时语法高亮和 lint 工具(如 ESLint、Pylint)提前发现。
进阶: 当错误信息指向一个不存在的行(例如宏展开),需要预处理器输出或查看生成代码。
4.2 编译/链接错误 —— 类型不匹配、符号未定义
Java 编译错误示例:
String s = 123; // 类型不匹配
obj.undefinedMethod(); // 符号未定义
排查方法:
检查 import 语句。
检查类路径(classpath)是否包含所有依赖。
检查泛型类型擦除导致的怪现象。
链接错误(C/C++): 函数声明了但没实现,或者库未链接。
4.3 运行时错误 —— 细分 10 种及对策
4.4 逻辑错误 —— 最难缠的敌人
逻辑错误的特征:程序能跑完,但输出错误。没有异常信息,只能靠检查中间状态。
分类:
数值错误:数学公式写错、取整错误、浮点精度。
条件错误:比较运算符反了(> 写成 <)、逻辑组合错误(and/or)。
流程错误:循环多一次或少一次、分支覆盖不全。
状态错误:忘记重置全局状态、共享变量未同步。
数据处理错误:字符串编码、JSON 解析错误但被忽略。
系统化排查方法:
二分检查点法:在代码逻辑的关键中间点输出结果,对比期望值。
单元测试 + 数据驱动:编写测试用例,提供多组输入/预期输出,观察哪个用例失败。
可视化执行:对于算法错误,使用 Excel 或手动画出每一步的状态变化。
变量跟踪表:手动模拟代码执行,记录变量变化。
示例:二分查找实现错误
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left < right: # 错误:应该是 <=
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# 测试:binary_search([1,2,3], 3) 返回 -1
通过单步执行,发现当 left=1, right=2, mid=1, arr[1]=2<3, left=2,此时 while 条件 2 < 2 为假,退出循环,未检查索引2。修复为 while left <= right。
4.5 并发错误 —— 多线程/多进程的噩梦
并发错误难以复现,因为依赖于线程调度的时序。
经典并发问题:
竞态条件:对共享变量的非原子操作。
死锁:线程互相持有对方需要的锁。
活锁:线程不断响应对方而改变状态,导致无法继续。
饥饿:低优先级线程永远得不到执行。
内存可见性:一个线程对变量的修改,另一个线程看不到(缺少 volatile 或同步)。
排查工具:
Java: jstack, JConsole, VisualVM, ThreadMXBean
Python: threading 模块的 enumerate(),sys._current_frames()
C++: ThreadSanitizer,helgrind (Valgrind)
实战案例:两个线程同时给同一个账户存款
public class BankAccount {
private int balance = 0;
public void deposit(int amount) {
int newBalance = balance + amount; // 读-改-写
balance = newBalance;
}
}
如果线程A读balance=0,然后线程B也读balance=0,然后A写1,B也写1,最终balance=1,损失了1元。修复:使用 synchronized 或 AtomicInteger。
检测竞态条件的技巧: 在关键代码前后添加计数器,检查不变量是否被破坏。例如,预期 balance 总是等于所有存款之和,用另外一个线程持续验证。
4.6 性能问题 —— 慢比崩溃更折磨人
常见性能瓶颈:
不合理的算法复杂度(O(n²) 处理大数据)
频繁的 I/O(磁盘、网络)
锁竞争导致线程阻塞
内存分配/GC 压力过大
资源池配置过小(连接池、线程池)
排查流程:
设定基线:在理想情况下测量正常响应时间。
压力测试:使用 JMeter、wrk、ab 逐步增加负载,找到拐点。
Profiling:找出最耗时的函数(CPU profiler)或分配最多内存的地方(memory profiler)。
火焰图:可视化调用栈及耗时占比。
Python 中使用 cProfile 和火焰图:
python -m cProfile -o output.prof my_script.py
snakeviz output.prof # 可视化
优化策略:
缓存(Redis、本地缓存)
异步处理(消息队列)
数据库索引、查询优化
批处理代替单条操作
减少锁粒度,使用读写锁
4.7 内存错误 —— 泄漏、越界、野指针
C/C++ 典型问题:
忘记释放内存(泄漏)
释放后继续使用(use-after-free)
越界写入(缓冲区溢出)
双重释放(double free)
排查工具:
Valgrind(重量级,适合开发环境)
AddressSanitizer(轻量级,编译时插入)
Heaptrack(堆内存跟踪)
Python 内存泄漏: 通常是由于全局容器(如列表、字典)不断增长,或循环引用导致垃圾回收不及时。使用 tracemalloc 定位:
import tracemalloc
tracemalloc.start()
# ... 运行代码 ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
4.8 环境与配置错误 —— “在我的机器上能跑”
这类错误包含:
依赖版本不匹配
环境变量缺失
文件权限不足
端口冲突
时区/地区设置不同
不同 OS 的行尾符差异
排查策略:
使用容器化(Docker)统一环境。
比较开发环境和生产环境的配置。
在代码中添加启动时的环境检查(如 assert sys.version_info >= (3,8))。
使用 strace (Linux) 或 procmon (Windows) 追踪系统调用,查看具体失败原因。
来源:
http://dffne.cn/