Files
HomeAgent/docs/zh/experiments/plugin-arch
JianFeeeee 62bdfa2b54 meta: 内核版本升到 1.0.0;生产切换脚本改走 hmap 正规通道(Part 6.5)
## 版本号

1.0.0:外部插件从 C ABI 动态库迁到子进程 + 共享内存。首个不再加载
`.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件必须用新版 plugindev 重编)。
SDKCompatibleVersion 同步升 1.0.0。

同时删掉 ABIVersion / CABINum / 51 个 Core<Method> 整数 ID —— 随 Part 6.2
删 internal/plugin/cabi/ 就已无使用者(grep 确认只剩定义处)。留着会让人
以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。

⚠️ 注意 Makefile 的 `VERSION ?= $(shell git describe --tags --dirty)`:
实际注入值来自 git tag,meta.go 里的默认值只在不带 ldflags 时生效。
make build 当前注入 v0.9.1-56-g2572688-dirty。要让 1.0.0 真正生效需打
v1.0.0 tag 或显式传 VERSION=1.0.0。

## 生产切换脚本重写

第一版是手工拷 plugin.bin + 手改 plugin.json 的 entry —— 那等于**重新实现
了一遍 hmap 解包逻辑,且实现得更差**。漏掉的东西:

  platforms 字段          hmap 内的 plugin.json 本来就写对了
  平台二进制选择          我硬编码 _linux_amd64,正规路径用 platformBinary()
  overwrite 语义          StopAndUnload 停旧实例但**保留配置表**
  失败回滚                os.Rename 备份旧目录,解包失败自动恢复
  校验                    validatePackage 查 manifest + 各平台二进制齐全

配置保留那条尤其关键:生产 17 个插件都有配置(qq 账号、weather 默认城市、
browser profile 路径)。我的脚本恰好没碰配置表所以侥幸不丢,但那是运气
不是设计。

改为 POST 到 pluginmgr 的 HTTP 端点(127.0.0.1:9876/plugins),
传 {path, overwrite:true} 走 installFromPath → installFromData。

保留的一个设计:**先全部校验再动手**。任一插件缺 hmap 就整批中止——
新 homed 不认 .so,「一半装了一半没装」的中间态最难排查。

## 生产切换已执行

顺序(先换二进制再装包,而非反过来):
  1. systemctl stop homeagent
  2. 换 /usr/local/bin/homed
  3. 起服务 —— 15 个 .so 插件报可操作错误被跳过,homed 本体与 16 个内置正常
  4. 逐个 POST 装 17 个 hmap(overwrite=true)
  5. 待重启核对

第 3 步顺带在真实二进制上验证了 Part 6.2 的可操作错误:
  [plugin] dynamic weather: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。
  外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin
  (业务代码无需修改)
不崩溃,只跳过。若反过来先装包,旧 homed 的 StopAndUnload 会停掉 qq
消息通道且无法重载 .bin,会卡在「插件全挂」的状态。

结果:17/17 成功,全部 config_kept=true;0 个残留 .so;17 个 plugin.bin
均有执行位;17 个 manifest 的 entry 均为 plugin.bin;无 .bak 残留。
bundle 包正确挑了当前平台(weather 目录只留 8.7MB 的 linux/amd64 那份)。

备份:/home/newqqagent-migration-backup-20260902-214812
(plugins 全目录 + homed.old + homeagent.service,162MB)

验证:go build ./... 通过;go test ./... 全仓无失败。

Ref: docs/zh/plugin-migration-plan.md Part 6.5
2026-09-02 22:40:47 +08:00
..

插件架构评估实验

../../架构迁移评估.md 中所有数字的来源。 18 项实验,一键复跑,用于复核结论或在改动后验证回归。

./run.sh          # 跑全部(约 3-5 分钟)
./run.sh 12 13    # 只跑指定实验
./run.sh 1 1c     # dlclose/NODELETE 组

依赖:go >= 1.21gcc、Linux用到 eventfd/memfd_create/dlopen)。 脚本在 mktemp -d 里构建,不污染主仓 go.mod;实验源码均带 //go:build ignore

拉取 golang.org/x/sys 需要网络(实验 1/2/4/8/10。本机走 clash

export HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890

目录

目录 主题 对应章节
01-dlclose-nodelete/ dlcloseDF_1_NODELETE 是 no-op 1.1 / 1.2
02-feasibility/ 新架构可行性 11 项 第七章
03-lost-update/ 副本模型的 lost update 8.4 / 8.6
04-cgo-uninterruptible/ cgo 调用不可中断 9.3

实验清单与最近一次实测结果

复跑于 2026-08-31go1.25.12 linux/amd64192.168.2.6012 核)。

01 组dlclose / NODELETE

# 实验 结论
1a Go 宿主经纯 C shim 加载/卸载第三层 .so 纯 C 目标可卸载Go c-shared 目标仍不可
1b /proc/self/maps 段数验证 纯 C: 5→0真卸载Go c-shared: 5→5
1c 版本化路径 dlopen handle 不同,ver=v2 生效(方案可行但泄漏,已否决)

关键DF_1_NODELETE 属于被卸载对象自身的 ELF 属性, 与谁调用 dlopen 无关——套任何层数的 C 中间件都绕不过去。

02 组:新架构可行性

# 实验 最近结果
1 eventfd 是否走 Go netpoller 200 goroutine 阻塞 → 线程 +0~1
2 跨进程 eventfd + 偏移解引用 父子 mmap 基址不同偏移仍正确post 10.9 µs
3 锁仲裁 RPC 往返成本 19.4 µs/次20000 次)
4 post-and-forget vs 同步 Publish 5.07s → 2.29ms2218x
5 17 子进程常驻开销 29.1MB RSS / 12.9MB PSS84 线程
6 子进程崩溃隔离 退出码 2EOF 2.5ms 感知,宿主存活
7 子进程热重载 同路径替换二进制 → v1→v2 立即生效
8 跨进程并发改写 StageContext 5 进程 × 300 轮,零丢失零撕裂
9 持锁进程崩溃自愈 无死锁,无需 robust mutex
10 二进制零拷贝 100KB/1MB/5MB → 14-22x,体积 100%
11 工具调用 RPC 延迟 p50 19.6 µs,占 LLM 往返 0.00065%

03 组:副本模型缺陷

# 实验 最近结果
12 副本模型 lost update 率 内置 0% vs 外部 35.8~36.8%
13 现网 sanitizer+weather 冲突 1.6~4.3% 清洗结果被覆盖

实验 12 的对照设计是重点:两组用完全相同的并发扇出 stages.go:124go func + wg.Wait()),唯一差异是 「共享同一 *StageContext」vs「快照-副本-写回」。

内置组 0% 证明并发扇出这个原始设计是正确的 副本组 36% 证明跨 C ABI 边界后锁语义失效才是缺陷所在。 不要据此得出"应该取消并发"的结论。

⚠️ 13 的比率随机器负载波动(观测区间 1.6%~4.3%)——它取决于两个插件 handler 的实际执行耗时比。文档正文引用 1.6% 是首次测量值, 应理解为「量级在百分之几」而非精确常数

04 组cgo 不可中断

# 实验 最近结果
14a cgo 死循环 vs 子进程 Kill cgo 泄漏;子进程 零泄漏
14b 泄漏增长曲线20 次) 泄漏 20 goroutine / 18 OS 线程,线性

复跑时的注意事项

结果会有波动,以下属正常

  • 实验 12/13 的丢失率随调度波动12 稳定在 3537%13 在 1.64.3%
  • 实验 1 的线程增长为 0 或 1取决于 netpoller 线程是否已存在)
  • 实验 10 的加速比 14~22x受 CPU 缓存状态影响)
  • 实验 5 的 PSS 受同机其他 Go 进程影响(共享页计算)

结果不应变的(若变了说明环境或结论有问题):

  • 实验 1b 中纯 C .so 的段数必须归 0Go c-shared 必须不归零
  • 实验 8 的「总字符数 == 最终长度」必须成立(零丢失)
  • 实验 9 必须无死锁
  • 实验 12 的内置模型必须 0%
  • 实验 14b 的泄漏必须线性增长

已知限制

  • 实验 8 的 arena 未实现压实64KB 用尽即停止写入(写入次数 < 5×300 属预期, 见评估文档 3.3
  • 实验 12/13 是链路复刻而非直接调用生产代码, 证明的是「副本模型这一机制」存在缺陷,不能替代对 sanitizer/weather 的真实行为回归测试
  • 实验 5 的插件是最小 stdio loop2.68MB),真实插件(如 qq 7.5MB)开销更高
  • 无 Windows 环境9.2 的 Windows DLL 缺陷未经实测,仅代码阅读