作业12:推理延迟压测与健康检查(curl → 并发 → 告警脚本)
作业目标:学会用
curl/脚本打/v1/chat/completions记录延迟;故意抬高并发观察延迟与错误;用 Cursor 写出「失败重试 3 次 + 告警文案」的健康检查脚本。验收标准:
1. 至少 3 次单请求延迟 CSV(含
http_code、time_total_s)2. 一组高并发结果 + 自己填写的 5 行「现象→原因→处理」
3. 健康检查脚本:成功打印
[PASS];--fail-demo时重试 3 次并写出告警文案到alerts.jsonl环境:
192.168.110.207LiteLLM:4000(推荐)或 Higress:18080/llm;上游 vLLM116.62.10.183:8000;模型qwen3.6-27bHTML:
docs/html_cursor/作业12_推理延迟压测与健康检查_cursor.html
运行环境:文档与脚本在 Windows 仓库维护;压测与验收建议在 207(或本机对 207 网络可达)。Python:
/usr/bin/python3(207)或本机python。路径:
/opt/workbuddy/scripts_cursor/作业12_推理延迟压测与健康检查/结果目录:
/opt/workbuddy/data/homework12/(Windows 默认同仓库data/homework12/)
目录
做完长什么样
| 项 | 本环境值 |
|---|---|
| 推荐入口 | http://192.168.110.207:4000/v1/chat/completions |
| 备用入口 | http://192.168.110.207:18080/llm/v1/chat/completions |
| Bearer | sk-litellm-bocheng-207 |
| model | qwen3.6-27b |
| 单次延迟参考(短 prompt) | 约 0.5~3s(视排队/冷启动) |
| 结果目录 | data/homework12/ |
| 健康检查成功 | [PASS] 推理接口健康 |
| 健康检查失败演示 | --fail-demo → 告警文案 + alerts.jsonl |
目录里应出现类似:
latency_single_YYYYMMDD_HHMMSS.csv
latency_conc_YYYYMMDD_HHMMSS.csv
health_YYYYMMDD_HHMMSS.json
alerts.jsonl # 仅失败或 --fail-demo 时学习路线(照着做)
步骤 A 手工 curl 一次,看懂 time_total
│
步骤 B 跑 hw12_01 单次延迟 ×3,保存 CSV
│
步骤 C 跑 hw12_02 并发(先 8×24,再可选 20×40)
│
步骤 D 根据汇总填写「现象→原因→处理」五联表
│
步骤 E 跑 hw12_03 健康检查(正常 + --fail-demo)
│
步骤 F 对照验收清单勾选Windows 本机可直接跑 Python 版(推荐学习路径);207 可跑 bash 版(与生产运维习惯一致)。两套逻辑等价。
第一部分 原理
1.1 延迟里有什么
客户端 curl
→ LiteLLM :4000(鉴权、选模型、转发)
→ vLLM :8000(排队 + 预填充 + 解码)
→ 返回首包/整包curl -w %{time_total} ≈ 端到端墙钟时间(含网络、网关、排队、生成)。 短 max_tokens 时,延迟里排队+预填充占比更大;长生成则解码时间变长。
1.2 为什么要故意打高并发
单机 vLLM 吞吐有限。并发升高后常见:
| 现象 | 常见原因 |
|---|---|
time_total 变大 | 请求在 vLLM 队列等待 |
| 偶发超时 | 客户端 --max-time / 网关超时过短 |
| HTTP 429 | 网关/限流插件(若已启用) |
| HTTP 500/502 | 上游 OOM、进程重启、代理错误 |
| 连接失败 | LiteLLM/vLLM 挂了或网络不通 |
1.3 健康检查为何要重试 3 次
瞬时抖动(GC、短暂排队、网络毛刺)不应立刻告警。 3 次失败再告警是运维最小可用策略;告警文案要含:时间、目标、错误、建议动作。
第二部分 前置条件
- LiteLLM 可访问:
curl -sS http://192.168.110.207:4000/v1/models \
-H "Authorization: Bearer sk-litellm-bocheng-207" | head -c 200- 仓库脚本已在本机;若要在 207 跑 bash,把目录同步到
/opt/workbuddy/(与其它作业相同方式)。 - 压测会占用 GPU:教学用默认
max_tokens=32、并发 8;不要在业务高峰对生产用户无感实验。 - 鉴权 Key(完整值):
sk-litellm-bocheng-207。
第三部分 详细操作步骤
步骤 A:手工 curl(建立手感)
在 207 bash 或安装了 curl 的环境:
curl -sS -o /tmp/hw12_one.json -w "http_code=%{http_code} time_total=%{time_total}\n" \
http://192.168.110.207:4000/v1/chat/completions \
-H "Authorization: Bearer sk-litellm-bocheng-207" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.6-27b","messages":[{"role":"user","content":"只回复:pong"}],"max_tokens":8,"stream":false}'
# 看返回
head -c 300 /tmp/hw12_one.json; echo经 Higress:
curl -sS -o /tmp/hw12_one.json -w "http_code=%{http_code} time_total=%{time_total}\n" \
http://192.168.110.207:18080/llm/v1/chat/completions \
-H "Authorization: Bearer sk-litellm-bocheng-207" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.6-27b","messages":[{"role":"user","content":"只回复:pong"}],"max_tokens":8,"stream":false}'记录:把打印的 time_total 抄到笔记本。期望 http_code=200。
Windows PowerShell 直接
-d '{...}'容易把 JSON 弄坏;请用步骤 B 的 Python 脚本,或把 JSON 写入文件再用curl --data-binary @file.json。
步骤 B:单次延迟脚本(×3)
Windows / 通用(推荐):
cd D:\workbuddy\192.168.110.207
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.py --rounds 3207 bash:
sed -i 's/\r$//' /opt/workbuddy/scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.sh
chmod +x /opt/workbuddy/scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.sh
ROUNDS=3 bash /opt/workbuddy/scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.sh打开生成的 latency_single_*.csv,确认有 3 行成功数据。
步骤 C:故意抬高并发
先温和:
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.py --concurrency 8 --n 24再加压(学习用,可能变慢或出错):
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.py --concurrency 20 --n 40207 bash 等价:
CONCURRENCY=8 N=24 bash /opt/workbuddy/scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.sh
CONCURRENCY=20 N=40 bash /opt/workbuddy/scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.sh脚本结束会打印 HTTP 码分布 与 min/p50/p95/max。把这两行截图或抄下,用于填五联表。
步骤 D:填写五联表
见 第四部分。 必须自己按本次跑出来的数字改写;文中「示例答法」仅供对照,照抄不算过关。
步骤 E:健康检查脚本(Cursor 交付物)
正常探测:
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_03_健康检查.py故意失败,看重试 3 次 + 告警文案:
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_03_健康检查.py --fail-demo期望:
- 正常:
[PASS] 推理接口健康,写出health_*.json - 演示失败:三次
[FAIL],打印【推理健康检查告警】,追加alerts.jsonl
自学要点(你用 Cursor 时应要求 AI 做到):
- HTTP POST
/v1/chat/completions - 失败则 sleep 后重试,最多 3 次
- 仍失败则生成可读告警文案并落盘
- 进程退出码:成功 0,失败非 0(本脚本用
2)
完整源码见第五部分(可直接复制;学习时建议先自己让 Cursor 写一版再对比)。
步骤 F:验收
勾选第七部分清单。
第四部分 五联表:现象→原因→处理
格式固定为 5 行。每行:
现象 → 原因 → 处理。
4.1 空白表(交作业用)
1) 现象:________ → 原因:________ → 处理:________
2) 现象:________ → 原因:________ → 处理:________
3) 现象:________ → 原因:________ → 处理:________
4) 现象:________ → 原因:________ → 处理:________
5) 现象:________ → 原因:________ → 处理:________4.2 示例答法(对照用,勿原样提交)
以下按「并发升高后常见情况」撰写;数字请换成你 CSV 里的真实值。
1) 现象:并发 8 时 p95≈数秒且仍 200 → 原因:vLLM 排队等待变长但未打满超时 → 处理:业务侧限流或扩 GPU 副本,监控 waiting 队列
2) 现象:并发 20 时部分请求 time_total 接近客户端超时 → 原因:排队+解码超过 curl/脚本 max-time → 处理:加大超时或降并发/降 max_tokens
3) 现象:偶发 HTTP 0 / Connection refused → 原因:LiteLLM 进程重启或瞬间不可达 → 处理:systemd 保活 + 健康检查重试(本作业脚本)
4) 现象:HTTP 401/403 → 原因:Bearer 错误或网关鉴权未透传 → 处理:核对 sk-litellm-bocheng-207;经 Higress 时确认未误开 key-auth
5) 现象:HTTP 429 或主动限流文案 → 原因:Higress/LiteLLM 限流策略生效 → 处理:申请提额、分散 Key、错峰;压测环境可临时关闭演示限流4.3 如何从 CSV 提炼「现象」
打开 latency_conc_*.csv:
- 看
http_code是否全是 200 - 用脚本打印的 p50/p95 对比步骤 B 的单次 avg
- 把「从 X 秒涨到 Y 秒」「出现 N 个非 200」写成现象句
第五部分 完整代码程序
仓库路径(勿改名):
| 文件 | 作用 |
|---|---|
scripts_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.py | 单次延迟 |
scripts_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.py | 并发压测 |
scripts_cursor/作业12_推理延迟压测与健康检查/hw12_03_健康检查.py | 重试+告警 |
scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_01_单次延迟.sh | bash 单次 |
scripts_bash_cursor/作业12_推理延迟压测与健康检查/hw12_02_并发压测.sh | bash 并发 |
5.1 hw12_03_健康检查.py(完整)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业12 · 步骤3:推理健康检查(失败重试 3 次 + 告警文案)
用法(207):
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业12_推理延迟压测与健康检查/hw12_03_健康检查.py
/usr/bin/python3 .../hw12_03_健康检查.py --via-higress
/usr/bin/python3 .../hw12_03_健康检查.py --fail-demo # 故意指向坏地址,演示告警
Windows 也可直接对本机可达的 207 执行:
python scripts_cursor/作业12_推理延迟压测与健康检查/hw12_03_健康检查.py
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
DEFAULT_LITELLM = "http://192.168.110.207:4000/v1/chat/completions"
DEFAULT_HIGRESS = "http://192.168.110.207:18080/llm/v1/chat/completions"
DEFAULT_KEY = "sk-litellm-bocheng-207"
DEFAULT_MODEL = "qwen3.6-27b"
MAX_RETRIES = 3
RETRY_SLEEP_S = 2.0
TIMEOUT_S = 60.0
def _out_dir() -> Path:
env = os.environ.get("HW12_OUT")
if env:
p = Path(env)
p.mkdir(parents=True, exist_ok=True)
return p
# 优先仓库 data/;仅当 Linux 上真实存在 /opt/workbuddy 时才用服务器路径
repo = Path(__file__).resolve().parents[2] / "data" / "homework12"
candidates: list[Path] = [repo]
opt = Path("/opt/workbuddy/data/homework12")
if os.name != "nt" and Path("/opt/workbuddy").is_dir():
candidates.insert(0, opt)
for cand in candidates:
try:
cand.mkdir(parents=True, exist_ok=True)
return cand
except OSError:
continue
p = Path.cwd() / "data" / "homework12"
p.mkdir(parents=True, exist_ok=True)
return p
def build_alert(
*,
url: str,
attempts: int,
last_error: str,
latencies: list[float],
) -> str:
"""告警文案:给值班同学可直接贴群的一段话。"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lat_txt = ", ".join(f"{x:.3f}s" for x in latencies) if latencies else "无"
return (
"【推理健康检查告警】\n"
f"时间:{now}\n"
f"目标:{url}\n"
f"结果:连续失败(已重试 {attempts}/{MAX_RETRIES} 次仍未成功)\n"
f"末次错误:{last_error}\n"
f"各次耗时:{lat_txt}\n"
"建议:\n"
"1) 查 LiteLLM/vLLM 进程与 GPU 显存(作业5 健康 Agent / nvidia-smi)\n"
"2) 查是否限流/鉴权失败(HTTP 401/429)\n"
"3) 高峰请降并发或扩副本;恢复后重跑本脚本确认\n"
)
def one_probe(url: str, key: str, model: str) -> tuple[bool, float, str, dict | None]:
payload = {
"model": model,
"messages": [{"role": "user", "content": "健康检查:只回复 ok"}],
"max_tokens": 8,
"stream": False,
"temperature": 0,
}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
method="POST",
)
t0 = time.perf_counter()
try:
with urllib.request.urlopen(req, timeout=TIMEOUT_S) as resp:
body = resp.read().decode("utf-8", errors="replace")
elapsed = time.perf_counter() - t0
code = getattr(resp, "status", 200)
if code != 200:
return False, elapsed, f"HTTP {code}: {body[:200]}", None
obj = json.loads(body)
content = (
((obj.get("choices") or [{}])[0].get("message") or {}).get("content")
or ""
)
if not str(content).strip():
return False, elapsed, "HTTP 200 但 content 为空", obj
return True, elapsed, content.strip()[:80], obj
except urllib.error.HTTPError as e:
elapsed = time.perf_counter() - t0
try:
detail = e.read().decode("utf-8", errors="replace")[:200]
except Exception:
detail = str(e)
return False, elapsed, f"HTTPError {e.code}: {detail}", None
except Exception as e:
elapsed = time.perf_counter() - t0
return False, elapsed, f"{type(e).__name__}: {e}", None
def run_health_check(url: str, key: str, model: str) -> int:
out = _out_dir()
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_path = out / f"health_{stamp}.json"
alert_path = out / "alerts.jsonl"
attempts_log: list[dict] = []
latencies: list[float] = []
last_error = ""
ok = False
snippet = ""
print(f"[INFO] 健康检查 URL={url}")
print(f"[INFO] 最多重试 {MAX_RETRIES} 次,间隔 {RETRY_SLEEP_S}s")
for i in range(1, MAX_RETRIES + 1):
success, elapsed, msg, raw = one_probe(url, key, model)
latencies.append(elapsed)
attempts_log.append(
{
"attempt": i,
"ok": success,
"latency_s": round(elapsed, 4),
"message": msg,
}
)
if success:
ok = True
snippet = msg
print(f"[OK ] 第 {i}/{MAX_RETRIES} 次成功 latency={elapsed:.3f}s reply={msg!r}")
break
last_error = msg
print(f"[FAIL] 第 {i}/{MAX_RETRIES} 次失败 latency={elapsed:.3f}s err={msg}")
if i < MAX_RETRIES:
time.sleep(RETRY_SLEEP_S)
report = {
"ok": ok,
"url": url,
"model": model,
"attempts": attempts_log,
"finished_at": datetime.now().isoformat(timespec="seconds"),
}
if ok:
report["status"] = "healthy"
report["reply_snippet"] = snippet
print("[PASS] 推理接口健康")
rc = 0
else:
alert = build_alert(
url=url,
attempts=MAX_RETRIES,
last_error=last_error,
latencies=latencies,
)
report["status"] = "unhealthy"
report["alert_text"] = alert
alert_path.parent.mkdir(parents=True, exist_ok=True)
with alert_path.open("a", encoding="utf-8") as f:
f.write(
json.dumps(
{
"ts": datetime.now().isoformat(timespec="seconds"),
"url": url,
"last_error": last_error,
"alert_text": alert,
},
ensure_ascii=False,
)
+ "\n"
)
print()
print(alert)
print(f"[ALERT] 已追加写入 {alert_path}")
rc = 2
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[INFO] 报告 {report_path}")
return rc
def main() -> int:
ap = argparse.ArgumentParser(description="作业12 推理健康检查")
ap.add_argument("--via-higress", action="store_true", help="经 Higress /llm 入口")
ap.add_argument("--url", default="", help="自定义 chat completions URL")
ap.add_argument("--key", default=os.environ.get("LITELLM_MASTER_KEY", DEFAULT_KEY))
ap.add_argument("--model", default=os.environ.get("LITELLM_CHAT_MODEL", DEFAULT_MODEL))
ap.add_argument(
"--fail-demo",
action="store_true",
help="故意打到不可达地址,演示重试与告警文案",
)
args = ap.parse_args()
if args.fail_demo:
url = "http://127.0.0.1:1/v1/chat/completions"
elif args.url:
url = args.url
elif args.via_higress:
url = os.environ.get("HIGRESS_CHAT_URL", DEFAULT_HIGRESS)
else:
url = os.environ.get("LITELLM_CHAT_URL", DEFAULT_LITELLM)
return run_health_check(url, args.key, args.model)
if __name__ == "__main__":
sys.exit(main())其余 hw12_01 / hw12_02 全文见仓库同名文件(步骤里已给运行命令);bash 版在 scripts_bash_cursor/作业12_推理延迟压测与健康检查/。
第六部分 常见问题
| 问题 | 处理 |
|---|---|
| PowerShell curl JSON 400 | 用 Python 脚本,或 JSON 落文件 + --data-binary @file |
| 全部很慢 | 看 183 上 vLLM 是否在跑;先把 --n 降到 4 |
| 经 Higress 失败、直连 LiteLLM 成功 | 查路由 /llm、rewrite;见 Higress 之三 |
| 健康检查误报 | 确认 Key/模型名;先手工 curl |
| 不想打扰业务 | 只跑 --rounds 1 与 --concurrency 2 --n 4 |
第七部分 验收清单
- [ ] 步骤 A:手工 curl 看到
http_code=200与time_total - [ ] 步骤 B:
latency_single_*.csv≥ 3 行成功 - [ ] 步骤 C:
latency_conc_*.csv+ 终端汇总(码分布与分位数) - [ ] 步骤 D:自己填写的 5 行「现象→原因→处理」(不是原文照抄示例)
- [ ] 步骤 E:
hw12_03正常[PASS];--fail-demo有告警文案且alerts.jsonl有记录 - [ ] (加分)对比直连
:4000与--via-higress的 p95 差异,写 2 句结论
附录 · 与其它作业关系
| 作业 | 关系 |
|---|---|
| 作业 5 | Agent 式健康检查(容器/GPU/队列);本作业是 HTTP 黑盒延迟/告警 |
| 作业 8 | SLA 大盘;本作业产出的 CSV 可作手工对照 |
| Higress 之六 | 限流/WAF 会导致本作业出现 429/403——正好写入五联表 |
维护:脚本与本文同步;改默认 Key/端口时同时改
_config.py与脚本内默认值。