Files
HomeAgent/scripts/kernel-stress/cmp.py
JianFeeeee 20e5d917c3 chore(stress): 加 waiter 远程更新脚本;修 cmp.py 的 docstring 格式
## deploy-waiter.sh:106 / 30 的 waiter 更新

现状(更新前):两台都是 8月27日构建的 /opt/waiter/waiter(11,388,177 字节),
以 root 跑 waiter-remote.service,配置指向 ws://192.168.2.60:9890/…

安全设计:
- 先备份旧二进制(`$BIN.bak-<时间戳>`),失败即回滚(脚本内自动)
- **只换二进制,不动 waiter.yaml**(配置由 deploy 后单独追加)
- **逐台更新并验证,不并行** —— 两台都连同一网关,同时重启会同时断链
- 106 走 admin+sudo、30 走 root
- 验证项:服务 active + 进程时长 + **配置 md5 未变**

用法:`check`(只读)/ `deploy <ip>`(需输 yes)/ `rollback <ip>`

## cmp.py:两处格式

docstring 的 `"""` 紧贴内容、以及函数段之间缺两个空行(PEP8)。纯格式,
无逻辑改动。
2026-09-27 19:42:06 +08:00

274 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""更新前后的**全面**性能对比与通过率。
## 为什么必须有「通过率」而不只是耗时
工具调用这一路的失败模式几乎都是**静默**的:工具没跑、参数混拼、只处理了
第一个 tool_call —— 都不报错,只是结果不对。所以"跑完没崩"完全不能说明它
work。本次每次测量都同时记录**处理数**(响应里有多少个工具结果标记)与
**是否出现错误帧**,任一不符即记为失败。
## 三块覆盖
1. **调度器**:多连接并发排队 + L4 中断(沿用 stress.py 的形态)
2. **批内工具调用**:!batchN / !slowbatchN / !serialbatchN
—— 并发批 vs 强制串行批 vs 单工具基线
3. **稳定性**:连续多轮的通过率 + 队列/背压计数
## 每轮两个必查项(任一不过即判失败)
- **工具真跑了**:响应里能看到 `done-N` 标记(cmd_run 的 echo 输出),
工具没跑就不可能有
- **无 error 帧**:`{"type":"error"}` 一律算失败
## 用法
python3 cmp.py <旧sock> <旧key> <新sock> <新key> [规模]
"""
import contextlib
import json
import re
import socket
import statistics
import sys
import threading
import time
# ---------------------------------------------------------------- 基础连接
class Conn:
"""一条 cli 连接。
★ auth 帧本身就是 {"type":"response"},必须先吃掉它再开始收集 ——
否则第一轮的"终止帧"就是 auth,测出来的耗时是 0。
"""
def __init__(self, sock, key, timeout=120):
self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.s.settimeout(timeout)
self.s.connect(sock)
self.s.sendall(("/auth %s\n" % key).encode())
self.f = self.s.makefile("rb")
auth = self.f.readline()
if b"authenticated" not in auth:
raise RuntimeError("auth 失败: %s" % auth[:80])
def ask(self, text, timeout=120):
"""发一条输入,等终止帧。返回 (耗时, 帧列表)。"""
t0 = time.time()
self.s.sendall((text + "\n").encode())
frames = []
while time.time() - t0 < timeout:
line = self.f.readline()
if not line:
break
t = line.decode("utf-8", "replace").strip()
frames.append(t)
if t.startswith("{"):
try:
if json.loads(t).get("type") in ("response", "error"):
break
except ValueError:
pass
return time.time() - t0, frames
def close(self):
with contextlib.suppress(OSError):
self.s.close()
def has_error(frames):
for f in frames:
if f.startswith("{"):
try:
if json.loads(f).get("type") == "error":
return True
except ValueError:
pass
return False
def tools_done(frames):
""" 数出工具真正执行的个数(按 done-N 标记去重)。"""
blob = " ".join(frames)
return len({int(m.group(1)) for m in re.finditer(r"done-(\d+)", blob)})
# ---------------------------------------------------------------- 单轮测量
def measure(sock, key, marker, rounds):
"""连一次、跑 rounds 轮(每轮唯一输入)。
★ 每轮输入必须唯一:内核 task.go:353 有输入去重
(isDuplicateInput,为 webui 断线重连重放而设),相同文本会被丢弃
并回空响应。第一版每轮同一个 marker,只有第 1 轮有效。
"""
dts, oks = [], 0
try:
c = Conn(sock, key)
except RuntimeError as e:
print(" 连接失败: %s" % e)
return None
try:
for _ in range(rounds):
dt, frames = c.ask("%s-%d" % (marker, time.time_ns()))
dts.append(dt)
# 无 error 帧即算通过(工具数由调用方按 marker 形态另行核对)
if not has_error(frames):
oks += 1
if has_error(frames):
break
finally:
c.close()
return {"rounds": rounds, "ok": oks, "median": statistics.median(dts),
"mean": statistics.mean(dts), "min": min(dts), "max": max(dts),
"all": dts}
# ---------------------------------------------------------------- 并发轰炸
def blast(sock, key, conns, inputs, tag):
"""conns 条连接并发,每条连接连发 inputs 条输入。返回通过率。
★ 线程数与计数:第一版按 `conns * inputs` 起线程、每个线程又跑
`inputs` 轮,于是总输入数是 conns×inputs²,分子分母量纲不一致,
算出过 "128/32 = 400%" 这种荒谬数字。
现在:**恰好 conns 个 worker,每个跑 inputs 轮** ⇒ 总输入 conns×inputs。
"""
results = [None] * conns
def worker(idx):
try:
c = Conn(sock, key)
except RuntimeError:
results[idx] = {"ok": 0, "sent": 0, "err": "connect"}
return
good = 0
try:
for j in range(inputs):
_, frames = c.ask(f"{tag}-{idx}-{j}-{time.time_ns()}")
if not has_error(frames):
good += 1
results[idx] = {"ok": good, "sent": inputs, "err": ""}
except Exception as e: # noqa: BLE001
results[idx] = {"ok": good, "sent": inputs, "err": repr(e)[:60]}
finally:
c.close()
t0 = time.time()
ths = [threading.Thread(target=worker, args=(i,), daemon=True)
for i in range(conns)]
for t in ths:
t.start()
for t in ths:
t.join()
wall = time.time() - t0
# 分母用"实际发出的输入数"(含连接失败的那些),而不是标称的 conns×inputs
# —— 连接失败时分子分母必须同步缩放,否则通过率会虚高。
total = sum(r["sent"] for r in results if r)
good = sum(r["ok"] for r in results if r)
return {"total": total, "good": good, "wall": wall,
"rate": good / total if total else 0}
# ---------------------------------------------------------------- 主流程
def arg_int(pos, default, name):
"""解析位置参数为正整数;非法时给出可执行报错而不是裸 ValueError。"""
if pos >= len(sys.argv):
return default
raw = sys.argv[pos]
try:
v = int(raw)
except ValueError:
sys.exit(f"参数 {name} 需要一个正整数,收到 {raw!r}(用法见本文件顶部)")
if v <= 0:
sys.exit(f"参数 {name} 必须 > 0,收到 {v}")
return v
def tools_for(sock, key, n):
"""单跑一轮,数出真正执行的工具个数(连不上返回 0)。"""
try:
c = Conn(sock, key)
except RuntimeError:
return 0
try:
_, frames = c.ask("!slowbatch%d-%d" % (n, time.time_ns()))
return tools_done(frames)
except Exception: # noqa: BLE001
return 0
finally:
c.close()
def main():
if len(sys.argv) < 5:
print(__doc__)
sys.exit(2)
old_sock, old_key, new_sock, new_key = sys.argv[1:5]
scale = arg_int(5, 1, "scale")
print("=" * 72)
print("更新前后全面对比(旧: 85e3d66 / 新: d3eaff4),scale=%d" % scale)
print("=" * 72)
# ---- ① 批内工具调用 ----
print("\n【① 批内工具调用】")
print("%-26s %-16s %-16s %s" % ("场景", "旧 中位/工具", "新 中位/工具", "变化"))
for n in (2, 4, 8):
row = {}
for tag, sock, key in (("old", old_sock, old_key), ("new", new_sock, new_key)):
m = measure(sock, key, "!slowbatch%d" % n, 3)
if m is None:
row[tag] = (0.0, 0)
else:
row[tag] = (m["median"], tools_for(sock, key, n))
o, w = row["old"], row["new"]
chg = "—" if o[0] == 0 else "%+.1f%%" % ((w[0] - o[0]) / o[0] * 100)
print("%-26s %-16s %-16s %s"
% ("!slowbatch%d 并发批" % n,
"%.3fs / %d 个" % o, "%.3fs / %d 个" % w, chg))
# ---- ② 并发 vs 强制串行(新版内部对照)----
print("\n【② 并发 vs 强制串行(新版内部对照)】")
print("%-6s %-14s %-14s %-10s %s" % ("N", "并发", "强制串行", "加速", "工具数"))
for n in (2, 4, 8):
cp = measure(new_sock, new_key, "!slowbatch%d" % n, 3)
cs = measure(new_sock, new_key, "!serialbatch%d" % n, 3)
if cp is None or cs is None:
print("%-6d 测量失败(连接不上)" % n)
continue
k = tools_for(new_sock, new_key, n)
sp = cs["median"] / cp["median"] if cp["median"] else 0
print("%-6d %-14s %-14s %-10.2f %d"
% (n, "%.3fs" % cp["median"], "%.3fs" % cs["median"], sp, k))
# ---- ③ 调度器并发轰炸 + 通过率 ----
print("\n【③ 调度器并发轰炸】")
conns, inputs = 8 * scale, 4 * scale
for tag, sock, key in (("旧", old_sock, old_key), ("新", new_sock, new_key)):
r = blast(sock, key, conns, inputs, "!batch2")
print(" %s版: %d/%d 通过(%.1f%%),墙钟 %.1fs"
% (tag, r["good"], r["total"], r["rate"] * 100, r["wall"]))
# ---- ④ 连续稳定性 ----
print("\n【④ 连续稳定性(20 轮)】")
for tag, sock, key in (("旧", old_sock, old_key), ("新", new_sock, new_key)):
m = measure(sock, key, "!batch2", 20)
if m is None:
print(" %s版: 连接失败" % tag)
continue
print(" %s版: %d/%d 无错误,中位 %.3fs,min %.3f / max %.3f"
% (tag, m["ok"], m["rounds"], m["median"], m["min"], m["max"]))
print()
if __name__ == "__main__":
main()