调试与日志
程序不会做你期望的事,只会做你写下的事。调试的核心,是把「我以为它做了什么」变成「它实际做了什么」。
先缩小范围
定位 bug 的第一步不是读完全部代码,而是找一个能稳定复现的最小输入。
- 记录完整的运行命令、输入、环境变量和版本
- 二分注释代码,确认问题落在哪一半
- 二分输入数据,缩小到触发条件
- 在函数边界打印参数,验证假设
- 把复现步骤写进 issue,避免下次重新猜
每次只验证一个假设:改一处、跑一次、记录结果。同时改三个地方,出错时无法判断是哪一个起了作用。
把「它应该输出什么」写下来,再和实际输出逐字符比较。很多 bug 来自对输入的假设,而不是算法本身。能反复触发的十行程序,抵得上一小时的脑内推演。
print 与日志
print 是最直接的手段:在可疑位置输出中间状态,跑一遍,看哪一步开始偏离预期。缺点是输出随手写、事后难清理、生产环境不能一直开着。
日志框架把「打印」变得可控:可以按级别过滤、切换输出目标、附加结构化字段。
| 能力 | print | logging |
|---|---|---|
| 分级过滤 | 无 | DEBUG、INFO、WARN、ERROR |
| 输出目标 | stdout | 文件、stderr、syslog |
| 结构化字段 | 手工拼 | 键值或 JSON |
| 运行时开关 | 改代码 | 改配置或环境变量 |
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
log = logging.getLogger("fetch")
log.debug("cache key=%s", key)
log.warning("retry %d for %s", n, url)- 先打在可疑区间的中点,确认问题在前半还是后半
- 再继续对半折,几轮就能压到几十行
- 比从头读到尾快得多
写成 log.debug("x=%s", x) 而不是先拼字符串,级别没开时可以省掉格式化开销。修完 bug 后,把仍然有诊断价值的 print 改成 log.debug 保留。下次同类问题出现时,需要的信息已经在那里。
从外部读日志
自己的程序有日志,别人的程序也有。先读日志,再猜原因。
./server -v # 提高详细度
./server -vv # 有些程序支持叠加
journalctl -u nginx --since "10 min ago"
journalctl -u nginx -f
tail -f /var/log/syslog| 场景 | 入口 |
|---|---|
| systemd 服务 | journalctl -u <unit> |
| 传统服务 | /var/log/ 下的文件 |
| 命令行工具 | -v / --verbose |
| 第三方库 | 环境变量或配置项 |
| 容器 | docker logs <container> |
很多库把调试开关放在环境变量里,例如 RUST_LOG=debug、DEBUG=*。不确定时查文档,不要靠猜。
- 读日志先对齐时间:本机时区、UTC、容器时区经常不一致
- 差八小时会让你把两次无关的事件串在一起
- 服务崩溃重启时,先看最后一次启动之前的那几行
- 真正的错误通常在重启标记上方,而不是下方的新进程日志
结构化日志
给人看的日志是句子,给机器查的日志是字段。线上环境两者都要。
- 每条日志带时间戳和唯一的
request_id - 可变部分做成键值:
user_id=42 status=500 - 需要聚合时输出 JSON Lines,一行一个事件
- 不要在日志里写密码、token 和完整证件号
- 高频路径做采样,避免日志本身成为瓶颈
{
"ts": "2026-11-06T10:00:00Z",
"level": "error",
"request_id": "a1b2",
"path": "/api",
"status": 500,
"ms": 1203
}字段名一次定好,后面用 jq、Loki 或 Elasticsearch 查询时就不用改管道。字段的取值基数也要控制:把用户 ID、URL 原样打进去,索引很快会被撑爆。
排查跨服务问题时,request_id 比时间戳更好用。让入口生成、沿途透传、日志统一带上,一次请求的完整路径就能串起来。
AI 辅助调试
LLM 擅长把晦涩的报错翻译成人话,尤其是 C++ 模板和 Rust 借用检查器的输出。
- 贴完整报错、编译命令和版本,不要只贴一行
- 先让它解释「为什么」,再让它给出验证步骤
- 跨语言边界的问题(Python 调 C 库)适合让它梳理层次
- 崩溃栈可以贴给它做初步归因
- 它给的修复必须用调试器或复现用例验证
给它最小复现和实际输出,比给它整份源码更有效。模型看不到你的运行环境,环境差异要自己补上。
LLM 会编出听起来合理的解释。把回答当成假设,不要当成结论。
常见坑
- 只在
except里打印"error",丢掉异常对象和栈 - 所有日志都用
INFO,等于没有级别 - 用
print往 stdout 写诊断信息,污染了管道数据 - 复现时忘了关掉之前的临时改动
- 日志里带上了用户密码,排查完才发现
- 日志按天切割但没设保留期,磁盘被写满