分布式 Agent:Rust(agent-rs)与已下线的 Go Agent 完整对比
状态(2026-10-09):Rust agent(
agent-rs/,cargo workspace)是唯一的分布式 agent。原 Go agent(cmd/agent)已退役并从仓库移除——其最后的发行版为 v0.6.0-95;两个实测节点自 2026-10-06 起已全程运行 Rust agent。本页是支撑该决策的完整工程对比,所有数据均标注测量来源。
为什么重写
Go agent 功能完好,但在产品真正面向的边缘硬件(ARMv7 单板机、256 MB–1 GB 内存的路由器、flash 存储)上,有几项成本始终无法消除:
- 内存下限:Go runtime + 指纹语料编译使 agent 常驻内存停留在 40–110 MB 区间(取决于语料编译策略)——比一块 ARMv7 板子愿意分给单个守护进程的内存高出一个数量级。
- GC 尾部抖动:扫描 worker + GC 循环造成 RSS 锯齿波动(38–63 MB 摆幅),小机型的容量规划只能靠猜。
- 分发:Go agent 从未以带对等测试覆盖的正式制品发布过,历次部署都是临时构建。
GitHub issue #471 把替换工程框定为严格 TDD,并设置了硬性验收门槛:分类器与 Go 实现逐字节一致(全量语料对拍)+ 与现有 center 的线上协议兼容。不搞"重写了事"——每个里程碑都挂在差分测试后面落地。
实测对比
测量方法
- Go 基线:来自 Go agent 在同一硬件上的生产期记录(NanoPi NEO,ARMv7,512 MB,systemd;center 的
lan-63网段)。内存区间来自 2 天 96 轮观察器(2026-10-04 → 10-06);扫描时长出自同一台账。语料急切→惰性编译的优化(指纹库 v0.1.1)先受益的是 Go agent——下表 Go 数字是优化后的。 - Rust 数字:同一块板上
mibee-agent-rs/0.1.5的生产实测(2026-10-09),加上同一提交下两个 agent 的全新构建做二进制体积对齐。 - 两者均为
-s -w/release 构建、静态链接(无运行时依赖)、相同扫描目标与节奏。
二进制体积
| Go agent | Rust agent | 倍率 | |
|---|---|---|---|
| linux/armv7(部署架构) | 19,988,640 B | 4,795,716 B | 4.2× |
| linux/arm64 | 19,464,352 B | ~4.8 MB | ~4.1× |
内存(同一块板、同一网段、生产流量)
| Go agent | Rust agent | 倍率 | |
|---|---|---|---|
| RSS 区间(2 天观察器 96 轮) | 38.5 – 62.8 MB | 10.9 – 12.4 MB | — |
| RSS 中位数 | 54.1 MB | 11.9 MB | 4.5× |
| 2026-10-09 实时采样(连续运行 2 天 8 小时) | — | 12.6 MiB RSS / 11.8 MiB(systemd MemoryCurrent) | — |
| 优化前稳态(急切语料编译期) | ~110 MB | — | — |
Rust agent 的内存不光更低,而且平:没有 GC 锯齿——这正是小机型容量规划要的确定性。
扫描性能(/24,主动扫描,相同目标)
| Go agent | Rust agent | |
|---|---|---|
| 时长区间 | 142 – 165 s | 84.8 – 88.6 s(6 次新鲜采样,alive=45) |
| 相对吞吐 | 1× | ~1.7× |
收益主要来自 Rust 引擎的探针调度与 SNMP 门控:L2/MIB 探针只对第一段已证明讲 SNMP 的主机运行(Go 引擎对每台非 SNMP 主机都白付数轮 walk 超时)。实测:0.1.4 加入五个 L2 探针后 /24 仍为 85 s(加之前 86 s)。
本地数据库足迹
| Go agent | Rust agent | |
|---|---|---|
agent.db 稳态 |
40 – 47 MB(保留清扫 + VACUUM 后) | 2.58 MB(同一保留策略) |
如实说明:两个 agent 写入本地影子表的行数并不完全一致,此行是指示性的而非严格同口径。但方向没有歧义——Go agent 即便带着清扫,文件也会爬向 flash 不友好的几十 MB;Rust agent 稳在个位数 MB。
测试与验证深度
| Go agent | Rust agent | |
|---|---|---|
| 单元/集成测试 | 105 个测试函数(cmd/agent + internal/agent) |
172 个测试(workspace,cargo test) |
| 分类器对等 | 参照实现本体 | 逐字节对拍:全量语料(约 2,600 条规则)双种子对照 Go 分类器,oracle 在库(agent-rs/difftest/) |
| SNMP 线上验证 | 信任库(gosnmp) | 对真实 net-snmp 差分:v2c walk 5/5、RFC 4293 回退 5/5、v3 authPriv(SHA/AES)5/5——全部逐字节一致;与 gosnmp 跨语言互操作 11/11 |
差分测试立刻回了本:它揪出了我们自己 v3 编码器的一个潜伏 BER bug(msgMaxSize 65535 被编成 FF FF = 有符号 −1;真实 agent 会静默丢弃我们发过的每一个 v3 报文——自测用的宽容版 echo server 永远发现不了)。已修复并加回归测试;这类 bug 正是验收门槛要设成字节级的原因。
代码规模(如实的成本)
| Go agent | Rust agent | |
|---|---|---|
| 代码行数 | 5,152 行(Go,借力 gosnmp/yaml.v3/Go crypto) | 15,937 行(Rust,自包含:自研 BER、SNMPv3 USM + 密钥本地化、AES-GCM vault、cron 解析器) |
3 倍的代码增长是重写两个初始动机的刻意代价:运行期内存无意外、不依赖分配行为不受我们控制的库。Go agent 把线上格式交给库,Rust agent 自己拥有它们——也拥有它们的差分测试。
功能对等矩阵(终态 0.1.5)
| 能力 | Go agent | Rust agent | 备注 |
|---|---|---|---|
| 六端点 center 协议(上报/轮询/命令/指纹/…) | ✔ | ✔ | 与同一套 wire 类型做契约测试 |
| 分类器(指纹语料) | 参照本体 | ✔ 逐字节一致 | 全语料对拍,语料 rev 全 fleet 共享(退役时 86e1ff8a) |
| SNMPv1/v2c + v3 USM(authPriv、SHA/AES、密钥本地化) | ✔(经 gosnmp) | ✔(自研实现) | 差分测试见上 |
| 凭据 vault(AES-256-GCM,密文不出机) | ✔ | ✔ | blob 格式已文档化供迁移 |
| 指纹语料同步(fpsync,rev 协商、校验后换装) | ✔ | ✔ | 同一 center 端点 |
| 本地 cron 扫描任务(robfig 5 字段语义) | ✔ | ✔ | 区间/列表/步进/日-周 OR |
| 路由器 ARP walk(legacy + RFC 4293,单飞缓存) | ✔ | ✔ | 跨网段 MAC 解析 |
| L2 拓扑探针(Bridge/Q-BRIDGE/LLDP/CDP/STP) | ✔ | ✔ | 挂 SNMP 门控(Go 对非 SNMP 主机白付超时,Rust 不付) |
neighbors wire 数组 → center device_neighbors |
✔ | ✔ | 同一发布列车双侧落地 |
| 主机名证据通道(NBNS、TLS 证书 CN → hostname 规则) | ✔ | ✔ | 0.1.4/0.1.5 |
| hostapd/iw WiFi 站点遥测 | ✔ | ✔ | 等待带 AP 的实测环境做活体验证 |
| 上报携带心跳探针规格 | ✔ | ✔ | |
| 保留清扫 + VACUUM | ✔ | ✔ | |
| conntrack 新主机上报 | ✔ | ✔ | ESTABLISHED/[ASSURED] 流的 LAN 端点(src 或 dst 侧,含 [UNASSURED] 拒收的解析对等)首次目击上报一次(#504) |
mDNS 单播查询(scanner.mdns.unicast_queries) |
✔ | ✔ | 每个查询同时直发目标 5353,覆盖只应答单播的设备(#20/#504) |
dns_log 源(dnsmasq --log-queries 尾随) |
✔ | ✔ | 字节偏移续读、轮转重置、首扫 EOF 跳过;查询域名成为扫描种子(#504) |
配置(agent.yaml 键 + MIBEE_ 环境变量) |
✔ | ✔ | 同构——存量 agent.yaml 无需改动即可使用 |
| 被动 eBPF TC 观测器(center #493/#496/#497/#508 全套) | ✗(Go agent 从未搭载) | ✔ feature ebpf(#498) |
同一内核对象、aya 加载器、默认关闭 |
被动 eBPF 观测器(#498,feature 门控)
center 侧的 TC 观测器(eBPF 被动观测)同样下沉到了 Rust agent,
以可选 cargo feature 的形式提供。内核侧程序是共享而非重写的:内嵌对象
(agent-rs/crates/mibee-ebpf/ebpf/tc_ingress.bpfel.o)与 center 的
bpf2go 产物逐字节相同 —— bpf2go 的 go generate 步骤一条命令同时刷新
两份拷贝(其第二条指令就是拷贝),漂移测试在两份同时存在的机器上把它们
钉在一起。一份经过实机验证的 verifier 产物,两个加载器。用户态加载器是
aya —— 纯 Rust、无 libbpf/BCC —— 因此静态 musl、
零 C 依赖的特性在开启 feature 后依然成立。
构建与配置:
- 构建:
make build-agent-rs-ebpf(两个 musl 架构),或cargo zigbuild --release --target <musl-target> -p mibee-agent --features ebpf。 默认构建不受影响 ——mibee-ebpfcrate 在依赖图中是可选且仅 Linux 的, 默认产物逐字节不变,aya 完全不进入。 - 配置(
agent.yaml):scanner.discovery.ebpf.enabled: true与scanner.discovery.ebpf.interfaces(留空 = 所有 up 的非 loopback 接口)。 配置键在任何构建下都可解析;默认构建会打一行 warn 并保持该源关闭。 - 要求:Linux ≥ 5.8 —— aya 在 ≥ 6.6 走 TCX、更老的内核回落到经典
clsact/netlink 挂载,比 center 仅 TCX 的加载器(6.6)低一档。root 或
ambient
CAP_BPF+CAP_NET_ADMIN;BTF 可选(CO-RE-free 程序)。 实测注意:6.18.44 sunxi armv7 rig 上 ambient-caps 形态会挂死 BPF 程序 加载(与 center 侧记录的同族内核怪癖)——该 vantage 的 agent 以 root 运行;MIBEE_EBPF_DEBUG=1会打出每条路由决策供现场排查。
上报面(与 center 漏斗同一路由规则):
- ARP 在网目击按 IP 首次目击上报一次被动主机,并为该主机的下次扫描 种入 MAC;ND 不带 IPv4,等待 MAC 键通道(#522)。
- 签名类(SSH / RTSP / HTTP / WS-Discovery / DHCP / TLS-SNI / mDNS /
SSDP)成为
passive:ebpf:tc证据种子,随该主机的下一次扫描报告上行 —— 走既有 agent→center 通道,无协议变更。
降级:任何失败(非 Linux 主机、内核 < 5.8、缺 caps、加载/挂载失败)只打
一行 warn 并保持该源关闭,主动探测不受影响。能力探测机是 center 侧逐行
移植到 crates/mibee-ebpf/src/capability.rs 的 1:1 端口,在所有平台上
都有单元测试,包括非 Linux 路径。
实战记录
- 2026-10-06:两个实测节点的 agent(NanoPi NEO armv7 走 systemd,FastRhino R68S arm64 走 iStoreOS 网页包通道)切换为 Rust agent。自该时刻起,节点的识别管线就是 Rust-only。
- 0.1.0 → 0.1.5,两节点共五次生产升级:零失败升级、零服务重启(整个窗口
NRestarts=0)、零扫描节奏缺失。随机 MAC 守卫、多宿主稳定性修复、TLS-CN 主机名通道都在这个窗口内发布并实测验证。 - 0.1.7(#554):上报器不再把 401 当终结态。该缺陷在 2026-10-12 主中心换库时被实测抓到:约 3 分钟的令牌恢复空窗让两个 agent 丢了批次,受影响的 agent 需人工重启才恢复上报。现在 401 会把批次压入队列并按 5 分钟冷却窗口低频重试——中心重建后原样恢复令牌的场景无需任何人工干预即可自愈。停机路径同样有界:
stop_flush只重试一轮即止,不会在排不空的队列上空转。 - Go agent 的最后一次生产值班是 2026-10-06 01:27 UTC(NEO)。2026-10-09 从仓库移除。
运维者退役须知
- 安装/升级路径:见分布式部署。Rust 采集器以静态 musl 二进制交付(
make build-agent-rs,aarch64 + armv7),路由器侧提供与中心相同的三种包形态(make package-openwrt-agent-rs{,-ipk,-apk};发版 tag 会挂上 GitHub Release)。 - 配置:存量
agent.yaml无需修改即可用于 Rust agent(同键;未知键会记日志警告并忽略)。 - 本地数据:Rust agent 的
agent.db是自有 schema(v1)。Go agent 的agent.db不做迁移——它只承载设备影子与扫描历史,center 的资产台账才是权威数据源,首次扫描即回填。 - 版本串:Rust agent 在 center 的 fleet 视图上报
mibee-agent-rs/x.y.z;混合舰队过渡期内 center 同时接受旧 Go 格式。
发布与验证门禁
- CI:cargo workspace 在每个 PR 上跑独立测试任务(
agent-rs)——全量测试套件加全语料加载,规则数断言为下限(语料只增不减)。2026-10-09 之前这套测试只在开发机上跑;该任务上线的当天就抓到一个在语料批次中静默断裂的计数 pin。 - 发版:
v*tag 构建静态 musl 二进制,覆盖 amd64、arm64 与 armv7,版本串在构建期由 tag 注入(MIBEE_AGENT_VERSION;日常构建报 Cargo.toml 版本)。制品以mibee-agent-linux-amd64/-arm64/-armv7挂上 GitHub Release,流水线还会在 runner 上执行 amd64 二进制核验版本戳。 - 实测覆盖率:两 crate 合计 74.3% 行 / 74.6% 函数(
cargo llvm-cov,2026-10-09,19,244 行被统计)。这个数字低估了验证强度:对等关键的分类器与 SNMP 层由全语料字节对拍和上文的真实 net-snmp 差分把门,行覆盖率看不见它们。Rust 侧的覆盖率棘轮是候选后续项,当前不是门禁。
已知缺口 / 后续项
- MIPS:Rust agent 可构建
mipsel(QEMU 下的对拍 oracle 在用),但 MIPS 实机未验证;center 本身也不支持 MIPS(modernc/libc)。 - hostapd 活体验证:WiFi STA 源在测试中解析真实
hostapd_cli/iw抓取;仍欠一台带 AP 的实测环境。 - 软件源发布:采集器的
.ipk/.apk为手搓二进制包、从文件安装;订阅 OpenWrt 软件源(buildroot 集成、签名)仍待做,与中心包共用此项。