跳到正文

调试与日志 ​

程序不会做你期望的事,只会做你写下的事。调试的核心,是把「我以为它做了什么」变成「它实际做了什么」。

先缩小范围 ​

定位 bug 的第一步不是读完全部代码,而是找一个能稳定复现的最小输入。

  • 记录完整的运行命令、输入、环境变量和版本
  • 二分注释代码,确认问题落在哪一半
  • 二分输入数据,缩小到触发条件
  • 在函数边界打印参数,验证假设
  • 把复现步骤写进 issue,避免下次重新猜

每次只验证一个假设:改一处、跑一次、记录结果。同时改三个地方,出错时无法判断是哪一个起了作用。

把「它应该输出什么」写下来,再和实际输出逐字符比较。很多 bug 来自对输入的假设,而不是算法本身。能反复触发的十行程序,抵得上一小时的脑内推演。

print 是最直接的手段:在可疑位置输出中间状态,跑一遍,看哪一步开始偏离预期。缺点是输出随手写、事后难清理、生产环境不能一直开着。

日志框架把「打印」变得可控:可以按级别过滤、切换输出目标、附加结构化字段。

能力printlogging
分级过滤无DEBUG、INFO、WARN、ERROR
输出目标stdout文件、stderr、syslog
结构化字段手工拼键值或 JSON
运行时开关改代码改配置或环境变量
python
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 保留。下次同类问题出现时,需要的信息已经在那里。

从外部读日志 ​

自己的程序有日志,别人的程序也有。先读日志,再猜原因。

bash
./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 和完整证件号
  • 高频路径做采样,避免日志本身成为瓶颈
json
{
  "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 写诊断信息,污染了管道数据
  • 复现时忘了关掉之前的临时改动
  • 日志里带上了用户密码,排查完才发现
  • 日志按天切割但没设保留期,磁盘被写满