Commit Graph

2 Commits

Author SHA1 Message Date
98dcb2556c fix(test): 崩溃隔离测试误杀同机生产插件,且断言无效
## 现象

生产 homed 的 editdoc 子进程从 9-03 起被 SIGKILL 9 次,间隔完全不规律
(126~485 分钟),全部发生在无工具调用的空闲期。查过 OOM(dmesg/
journalctl -k/cgroup oom_kill 全为 0)、systemd 内存限制(MemoryMax=
infinity)、cron/timer、内核自身的 StopAll 路径、agent 执行过的 cmd_run
命令,以及 Pdeathsig 绑创建线程的可能——全部排除。

## 真因:测试杀了生产的进程

用 ftrace 的 signal_generate tracepoint 挂监视器后抓到发送者 cmdline:

  /tmp/go-build.../plugins.test -test.run=TestRealPlugin_CrashDoesNotKillKernel

findPluginPID 用**全系统** `pgrep -f plugin.bin`,然后只比"exe 路径含
editdoc"。生产实例的 /home/newqqagent/plugins/editdoc/plugin.bin 也满足
这个条件,谁先被 pgrep 列出来就杀谁。9 次 kill 全部落在有人跑 go test
的时段——18:18:30 那次正是一轮 `go test ./... -race` 的窗口。

之前几轮排查一直在生产实例内部找原因,方向从一开始就错了:杀手在仓库里。

## 更严重的是这个测试本身无效

旧断言是"SIGKILL 之后内核仍存活"。可内核本来就活着——即使信号发错了
对象(杀了生产实例的插件),测试内核的插件压根没死,断言照样通过。
**它在测一件没发生的事**,同时把生产环境打坏了,而绿色的测试结果掩盖了
这一切。这也是它能连续 9 次造成生产故障却从没被注意到的原因。

## 修法

findPluginPID 增加 root 硬约束:/proc/<pid>/exe 必须以测试自己的 plgDir
为前缀,且是目标插件,两道条件同时成立才算命中。root 为空直接 t.Fatal
——这不是可选过滤器,是防误杀的前提。

  - 用 exe 而非 cmdline:cmdline 可被进程自行改写,exe 符链由内核维护。
  - root 先过 EvalSymlinks:/tmp 在部分发行版上是符链,不归一化会让前缀
    比较永远不命中,退化成静默 Skip(那样测试就白跑了)。

断言改成两步:先轮询确认目标进程真的退出(3s 上限),再验内核未被连带。
两步都成立才能证明隔离生效。

## 验证

修复后跑 TestRealPlugin_CrashDoesNotKillKernel:
  - 杀的是 pid=3448366,exe 在 /tmp/hc_integration_3823918128/plugins 下 ✓
  - 生产 editdoc pid 测试前后均为 3362892,存活时长连续 ✓
  - 四个 TestRealPlugin_* 全部 PASS
2026-09-04 21:44:05 +08:00
1d7f011e5d plugin: 17 插件全量重编 + 端到端冒烟验证(Part 6.3)
## 17 个插件源码零改动,全部重编为 plugin.bin

16 个 × 3 平台(linux/darwin/windows),qq 1 平台(plg.json 自己声明
bundle:false)。luademo 走 Lua 解释器不适用。

git status example/ 无输出 —— 这是「业务代码零改动」的硬证据。
批 3 那些预估高风险的插件(qq 2686 行双向通道、browser 12 工具 +
InjectInterruptText、a2a/acp 的 InjectInputSync 同步注入)一次全过,
因为它们只碰公开 SDK 合同面,而合同面在 Part 2 已 51 个 method 全量平移。

唯一一次失败与迁移无关:rss 的 github.com/mmcdole/gofeed 不在本地模块
缓存且 proxy.golang.org 不通,换 GOPROXY=https://goproxy.cn 后通过。

## 真实 homed 加载验证

15 个外部插件全部经 proc 通道建链(protocol=1 sdk=0.9.2,各自独立 PID),
31 个插件 loaded(15 外部 + 16 内置)。

ai_image / files 未走 proc 通道:同名内置插件优先(工厂编译期注册),
外部插件被遮蔽。这是既有行为,与迁移无关。

事件环与共享段均正常创建,且共享段是**一块** 256KB 服务全部 15 个插件。

## 冒烟测试 4 项(internal/plugins/real_plugin_smoke_test.go)

用真实 example 产物而非 testdata 假插件;manifest 刻意写 "entry":"plugin.so"
验证工具链与内核都已不看 entry 值。未重编时 skip 而非 fail。

- ToolInvokeRoundTrip:工具真实调用往返(此前只验证到"注册")。
  weather_current 返回结构化参数校验错误——这恰是链路通的证据。
- StageRewriteTakesEffect:sanitizer 清洗 ANSI 序列,改写经共享段回到
  内核 StageContext
- MultiPluginShareOneSegment:sanitizer + weather 并发,清洗结果不被覆盖
- CrashDoesNotKillKernel:SIGKILL 插件进程后 homed 存活、17 插件仍在
  (对比 C ABI 下插件 panic 直接带崩 homed,§1.2 现网已发生)

## 开销实测与基线偏差

15 个插件进程 RSS=88.0MB PSS=87.9MB 线程=82,均摊 5.87MB / 5.5 线程。

RSS 88MB vs 实验 5 基线 29.1MB **不是回归,是基线不可比**:实验 5 用
2.68MB 最小插件,真实插件 3.1~14.8MB。可比的结构性指标:
- 均摊线程 5.5 vs 4.9 —— 同量级,无线程膨胀
- PSS/RSS 99.9% vs 44% —— **明显差于基线**

第二项是真实发现:基线里 PSS 远低于 RSS 说明 Go runtime 只读代码页在
进程间共享;实测几乎不共享,因为 15 个插件是 15 个不同的二进制,没有
共同物理页可映射。这是「每插件独立二进制」的固有代价,意味着实际内存
开销高于 §4.3 的乐观估计。压这一项的方向是共享 launcher 二进制。

## 工具脚本入 experiments/19-migration-verify

scripts/ 被 .gitignore 排除,故放到已跟踪的 experiments 目录下,
与 01~18 的可复跑实验并列。

measure-plugin-overhead.sh 第一版有统计口径 bug:RSS 读 status 的 VmRSS、
PSS 读 smaps_rollup 的 Pss,输出 PSS(87.9MB) > RSS(69.1MB) —— 物理上不可能。
两者对共享内存段计入方式不同(smaps 的 Rss 含 Pss_Shmem)。已统一从
smaps_rollup 读。另修 bc 不可用导致 MB 全显示 0.0(改用 awk)。

Ref: docs/zh/plugin-migration-plan.md Part 6.3、docs/zh/架构迁移评估.md §4.3
2026-09-02 20:20:25 +08:00