服务器和windows客户端重启后,发现agent连不上lightmem和hive mcp了

后来按了重联后,hive连上了,但是lightmem还是不行,日志信息: fetch failed (connect ECONNREFUSED 192.168.0.88:18444)

手工开lightmem任务:

skywalk@fb98:~/dswork $  sudo service lightmem start
lightmem: PostgreSQL 10.0.10.10:5432 在 60s 内不可达
 

看看谁没有起来? 

skywalk@fb98:~/dswork $ jls
   JID  IP Address      Hostname                      Path
     1                  lims-pg                       /usr/jails/lims-pg
     2                  lims-redis                    /usr/jails/lims-redis
     3                  lims-rabbit                   /usr/jails/lims-rabbit
     4                  lims-minio                    /usr/jails/lims-minio
     5                  lims-router                   /usr/jails/lims-router

手工开这个jail ,竟然说已经exists....

skywalk@fb98:~/dswork $ sudo service jail start lims-pg
Starting jails: cannot start jail  "lims-pg": 
jail: "lims-pg" already exists
.

怒了,还是让AI来做吧,Workbuddy,开工! 

FreeBSD vnet jail 二层「静默断链」:bridge0 成员都在,包就是不转发

一台 FreeBSD 15 上的服务起不来,报了「PostgreSQL 60 秒内不可达」。
顺着这个报错查下去,PG 是活的、jail 是跑着的、bridge0 的成员接口一个不少——
但宿主和 jail 之间就是 ping 不通。
这篇文章记录这次排错:三个看似合理的假设全错,真正的判据藏在接口计数器里。
后面还有第二个坑——它在开机时根本没有尝试启动过。


一、现场:一句「60s 内不可达」

lightmem(一个带 pgvector 后端的记忆服务)的 rc 脚本长这样:

# 启动前等待 lims-pg jail 的 5432 就绪
_i=0
while [ "${_i}" -lt "${lightmem_pg_wait}" ]; do
    if nc -z -w 2 "${lightmem_pg_host}" "${lightmem_pg_port}" 2>/dev/null; then
        return 0
    fi
    _i=$((_i + 1))
    sleep 1
done
echo "lightmem: PostgreSQL ${lightmem_pg_host}:${lightmem_pg_port} 在 ${lightmem_pg_wait}s 内不可达" >&2
return 1

服务没起来,宿主 nc -z 10.0.10.10 5432 失败。拓扑是标准的 FreeBSD vnet jail:

宿主 em0 192.168.0.88
  └─ bridge0 10.0.10.1/24
       ├─ epair10a ── epair10b  lims-pg      10.0.10.10  (PostgreSQL)
       ├─ epair20a ── epair20b  lims-redis   10.0.10.20
       ├─ epair30a ── epair30b  lims-rabbit  10.0.10.30
       ├─ epair40a ── epair40b  lims-minio   10.0.10.40
       └─ epair50a ── epair50b  lims-router  10.0.10.50

二、被数据推翻的三个假设

假设 1:PG 连接打满(因为「有 271 个进程」)

一个只跑 PG 的 jail 有 271 个进程,确实可疑——正常 PostgreSQL 也就十来个。但进去一数:

$ sudo jexec 1 ps -ax -o comm= | sort | uniq -c | sort -rn | head -6
   9 postgres
   6 syslogd
   2 cron

9 个。那个 271 是历史观测,早就不是现场了。用过期数据做推理,是排错里最贵的错误。

假设 2:空壳 jail

这个假设对了一半,但对错的不是那个 jail。查了一圈 /usr/local/etc/rc.d/ 才发现:lims-redis、lims-rabbit、lims-minio、lims-router 四个 jail 里一个服务都没装,只有 syslogd 在跑——它们本来就是占位壳。真正有业务的 lims-pg 是好的。

假设 3:PG 没启动

$ sudo jexec 1 pg_isready -h 10.0.10.10 -p 5432 -t 3
10.0.10.10:5432 - accepting connections

活着,而且 sockstat 显示它老老实实监听在 10.0.10.10:5432。所以问题不在 PG,在「宿主能不能走到 jail」这一层。

顺带两个假警报,别被它们带偏

警报 A:pg_isready -h 127.0.0.1 在 jail 里返回 no response。

第一反应是「PG 没监听回环」。但 sockstat 里那个 127.0.0.1:5432 其实属于宿主自己另装的一个 postgres(jid 0)。jail 有独立 vnet,它的 127.0.0.1 是另一套回环——jail 的 PG 只绑了 10.0.10.10,在 jail 内查 127.0.0.1 本来就不该通。

警报 B:jail 里 ps -o lstart 显示进程启动时间比宿主记录早 8 小时,看着像「进程跨重启存活」这种灵异事件。实际是 jail 里 /etc/localtime 是 UTC、宿主是 CST。时区差,不是残留。

三、真判据:别信「状态」,去数「包」

怀疑链路的第一个动作通常是 ifconfig。但这次 ifconfig 是最会骗人的那个:

$ ifconfig epair10a
epair10a: flags=1008942<BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP>
	status: active          # ← 看着完全正常

$ ifconfig bridge0 | grep member
	member: epair10a flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
	member: epair20a ...    # ← 五个成员一个不少

UP、RUNNING、member 齐全。但真正说明问题的是这两条:

$ netstat -I epair10a -b
Name       Mtu Network   Address              Ipkts Ierrs Idrop  Ibytes Opkts Oerrs  Obytes Coll
epair10a* 1500 <Link#4>  58:9c:fc:10:55:c6        0     0     0       0     0   383       0    0
#                                                    ↑收 0        ↑发 0  ↑错误 383 且在涨

$ ifconfig bridge0 addr
# 空。MAC 转发表里一条记录都没有。

$ ping -c 3 10.0.10.10
100.0% packet loss
$ arp -an | grep 10.0.10.10
? (10.0.10.10) at (incomplete) on bridge0 expired [bridge]

Ipkts=0 / Opkts=0 / Oerrs 持续递增,这个组合基本只有一种解释:桥把帧交给 epair10a 输出,epair_output() 每次都返回错误——对端的 softc 已经不在了。

再用 tcpdump 确认一次:

$ sudo tcpdump -i bridge0 -n -c 20 arp    # 另一窗口同时 ping
08:08:12.673564 ARP, Request who-has 10.0.10.10 tell 10.0.10.1, length 28
08:08:13.673922 ARP, Request who-has 10.0.10.10 tell 10.0.10.1, length 28
08:08:14.674933 ARP, Request who-has 10.0.10.10 tell 10.0.10.1, length 28
# 只有 request 出去,没有一个 reply

jail 里抓不了包怎么办

想在 jail 侧对照,tcpdump 直接拒绝:

tcpdump: epair10b: Packet capture is not supported on that device
((there are no BPF devices))

jail 的 devfs 里没给 bpf。这时候不用折腾 devfs 规则,用两端计数器对照就够了:

$ jexec lims-pg netstat -I epair10b -b
epair10b  ...  0  0  0  0   5  0  168  0
#                            ↑jail 确实发出去了 5 个包

$ netstat -I epair10a -b    # 宿主侧 a 口
epair10a* ...  0  ...      0  0  0  0
#              ↑ 一个都没收到

单向丢包可能是路由问题,双向都丢、且发端计数在涨、收端计数是 0,就只能是这对 epair 本身不通。

四、根因:vnet jail 的 epair 不能「二次入狱」

epair 是一对背靠背的虚拟网卡(a 端留宿主、b 端塞进 jail 的 vnet)。正常生命周期是:建对 → b 端 move 进 vnet 一次。

问题出在「再来一次」。只要 jail 走过 restart(或 stop + start),jail(8) 会把已经存在的那对 epair 再次 move 进 vnet——这一次之后,跨 vnet 的转发通道就废了:

  • 两端 ifconfig 都是 UP/RUNNING
  • bridge0 的 member 列表照旧
  • jls 里 jail 好好地跑着
  • 但帧到了 epairXa 就报错,出不去

这也是为什么它特别难查:所有「看状态」的命令都在告诉你没事。

有个反直觉的现象可以佐证配对关系没乱:

$ sudo service jail stop lims-pg
$ ifconfig -l | tr ' ' '\n' | grep epair10
epair10a
epair10b        # ← 停 jail 后 b 端“回到”宿主 vnet
$ sudo ifconfig epair10a destroy
$ ifconfig -l | tr ' ' '\n' | grep epair10
# epair10b 跟着一起消失了 → 证明 a/b 确实是同一对,不是配对错乱

同一对、没配错、就是不转发。 所以修法只有一个:整对销毁重建。

五、修法:四步重建

NAME=lims-pg; N=10

sudo service jail stop $NAME
sudo ifconfig epair${N}a destroy          # a 端一销毁,b 端连带消失
sudo ifconfig epair${N} create            # 建出全新的 a/b 对
sudo ifconfig epair${N}a up               # 少这一步会因为缺 IFF_UP 完全不通
sudo ifconfig bridge0 addm epair${N}a
sudo service jail start $NAME

验证:

$ ping -c 2 10.0.10.10
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.071/0.071/0.071/0.000 ms

$ nc -z -w 2 10.0.10.10 5432 && echo PG-REACHABLE
PG-REACHABLE

两个坑

坑一:bridge0 deletem / addm 没用。 这是网上最常见的建议(我自己的旧笔记里也这么写过),但成员关系本来就是对的,重置成员治不了「对端已死」。必须销毁重建。

坑二:别忘了 ifconfig epairXa up。 新建出来的 a 端默认不是 UP,只 addm 到 bridge 而不 up,现象和没修一样——很容易误判成「重建也没用」。

六、把一次性修复变成机制

手工修五个 jail 只是止血。下次谁再 service jail restart,同样的坑还在。所以把「重建」挂进 jail 的生命周期:

#!/bin/sh
# /usr/local/sbin/jail_epair.sh up|down <base>   例:jail_epair.sh up epair10
set -u
ACTION="$1"; BASE="$2"

case "${ACTION}" in
up)
    if ifconfig "${BASE}a" >/dev/null 2>&1; then
        exit 0                      # 开机时 rc.conf 已建好,直接用
    fi
    ifconfig "${BASE}" create || exit 1
    ifconfig "${BASE}a" up       || exit 1
    ifconfig bridge0 addm "${BASE}a"
    ;;
down)
    # 停 jail 后 b 端会回到宿主 vnet,销毁 a 端即连带销毁 b 端,
    # 保证下次 start 一定走 up 分支建出「全新的对」
    ifconfig "${BASE}a" destroy >/dev/null 2>&1 || true
    ;;
esac

/etc/jail.conf 里每个成员加两行:

lims-pg {
    vnet;
    vnet.interface = epair10b;
    exec.prestart = "/usr/local/sbin/jail_epair.sh up epair10";
    exec.poststop = "/usr/local/sbin/jail_epair.sh down epair10";
    ...
}

为什么是 poststop 销毁,而不是 prestart 里判断「旧的是不是坏了」? 因为坏掉的那对在用户态根本看不出异常(这正是这次排错的全部难点)。既然判断不可靠,就不要判断——每次停机都销毁,每次启动都新建,让「复用旧对」这条路根本不存在。

改完实测:

$ sudo service jail restart lims-redis
$ netstat -I epair20a -b | tail -1
epair20a  1500 <Link#6>  58:9c:fc:10:22:0e   6  0  0  364   3  0  238  0
#                                            ↑收 6      ↑发 3  ↑Oerrs=0
$ ping -c 2 10.0.10.20
2 packets transmitted, 2 packets received, 0.0% packet loss

顺便把这个「开机路径」单独验了一遍(不动生产 jail):新建一对 epair99、挂 bridge、再起一个临时 jail 把 b 端塞进去——Oerrs=0、ping 通。这说明首次建链没问题,问题只在复用,和上面的结论对得上。

七、第二个坑:它开机时压根没尝试启动

链路修好后 service lightmem start 两秒就起来了(不再等 60 秒)。但问题还没完:下次重启机器,它还会自动起来吗?

翻日志:

$ grep '\[boot\]' /var/log/lightmem.log | tail -3
2026/10/27 10:14:41 [boot] profile=family backend=pgvector tier=family
2026/10/27 10:14:41 [boot] listening on :18443
2026/11/06 08:12:12 [boot] ...        # ← 这是我手工起的

11-05 那次开机(kern.boottime = 07:43:18)一条 [boot] 都没有。也就是说:二进制从未被拉起,它是卡在「等 PG」那一步就退出了,连日志都来不及写(超时消息走的是 stderr/console)。而且超时后不重试。

再看启动顺序,问题的全貌就出来了:

$ rcorder /etc/rc.d/* /usr/local/etc/rc.d/* | grep -nE '/(jail|lightmem|NETWORKING)$'
85:/etc/rc.d/NETWORKING
86:/usr/local/etc/rc.d/lightmem      # ← 第 86 位
216:/etc/rc.d/jail                   # ← 第 216 位

lightmem 比 jail 早启动 130 个服务。 而它的等待窗口只有 60 秒——11-05 那天从开机(07:43:18)到 jail 起来(07:49:36)用了 6 分 18 秒。窗口永远等不到,这就是「必然失败」而不是「偶发失败」。

冷知识:.bak 脚本会被开机执行

顺手发现 rc.d 里躺着几个历史备份:lightmem.bak.r28、lightmem.bak.r29、lightmem_mcp.bak.r29——全部可执行,且都带 # PROVIDE: lightmem。

FreeBSD 的 /etc/rc 是把 /usr/local/etc/rc.d/* 整个 glob 喂给 rcorder 的,.bak 不会被跳过。也就是说以前每次开机,可能有多个「lightmem 启动脚本」一起跑。

结论:在 rc.d 里留备份,等于留了个会在开机时执行的幽灵。备份请挪到 rc.d 目录之外。

八、三层兜底

修完之后是这样一套组合,而不是单点:

第一层:改依赖顺序。 让 lightmem 排在 jail 之后。

# REQUIRE: NETWORKING jail

改后 rcorder:jail 212 → lightmem 217 → lightmem_mcp 219。这里有个关键前提——jail 的 exec.start 是同步执行 /bin/sh /etc/rc 的,所以 service jail start 返回时,PG 已经在监听,不需要靠 sleep 猜。

第二层:放宽等待窗口。 lightmem_pg_wait 从默认 60 提到 180,纯粹当安全垫(比如 PG 要做 WAL 回放时)。

第三层:watchdog。 每 2 分钟一次,逻辑刻意做得「保守」:

# PG 不可达 → 只记日志,绝不动手
if ! nc -z -w 2 "${PG_HOST}" "${PG_PORT}" 2>/dev/null; then
    if fetch -q -T 2 -o - "${KERNEL_URL}" >/dev/null 2>&1; then
        log "WARN PG 不可达,但 kernel 仍在服务(PG 可能挂了)"
    fi
    exit 0
fi

# PG 可达但 18443 不健康 → 才拉起
fetch -q -T 3 -o - "${KERNEL_URL}" >/dev/null 2>&1 || service lightmem start

为什么 PG 挂了反而不动手? 两个原因:

  1. 强行拉起只会让进程反复启动失败、把日志刷满,掩盖真正的故障点;
  2. 「PG 不可达」和「lightmem 挂了」是两个故障,watchdog 不该把前者伪装成后者。

这个设计的价值在硬关机场景:断电后 PG 要走 WAL 崩溃恢复,可能远超 180 秒。这时 rc 那一轮会放弃,但 watchdog 会在 PG 就绪后的 2 分钟内把服务拉起来——把「开机一次性机会」变成了「持续收敛」。

实测(不是看着日志猜):

$ /usr/local/sbin/lightmem-watchdog.sh          # 健康时:零动作,日志都没生成
$ sudo service lightmem stop && sudo service lightmem_mcp stop
$ /usr/local/sbin/lightmem-watchdog.sh
$ cat /var/log/lightmem-watchdog.log
09:14:48 [watchdog] kernel 18443 不健康,执行 service lightmem start
09:14:52 [watchdog] kernel 已恢复(pid 38046)
09:14:52 [watchdog] mcp 18444 不健康,执行 service lightmem_mcp start
09:14:55 [watchdog] mcp 已恢复

九、可以抄走的七条

  1. 状态类命令(ifconfig/jls/service status)会骗人,计数器不会。 链路怀疑时先 netstat -I <if> -b,看 Ipkts/Opkts/Oerrs,别只看 status: active。
  2. bridge0 addr 的 MAC 表为空,是个极强的负向信号。 桥连自己的成员都没学到东西,说明帧根本没进出。
  3. 抓不了包就比计数。 两端 netstat -I 的收发差,等价于一次单向 tcpdump,还不用碰 devfs/BPF 权限。
  4. vnet jail 的 epair 是一次性用品。 只要走过 restart,就重建;判断「坏没坏」不如直接重建便宜。
  5. 验证服务别只看端口。 healthz 说 status: ok 可能只读了配置;跑一次真实的业务请求(这里是 POST /tools/memory/recall 拿到命中)才算数。
  6. 「起不来」和「没启动」是两件事。 先看日志里有没有启动记录([boot] 之类),没有就说明它卡在启动前的检查里,方向完全相反。
  7. 排错笔记要写「怎么判断」,不要只写「怎么修」。 这次能一次定位,靠的是旧记忆里那条「epairXa 计数异常」的线索;而旧记忆里「bridge0 deletem/addm 可修」那条是错的——错的结论会原样传给自己下一次,所以修完要回去改掉它。

整件事最值得记的不是那四步命令,而是一个教训:

当所有「状态」都正常、只有「行为」不正常时,说明你在看错的东西。

ifconfig 看的是配置,ping 看的是行为。这次配置全对、行为全错,差的就是那两行不该为 0 的计数器。

Logo

更多推荐