mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
## 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
插件架构评估实验
../../架构迁移评估.md 中所有数字的来源。
18 项实验,一键复跑,用于复核结论或在改动后验证回归。
./run.sh # 跑全部(约 3-5 分钟)
./run.sh 12 13 # 只跑指定实验
./run.sh 1 1c # dlclose/NODELETE 组
依赖:go >= 1.21、gcc、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/ |
dlclose 对 DF_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-31,go1.25.12 linux/amd64,192.168.2.60(12 核)。
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.29ms(2218x) |
| 5 | 17 子进程常驻开销 | 29.1MB RSS / 12.9MB PSS,84 线程 |
| 6 | 子进程崩溃隔离 | 退出码 2,EOF 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:124 的 go 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 稳定在 35
37%,13 在 1.64.3%) - 实验 1 的线程增长为 0 或 1(取决于 netpoller 线程是否已存在)
- 实验 10 的加速比 14~22x(受 CPU 缓存状态影响)
- 实验 5 的 PSS 受同机其他 Go 进程影响(共享页计算)
结果不应变的(若变了说明环境或结论有问题):
- 实验 1b 中纯 C
.so的段数必须归 0,Go c-shared 必须不归零 - 实验 8 的「总字符数 == 最终长度」必须成立(零丢失)
- 实验 9 必须无死锁
- 实验 12 的内置模型必须 0%
- 实验 14b 的泄漏必须线性增长
已知限制
- 实验 8 的 arena 未实现压实,64KB 用尽即停止写入(写入次数 < 5×300 属预期, 见评估文档 3.3)
- 实验 12/13 是链路复刻而非直接调用生产代码,
证明的是「副本模型这一机制」存在缺陷,不能替代对
sanitizer/weather的真实行为回归测试 - 实验 5 的插件是最小 stdio loop(2.68MB),真实插件(如 qq 7.5MB)开销更高
- 无 Windows 环境,9.2 的 Windows DLL 缺陷未经实测,仅代码阅读