mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 05:13:27 +00:00
打的是**生产实例** 127.0.0.1:8080。 ## 为什么只压只读端点 压测绝不能改状态。18 个端点**逐个探测确认**是 GET + 只读: 小(/status…/proxy/services,104B–1.8KB)、中(/plugins…/knowledge, 4.4KB–53KB)、大(/kernel 157KB、/memory/graph 338KB、/knowledge/tree 425KB)。 **排除**:/chat(真调 LLM)、/chat/interrupt(中断在跑的任务)、 /settings/* 与 /plugins/*(改配置)、/login /logout(改会话)、 knowledge/memory 写接口、/device/*(控制真实设备)。 ## ★ 两条判据(都不是"看有没有报错") ### 1. 必须先 `--probe`:webui 无认证时返回 200 + 登录页 HTML 含 `THEME_PLACEHOLDER`,**状态码是 200**。只看 `http_code` 会把登录页 当成健康响应 ⇒「全部 200」是假的。脚本因此额外校验响应体。 ### 2. 端点表漏了 `/api/v1` 前缀 ⇒ 18 个端点全 404 第一版把端点存成路径后半段(`"/status"`),拼出 `http://127.0.0.1:8080/status`,而真实路径是 `/api/v1/status` ⇒ **全部 404**,同一时刻 curl `/api/v1/status` 却是 200。 ⇒ **压测脚本必须先 probe 再压。** 不 probe 的话那一跑的结论会是 「webui 全挂」,完全错误。这条已写进手册。 ## 实测(2026-09-28) | scale | 请求 | 吞吐 | p50 | p95 | p99 | max | 成功 | | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | 360 | 779 req/s | 6.7ms | 26.8ms | 45.5ms | 51.6ms | 360/360 | | 2 | 720 | 788 req/s | 12.2ms | 52.6ms | 163.0ms | 213.5ms | 720/720 | | 3 | 1080 | 1038 req/s | 16.4ms | 60.4ms | 79.7ms | 123.0ms | 1080/1080 | **2160 请求零失败**;压测期间服务端 `active`、0 个 5xx、0 个 webui 错误, QQ/agent 链路未受影响,homed CPU 仅 1.2%。 ### 重响应不是瓶颈(并发 1 → 16) | 端点 | 大小 | 并发 1 | 并发 16 | 吞吐 | | --- | --- | --- | --- | --- | | /status | 0.2KB | 104 req/s | **1781 req/s** | 320KB/s | | /kernel | 153KB | 126 req/s | 375 req/s | 19 → **57 MB/s** | | /memory/graph | 330KB | 70 req/s | 410 req/s | 23 → **135 MB/s** | | /knowledge/tree | 415KB | 79 req/s | 717 req/s | 33 → **297 MB/s** | 大 JSON 端点的 p50 **不随并发上升**(/knowledge/tree 12.3ms → 8.9ms, 排队更充分、效率更高)⇒ 瓶颈不在 JSON 序列化。 ## ⚠ 一处未下结论的观察 scale=2 的 p99(163ms)反而**高于** scale=3(79.7ms)。看着反常,但当时 机器上另一个 agent 的 chromium 占 **766% CPU**(另有 cjpm/cjc 在编译) ⇒ 是**环境噪声**。 **未在可比条件下重测就不下结论** —— 不拿这一组当性能特征。要判定需先 固定负载条件再跑。这条也写进手册。 ## 顺带 ruff 报的三条 blocker 都修了:`pct()` 空样本不再抛错(调用方直接拿去做 f-string 格式化)、写 JSON 失败明确报错而非静默丢报告、错误体读取用 `contextlib.suppress` 免得二次异常盖掉真正的状态码。
内核二进制压力测试(kernel-stress)
对本仓库编译出来的真实内核做压力测试 —— 与 go test 的区别是:它跑真二进制、
真插件加载、真 unix socket 协议,因此能抓到只在集成面上出现的问题
(已有战绩:根 agent DataDir 漏接线、插件通道没登记为 inputch、create 后子不开工)。
为什么必须放在私有 netns 里
生产实例占着 *:8080 / *:9890 / *:9876,而插件的监听都设了 SO_REUSEADDR:
同机再起一个实例会在 127.0.0.1 上与之并存绑定(实测抢到过 127.0.0.1:9890 约 1 分钟)。
unshare -n 后实例只有 lo,结构上不可能碰到生产端口。
unix socket 是文件系统对象,跨 netns 仍可驱动,所以驱动脚本在 netns 外也能用。
前置
go build -o /tmp/homed-stress ./cmd/homed # 被压的内核
export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath TMPDIR=/var/tmp/gotmp
用法
# 1) 准备数据目录 + 把 LLM 指向本地 mock(无外网也能跑,且快、可控)
DATA=/var/tmp/kstress
mkdir -p $DATA
# 先跑一次实例建出 config.db,再写入下面这些键(也可直接复用现成目录):
# core.llm.provider=mock
# core.llm.sources.mock.base_url=http://127.0.0.1:9099/v1
# core.llm.sources.mock.model=mock api_key=mock adapter=openai
# core.llm.sources.mock.adapter_path=adapters/openai.lua priority=100
# core.defaults.llm_endpoints=http://127.0.0.1:9099/v1/models # 探活端点(探活用 HEAD!)
# core.defaults.rollback.max_retries=100000 auto_rollback=false
# 并把 core.llm.sources.deepseek* 删掉(netns 里它不可达,会让 agent 被判 degraded → rollback 循环)
# 2) 起 mock LLM + 内核(都在同一个私有 netns 里)
MOCK_DELAY_MS=300 MOCK_CHUNKS=8 ./launch.sh /tmp/homed-stress
# 3) 取认证密钥(cli 插件回落到 webui.api_key)
export KCLI_KEY=$(sqlite3 $DATA/config.db "select value from config_webui where key='api_key';")
# 4) 压
./kcli.py $DATA/cli.sock stats full # 看内核状态(含 scheduler 计数)
./stress.py $DATA/cli.sock "$KCLI_KEY" 16 12 4 20 dense 0.05 # 16 连接×12 输入 + 4 线程×20 中断
./stress.py $DATA/cli.sock "$KCLI_KEY" 12 12 1 25 0.0 3.0 # 稀疏中断 ⇒ 压抢占/挂起/恢复
./stress.py $DATA/cli.sock "$KCLI_KEY" 1 1 0 0 resident 0.0 # 驻留子全链路(mock 见 !resident 标记)
远程设备(agent ↔ 设备)端到端
设备网关在实例的 netns 内监听 127.0.0.1:9890,所以设备客户端要进同一个 netns 跑:
TOKEN=$(sqlite3 $DATA/config.db "select value from config_remotedevice where key='ws_token';")
nsenter -t $(cat $DATA/pid) -n python3 ./devclient.py --port 9890 --token "$TOKEN" \
--id pydev-1 --caps cmd --seconds 30 --out /var/tmp/push.txt
然后让 agent 主动发一条(mock 里 !push 会回一个 output_send__device/pydev-1 的工具调用):
# 经 CLI socket 发 "!push",设备侧应收到 {"op":"push", ...}
设备上线/下线会在内核里登记/注销输出通道 device/<id>,可用 /kernel 的 channels 观察
(在线时出现、掉线后消失)。
设计口径:agent → 设备必须是 agent 的主动调用(
output_send__device/<id>); 设备的上报(op=event)虽然会被注入成输入,但 agent 的回复不会被插件自动转回设备 —— 全仓只有 webui 与 cli 两个交互界面"主动转发"(把最终回复渲染成气泡/终端输出), 其它通道(qq、设备等)一律要求显式output_send__<通道>。
读结果
| 指标 | 含义 |
|---|---|
executed / rejected |
任务执行数 / 被拒数(压力下应为 0) |
峰值 峰值_队列 / 峰值_待处理中断 |
采样到的最大排队深度 / 待处理中断数 |
suspended / resumed / preempted |
抢占三件套。中断要放稀才会打在高优先级任务上:密集中断会互相同级(L4 vs L4)不抢占,数值会很低 |
max_suspend_depth |
中断栈结构上界(= 中断级数 4) |
已知坑
- 探活用 HEAD(
internal/network/monitor.go):mock 必须实现do_HEAD,否则 501 → 判不可达 → agent degraded → rollback 循环(会 reset 合并目录)。 - CLI 协议每条连接串行处理(
handleChat阻塞到本次响应产出):单连接狂发只会排成一条线,造不出队列压力 —— 必须多连接。 - 认证:连接后先发
/auth <key>,否则一切命令返回unauthorized。 mockllm.py的!resident/!notify标记用来让 mock 回工具调用,从而在真内核里驱动resident_agents/notify_parent。