08. 调试方法论

这几天调试 PVE / Syncthing / VM105 时沉淀下来的方法论。 每条都是从一次实际跑偏/正确中找到的。

核心原则:先查本端,再碰远端 ⭐

反面例子

排查"VM105 的工作学习 文件夹为什么不同步到 CT111"时,我先尝试:

  1. 挂载 VM105 数据盘 (失败:RAID member)
  2. qemu-nbd 暴露 zvol (失败:boot 盘已挂载)
  3. zfs send VM105 数据盘到文件 (慢) 5+ 步后才发现:CT111 的 /rest/events 已经把错误说清楚了 (mkdir /data/工作学习/.obsidian: permission denied)。

正解: CT111 端 2 步搞定 (查 events → 修 owner)。

正面例子

排查"为什么 SSH 慢" 时:

  1. time -p ssh user@host echo → 总耗时 5s
  2. ssh -vvv → 看到 DNS 反查慢
  3. /etc/ssh/sshd_config: UseDNS no → 立刻好

原则: 任何"为什么 X 没工作"的问题,先查 X 这一端的可观测面 (logs / metrics / events / API status),再决定是否要碰 Y 端。

适用场景

  • Syncthing 不 sync → 看 CT111 /rest/events (不是看 VM105 GUI)
  • 数据库读写慢 → 看 DB 本端 slow log (不是看客户端网络)
  • API 返回错 → 看服务端 logs (不是看客户端代码)
  • VM 不通 → 看 VM 本端 console (不是看宿主机)

调试 checklist (跨服务问题)

排查任何 “A 服务没收到 B 服务的消息” 问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
□ 1. 物理/网络连通
     ping / nc -zv / traceroute
□ 3. A 端的"应该接收"队列有没有目标数据
     DB count / queue size / API GET / 本地 events
□ 4. A 端的"接收处理"有没有错误
     logs / metrics errors / pullErrors / deadletter
□ 5. B 端的"发送"有没有成功
     B 的 logs / outBytesTotal
□ 6. 中间件 / 协议 / 配置
     ACL / shared devices / firewall rules / auth

顺序很重要: 从物理层往上查,不要跳。

工具选择

看 Syncthing 状态

1
2
3
4
5
6
7
8
# 必备三件套
curl -H "X-API-Key: $KEY" http://127.0.0.1:8384/rest/db/status?folder=$ID
curl -H "X-API-Key: $KEY" http://127.0.0.1:8384/rest/system/connections
curl -H "X-API-Key: $KEY" http://127.0.0.1:8384/rest/events?limit=100

# 触发 rescan (调试时常用)
curl -X POST -H "X-API-Key: $KEY" \
  http://127.0.0.1:8384/rest/db/scan?folder=$ID

看 PVE 状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# VM 状态
qm list
qm status $VMID

# LXC 状态
pct list
pct status $CTID

# ZFS 状态
zfs list
zpool status

看磁盘健康

1
2
3
smartctl -H -A /dev/sdX
smartctl -l selftest /dev/sdX
smartctl -x /dev/sdX

跨服务调试技巧

1. 看 “对称状态"找差异

如果 A 状态对、B 状态错,差异在哪?

1
2
[VM105] /rest/db/status  → globalBytes=16GB  ✓
[CT111] /rest/db/status  → globalBytes=16GB, localBytes=0  ✗

差异 = CT111 没拉到。看 pullErrors 字段。

2. 看 “最近事件"找时间线

1
curl /rest/events?since=$ONE_HOUR_AGO&limit=1000

按时间排序,事件流告诉你:

  • 几时发生了什么
  • 错误是什么
  • 谁先谁后

3. 触发操作看响应

触发 rescan / 强制 sync:

1
2
curl -X POST /rest/db/scan?folder=$ID
# 立刻看 events,有反馈

“主动触发” 比 “被动观察” 调试快很多。

踩坑模式库

模式 1: 静默错误

症状: Syncthing 不报警、不弹窗,数据悄悄没 sync。

检测: 监控脚本定期查 pullErrors

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash
# /usr/local/bin/syncthing-healthcheck.sh
KEY=$(cat /etc/syncthing.key)
ERRORS=$(curl -sS -H "X-API-Key: $KEY" \
  http://127.0.0.1:8384/rest/db/status?folder=$1 | \
  python3 -c "import json,sys; print(json.load(sys.stdin).get('pullErrors',0))")

if [ "$ERRORS" -gt 0 ]; then
  echo "ALERT: $1 has $ERRORS pull errors"
  # 发邮件 / 发 push
fi

模式 2: UID 不匹配

症状: daemon 进程跑起来了,但写不进数据目录。

检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 进程跑在哪个 UID
ps -ef | grep $DAEMON | grep -v grep
# syncthi+  1517  1  0 00:56 ?  00:00:00 /usr/bin/syncthing serve ...

# 2. 数据目录 owner
ls -lad /data/$DIR
# drwxr-xr-x 3 root root /data/$DIR  ← 错配!

# 3. 测试可写性
sudo -u $DAEMON_USER touch /data/$DIR/test
# permission denied ✗

修复:

1
chown -R $DAEMON_USER:$DAEMON_GROUP /data/$DIR

模式 3: 网络可达但协议不通

症状: ping 通端口不通 / 协议握手失败

检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 物理通
ping -c 1 $HOST  # OK

# 2. 端口通
nc -zv $HOST $PORT  # OK

# 3. 协议通
curl -I http://$HOST:$PORT  # 失败

# 4. 防火墙
iptables -L -n -v  # 看是否有 drop

模式 4: 路径写错但程序没报错

症状: 程序以为成功了,实际写到别处了。

检测:

1
2
3
4
5
6
# 1. 程序用的路径
strings /proc/$(pidof $DAEMON)/cmdline | grep -i path
lsof -p $(pidof $DAEMON) | grep $DIR

# 2. 实际文件在哪
find / -name $EXPECTED_FILE 2>/dev/null

zfs / qemu-nbd / mount 调试技巧

看 zd* 和 zvol 映射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ls -la /dev/zd*
# /dev/zd0, zd16, zd32, zd48, zd64, zd80 ...
# device numbers increment by 16

ls -la /dev/zvol/SSD_1TB/ 2>&1 | grep vm-105
# /dev/zvol/SSD_1TB/vm-105-disk-0 -> ../../zd80
# /dev/zvol/SSD_1TB/vm-105-disk-1 -> ../../zd32

blockdev --getsize64 /dev/zdN
# 实际大小

qemu-nbd 暴露 zvol

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 加载 nbd 模块
modprobe nbd

# 2. 暴露
qemu-nbd -r -c /dev/nbd0 /dev/zd32

# 3. 看分区
fdisk -l /dev/nbd0

# 4. mount
mount -o ro /dev/nbd0p1 /mnt/vm105

# 5. 清理
umount /mnt/vm105
qemu-nbd -d /dev/nbd0

注意:

  • -r 只读 (不是 --readonly)
  • 如果设备是 linux_raid_member,不能直接 mount
  • 跑着的 VM 数据盘 mount 可能损坏 FS,小心

看 mount 信息

1
2
3
4
5
6
7
8
mount | grep $DIR
# 确认实际 mount 到哪

findmnt $DIR
# 更结构化的输出

stat $DIR | grep Device
# 看 inode 落在哪个 device

调试时间管理

5 分钟规则

任何调试,5 分钟内没找到思路,停下来:

  1. 重述问题给"听众”(可以是用户、另一个 agent、屏幕录像)
  2. 重新看错误信息,逐字读
  3. 列 3 个最可能的假设
  4. 选最便宜的验证方式

不要"调试到天亮”。

10 分钟没进展:换路径

走错路时,10 分钟没进展就停,重新评估:

  • 我在查的是症状还是根因?
  • 我有的信息够不够?
  • 有没有更便宜的查法?

30 分钟还卡住:问人

求助不是失败。卡住 30 分钟,问题大概率不是你一个人能解决的。

调试心理学

不要陷入 “沉没成本”

走错 5 步后,承认"走错了"比"再试一次"更重要

我这次调试 VM105 时:

  • 第 1 步失败
  • 第 2 步失败
  • 第 3 步失败
  • 第 4 步失败
  • 第 5 步勉强成功但还是没答案

正确反应: 第 2 步失败时就该回到 CT111 events 查根因。

区分 “症状” 和 “问题”

  • 症状: 工作学习 文件夹不 sync (0/16GB)
  • 真问题: mkdir permission denied (16,379 个)
  • 根因: 目录 owner 是 root,daemon 是 syncthing 用户

一直在查"为什么不 sync"(症状),没问"为什么不 sync" → 才发现真问题。

看错误信息的"具体内容"

错误消息通常已经告诉了你答案:

1
2
3
mkdir /data/工作学习/.obsidian: permission denied
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ← 路径
                              ^^^^^^^^^^^^^^ ← 原因
1
2
3
4
5
"OK" 我知道是 permission,但问 permission 是哪个层级:
- filesystem (chmod)?
- directory owner (chown)? ← 答案是这个
- SELinux (setenforce 0)?
- quota?

错误信息告诉结果,不一定告诉根因

调试日志模板

每次"踩坑后解决问题",写下来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## 问题
[一句话]

## 排查过程
1. 第一步做了什么,看到了什么
2. 第二步做了什么,看到了什么
...

## 根因
[具体什么原因]

## 修复
[具体怎么修]

## 教训
[一句话原则]

## 相关
[链接其他文档 / memory]

(本次 06-16379个permission-denied排查 就是按这个模板写的)

核心原则回顾

  1. 先查本端可观测面,再碰远端磁盘
  2. 5 分钟没思路,停下来重述问题
  3. 10 分钟没进展,换路径
  4. 区分症状 / 问题 / 根因
  5. 看错误信息的具体内容,别只看表面
  6. 沉没成本不是继续走的理由
  7. 写下来,下次不踩

下一篇

09. 经验教训汇总 →