08. 调试方法论#
这几天调试 PVE / Syncthing / VM105 时沉淀下来的方法论。
每条都是从一次实际跑偏/正确中找到的。
核心原则:先查本端,再碰远端 ⭐#
反面例子#
排查"VM105 的工作学习 文件夹为什么不同步到 CT111"时,我先尝试:
- 挂载 VM105 数据盘 (失败:RAID member)
- 用
qemu-nbd 暴露 zvol (失败:boot 盘已挂载) zfs send VM105 数据盘到文件 (慢)
5+ 步后才发现:CT111 的 /rest/events 已经把错误说清楚了
(mkdir /data/工作学习/.obsidian: permission denied)。
正解: CT111 端 2 步搞定 (查 events → 修 owner)。
正面例子#
排查"为什么 SSH 慢" 时:
time -p ssh user@host echo → 总耗时 5sssh -vvv → 看到 DNS 反查慢- 修
/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 分钟内没找到思路,停下来:
- 重述问题给"听众”(可以是用户、另一个 agent、屏幕录像)
- 重新看错误信息,逐字读
- 列 3 个最可能的假设
- 选最便宜的验证方式
不要"调试到天亮”。
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排查 就是按这个模板写的)
核心原则回顾#
- 先查本端可观测面,再碰远端磁盘
- 5 分钟没思路,停下来重述问题
- 10 分钟没进展,换路径
- 区分症状 / 问题 / 根因
- 看错误信息的具体内容,别只看表面
- 沉没成本不是继续走的理由
- 写下来,下次不踩
下一篇#
09. 经验教训汇总 →