mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
test(kernel-stress): 把"真二进制压力测试"harness 固化进仓库(私有 netns + mock LLM + 多连接驱动)
本轮压测是从 /var/tmp 里现写脚本跑的,能复现才能长期用 —— 固化到这。
为什么要有它(与 go test 的分工):单测都在进程内、参数显式给,抓不到集成面问题;
这一轮它抓到了三个只有真二进制才暴露的问题(DataDir 漏接线、插件通道未登记为
inputch、create 后子不开工),以及两处生成器缺陷(旧 SDK 导致生成工程编译失败;
模板工程从不示范通道登记)。
- `launch.sh`:在**私有 netns** 里同时起 mock LLM 与内核实例。
必须隔离的原因:生产实例占着 *:8080/*:9890/*:9876 且插件设了 SO_REUSEADDR,
同机再起一个实例会在 127.0.0.1 上与之并存绑定(首次实测抢到 9890 约 1 分钟);
netns 里只有 lo,结构上不可能碰到生产端口。unix socket 是文件系统对象,
所以驱动脚本在 netns 外照样能连。
- `mockllm.py`:OpenAI 兼容 mock(可控延迟 + SSE 分块 + 按 `!resident`/`!notify`
标记回工具调用),让无外网也能跑、且**流式段可被中断**(压抢占路径的前提)。
注意必须实现 `do_HEAD`:内核探活用 HEAD,501 会被判不可达 → degraded → rollback 循环。
- `stress.py`:多并发连接轰炸(单连接是串行的,造不出队列压力)+ 中断线程 + 峰值采样。
- `kcli.py`:CLI socket 客户端(`/auth` → `/kernel` / 广播)。
- README:前置配置(含探活端点、rollback 关掉)、用法、指标读法、已知坑。
实测结果(详见 6e60351 提交信息):密集 274 任务 executed=274/rejected=0;
稀疏中断下 suspended=resumed=preempted=27;驻留子全链路 + 优雅退出不留孤儿。
This commit is contained in:
81
scripts/kernel-stress/kcli.py
Normal file
81
scripts/kernel-stress/kcli.py
Normal file
@ -0,0 +1,81 @@
|
||||
import json, os, socket, sys, threading, time
|
||||
|
||||
SOCK = sys.argv[1]
|
||||
|
||||
KEY = os.environ.get("KCLI_KEY", "")
|
||||
|
||||
def connect(auth=True):
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.settimeout(20)
|
||||
s.connect(SOCK)
|
||||
if auth and KEY:
|
||||
s.sendall(("/auth %s\n" % KEY).encode())
|
||||
buf = b""
|
||||
while b"\n" not in buf:
|
||||
c = s.recv(65536)
|
||||
if not c:
|
||||
break
|
||||
buf += c
|
||||
if b"authenticated" not in buf:
|
||||
raise RuntimeError("auth failed: %r" % buf[:120])
|
||||
return s
|
||||
|
||||
def kernel_stats(sock=None, timeout=15):
|
||||
"""连一条新连接查 /kernel,返回解析后的 dict。"""
|
||||
s = sock or connect()
|
||||
try:
|
||||
s.sendall(b"/kernel\n")
|
||||
buf = b""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
chunk = s.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
for line in buf.split(b"\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line.decode("utf-8", "replace"))
|
||||
except Exception:
|
||||
continue
|
||||
if obj.get("type") == "response" and obj.get("content", "").lstrip().startswith("{"):
|
||||
return json.loads(obj["content"])
|
||||
raise RuntimeError("no /kernel response")
|
||||
finally:
|
||||
if sock is None:
|
||||
s.close()
|
||||
|
||||
def blast(n_queued, n_interrupt, tag):
|
||||
"""一条连接狂发:排队输入与中断交错。响应在后台线程里丢弃,避免缓冲阻塞。"""
|
||||
s = connect()
|
||||
threading.Thread(target=lambda: drain(s), daemon=True).start()
|
||||
sent = 0
|
||||
for i in range(max(n_queued, n_interrupt)):
|
||||
if i < n_queued:
|
||||
s.sendall(("queued-%s-%d\n" % (tag, i)).encode())
|
||||
sent += 1
|
||||
if i < n_interrupt:
|
||||
s.sendall(("/interrupt intr-%s-%d\n" % (tag, i)).encode())
|
||||
sent += 1
|
||||
return sent
|
||||
|
||||
def drain(s):
|
||||
try:
|
||||
while s.recv(65536):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[2] if len(sys.argv) > 2 else "stats"
|
||||
if cmd == "stats":
|
||||
st = kernel_stats()
|
||||
if len(sys.argv) > 3 and sys.argv[3] == "full":
|
||||
print(json.dumps(st, ensure_ascii=False))
|
||||
else:
|
||||
sched = st.get("scheduler") or st
|
||||
print(json.dumps(sched, ensure_ascii=False))
|
||||
elif cmd == "blast":
|
||||
n = blast(int(sys.argv[3]), int(sys.argv[4]), sys.argv[5] if len(sys.argv) > 5 else "b")
|
||||
print("sent=%d" % n)
|
||||
Reference in New Issue
Block a user