排错时先找证据,再改代码
围绕搜索页持续加载的失败路径,结合复现、网络证据、断点与合成追踪时间线,验证修复是否覆盖边界。
写出可重复的症状和可反驳假设,用原始及相邻失败输入核验一次最小修复。
资料搜索页一直显示加载中,控制台没有异常,网络面板却显示请求已经返回。此时继续改超时参数未必有用:问题可能在响应字段、状态转换,也可能是旧请求覆盖新请求。排错需要先把可观察的失败放在明确边界里,再用证据筛选假设。
本课使用合成请求与时间线,不接真实监控系统。目标是形成一份能重复失败、解释定位依据、验证修复范围的记录,而不是从一条日志猜出所有原因。
先给症状写出复现条件
记录操作顺序、输入、预期、实际结果和环境。对于搜索页,可以写成:输入不存在的标题,点击搜索;预期出现无结果提示,实际持续加载。随后记录是否每次发生、刷新是否改变结果,以及最后一个已知正常版本。输入数据与版本要能让另一位读者复现。
把失败分层:进程无法启动属于运行环境;HTTP 非成功状态属于请求结果;成功响应缺少约定字段属于数据契约;正确数据却显示错误状态属于界面逻辑。它们可能串联发生,但需要逐层检查,不能以“接口成功”证明整个功能成功。
先减小复现范围。保留一个查询、一条必要数据和一条操作路径,删除与故障无关的合成内容。不要在尚未确认输入一致时同时升级依赖、改缓存和重写组件;这样即使症状消失,也不知道哪个变化起作用。
网络记录和日志分别证明什么
浏览器 Network 面板可以观察请求地址、方法、状态、响应内容与阶段耗时。确认页面确实发了请求,再检查它返回什么。HTTP 200 与应用层 error 字段可以同时出现,JSON 解析成功也不代表字段类型正确。根据接口契约判断,不只看绿色状态。
Console 中的堆栈能定位异常发生的位置,但异常出现的那一行可能只是错误数据第一次被使用。日志中的时间相近也不等于因果相关。为请求分配关联标识,在服务入口、关键依赖调用和结果映射位置记录必要信息,才能串起同一次操作。
请求头、Cookie、响应体可能包含凭证或个人信息。导出 HAR、复制日志或截图之前,先裁剪和脱敏。调试所需的通常是方法、路径、状态、字段类型、错误码与合成标识,而不是整份真实用户内容。保留证据的同时要控制其传播范围。
断点检查变量,假设解释变化
断点暂停执行,让你检查当前作用域、调用栈和局部变量。VS Code 或 Python pdb 的单步进入适合观察被调用函数,单步越过适合观察调用前后的状态。选用哪种操作取决于疑问,而不是每行都停一次。
对于持续加载,可以在响应到达后检查 data 的实际类型,再在状态更新处检查分支是否执行。若代码只有非空结果才设置 success,那么空数组路径就可能遗漏结束状态。把这写成可反驳假设:相同空数组应重复失败,非空数组应走另一分支。
断点会改变时序。竞争问题在暂停后消失,并不能证明修复了竞争;日志过多也可能改变运行节奏。需要用不暂停的重复执行和可控响应顺序测试补充验证。不要把“单步执行正常”作为并发安全结论。
用时间线确认测量边界
OpenTelemetry 用 traces 和 spans 表达关联操作;一个 span 通常表示某项工作的时间区间,可包含属性与父子关系。跨服务需要传播上下文,采样会影响哪些操作被保存。找不到 trace 可能是未采集或未传播,不直接说明请求没发生。
下面的合成时间线以同一时间基准、任意单位表达 request、query、render。断言先检查结束不早于开始,再检查子步骤位于请求区间内。它没有调用 OpenTelemetry SDK,也没有验证跨机器时钟同步。
spans = [
{"name": "request", "start": 0, "end": 50},
{"name": "query", "start": 8, "end": 30},
{"name": "render", "start": 32, "end": 45},
]
assert all(s["end"] >= s["start"] for s in spans)
assert spans[1]["end"] - spans[1]["start"] == 22
assert all(0 <= s["start"] <= s["end"] <= 50 for s in spans)
print({s["name"]: s["end"]-s["start"] for s in spans})
输出为 {'request': 50, 'query': 22, 'render': 13}。query 的 22 来自 30 减 8。两个子步骤持续时间相加为 35,不等于整个请求的 50;剩余区间可能包含未建模的工作。若步骤重叠,简单相加还会重复计算时间,不能称为总延迟。
这个模型只验证给定数据的区间关系。实际系统要区分墙上时钟与适合测量耗时的时钟,还要确认埋点边界。缺少排队或序列化阶段时,不能把未解释耗时直接归为数据库问题。
修复只改变已验证的原因
确认遗漏空结果分支后,补全 loading 到 empty 的转换,而不是顺手重写全部状态逻辑。保存修改前后 diff,重新执行原始复现,然后检查非空结果、网络失败、解析失败和连续查询。修复原始输入却引入相邻失败,仍需要继续处理。
对于间歇问题,记录重复次数和可控条件,不用一次成功概括所有情况。日志能证明观察到了什么,测试能证明特定输入满足断言,二者都不能证明所有未来请求安全。将剩余不确定性写成具体待测路径。
避免永久留下高噪声调试输出。保留能解释关键状态的结构化日志,移除临时完整对象打印,并确认错误响应不会泄露内部堆栈。修复后重新检查日志格式,确保将来同类问题仍能定位。
修复前保留一个可恢复版本,失败时可以回到同一输入与代码基线。怀疑数据库结果时,先确认查询参数和返回行数,再检查序列化映射,避免直接用手工改数据消除症状。证据记录应区分已观察事实与仍待验证的解释,方便后续接手者继续实验。
练习:写一份可被反驳的排错记录
用合成搜索结果构造空数组、非空数组、无效字段三种响应。记录预期界面状态,先定位最小失败输入,再列出两个竞争假设,为每个假设设计能支持或排除它的观察。只按证据选择一项改动。
验收要求:另一位读者按记录重现原始错误,看到改动前后的差异,且三种响应都有明确终态。再让两次查询逆序完成,检查旧结果不会覆盖新结果。时间线练习则需检测负持续时间和超出父区间的步骤,并明确模型不能证明真实系统的时钟与追踪正确。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。