OpenMed 服务负载测试与延迟 SLO 门禁实战指南
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
导读
本文以 OpenMed 仓库中 docs/serving/load-testing.md 为骨架,完整讲解如何使用仓库内置的 k6 负载测试工具链对本地 OpenMed 服务容器进行压测,并通过 p95/p99 延迟、错误率、吞吐量四类 SLO 门禁判断服务是否达标。读完本文,你将掌握:一键运行混合流量压测(/analyze、/pii/deidentify、/pii/extract/stream三路按 4:4:2 加权)、按设备分级调整门禁阈值、解读 JSON 报告与失败归因,以及如何在 CI 定时任务中复用同一套门禁。
一、压测工具链概览:k6 场景 + Docker 包装器
OpenMed 的负载测试不是临时脚本,而是一套“合成流量 + 本地回环 + SLO 门禁”的完整工具链,代码全部收录在仓库中:
- 场景脚本:deploy/loadtest/scenario.js —— 基于 Grafana k6 编写,负责生成确定性混合流量、采集指标、判定阈值并产出 JSON 报告;
- 包装脚本:deploy/loadtest/run.sh —— 负责构建镜像、拉起临时容器、等待就绪、预热路由、调用 k6、退出时清理容器;
- 服务镜像:deploy/docker/Dockerfile —— 基于
python:3.11-slim,安装 CPU 版 PyTorch 与openmed[hf,service]依赖,以uvicorn openmed.service.app:app启动; - 夜间 CI 工作流:.github/workflows/loadtest.yml —— 每天 UTC 02:17 定时运行,也支持
workflow_dispatch手动触发调参。
设计红线(务必遵守):压测夹具只使用合成文本,绝不读取生产流量,也不接受远程目标。包装脚本会把临时容器绑定到回环地址,并且一旦发现LOADTEST_BASE_URL不是回环地址就立即拒绝执行(见 deploy/loadtest/run.sh)。不要用患者真实文本、凭据或生产 URL 替换合成夹具。
合成负载内容与三个被测端点
场景脚本内置一段固定合成病历文本SYNTHETIC_NOTE(见 deploy/loadtest/scenario.js),保证每次运行负载内容完全一致、可复现:
"Taylor Reed, born 1981-02-03, visited Example Clinic for a routine follow-up. Call the fictional records desk at 555-0100."
流量按固定权重分发到三个端点(确定性混比见 deploy/loadtest/scenario.js,权重声明见 deploy/loadtest/scenario.js):
| 端点 | 权重 | 请求体要点 |
|---|---|---|
POST /analyze | 40% | model_name=disease_detection_superclinical,confidence_threshold=0 |
POST /pii/deidentify | 40% | method=mask,model_name=OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1,confidence_threshold=0 |
POST /pii/extract/stream | 20% | 流式参数:chunk_size=128、window_chars=256、tokenizer_context_chars=64、max_entity_chars=128、include_text=false |
流量分配采用(__VU + __ITER) % 10的取模方式,因此负载在保持并发混合的同时是可复现的。三个端点的请求模式(Pydantic 校验、批处理、流式分块)分别定义在 openmed/service/schemas.py 与路由实现 openmed/service/app.py 中;其中流式端点以application/x-ndjson返回逐事件 JSON,include_text=false可显著降低响应体大小,从而把压测重心放在模型推理耗时本身。
服务端前置条件:模型预加载与 keep-alive
为了让压测结果稳定,run.sh在启动容器时通过环境变量预加载两个模型(见 deploy/loadtest/run.sh):
OPENMED_SERVICE_PRELOAD_MODELS=disease_detection_superclinical,OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1 OPENMED_SERVICE_KEEP_ALIVE=10m对应地,服务端 openmed/service/runtime.py 的parse_preload_models()会把逗号分隔的模型名解析、去重、校验后交给ServiceRuntime.preload(),在启动阶段就加载进热池(warm pool,见 openmed/service/warm_pool.py),避免压测期间出现“首次加载模型”的冷启动毛刺。/readyz只有在预加载完成后才返回 200,否则返回 503(见 openmed/service/app.py),这是压测包装器等待就绪的判断依据。
模型下载只在镜像本地缓存缺失时由服务执行;若镜像已内置或预加载过模型,压测不会触发网络下载。
二、本地一键运行压测
前置依赖
- Docker(
docker命令可用) curl- k6(
k6命令可用)
默认运行
在仓库根目录直接执行:
deploy/loadtest/run.sh包装脚本的完整流程(对应 deploy/loadtest/run.sh):
- 用
deploy/docker/Dockerfile构建镜像,标签默认openmed:loadtest; - 以
--detach --rm方式启动临时容器,把宿主机的127.0.0.1:18080映射到容器内8080; - 轮询
http://127.0.0.1:18080/readyz,最长等待 900 秒(LOADTEST_STARTUP_TIMEOUT_SECONDS),期间每 2 秒探测一次; - 预热三个路由(
/analyze、/pii/deidentify、/pii/extract/stream各发一次合成请求),消除冷启动影响; - 运行 k6 场景并强制执行 SLO 阈值;
- 退出时(含出错)强制删除临时容器。
由于容器使用--rm且脚本注册了trap cleanup EXIT,本地运行结束后不会残留容器,报告默认写入系统临时目录,因此本地压测不会在仓库里产生任何文件。
常用变体
跳过构建与预热(使用预构建镜像):
LOADTEST_SERVICE_IMAGE=openmed:local \ LOADTEST_SKIP_BUILD=1 \ LOADTEST_WARMUP=0 \ deploy/loadtest/run.shLOADTEST_SKIP_BUILD=1:不重新构建镜像,直接用LOADTEST_SERVICE_IMAGE指定的镜像;LOADTEST_WARMUP=0:跳过三路预热请求,适合已处于稳定状态的常驻服务。
直接压测一个已在回环地址运行的服务(绕过包装器,自行调用 k6):
BASE_URL=http://127.0.0.1:8080 \ LOADTEST_RESULT_FILE=/tmp/openmed-loadtest-summary.json \ k6 run deploy/loadtest/scenario.js注意:场景脚本同时接受LOADTEST_BASE_URL与短别名BASE_URL(见 deploy/loadtest/scenario.js),目标地址必须以http://开头且为回环地址。直接运行 k6 时报告路径完全由LOADTEST_RESULT_FILE决定。
其他可用包装参数
在 deploy/loadtest/run.sh 中还可调整:
| 变量 | 默认值 | 说明 |
|---|---|---|
DOCKER_BIN | docker | Docker 可执行文件 |
K6_BIN | k6 | k6 可执行文件 |
LOADTEST_SERVICE_PORT | 18080 | 宿主机映射端口(须为 1024–65535 的非特权端口) |
LOADTEST_CONTAINER_NAME | 随机 | 临时容器名 |
LOADTEST_STARTUP_TIMEOUT_SECONDS | 900 | 就绪等待上限(须为正整数) |
OPENMED_PROFILE | prod | 服务 profile,透传给OPENMED_PROFILE |
OPENMED_OFFLINE | 未设置 | 若已设置,则透传给容器开启离线模式 |
三、SLO 门禁:阈值如何配置、如何判定
k6 通过thresholds强制执行门禁(见 deploy/loadtest/scenario.js),任何一条阈值被击穿,k6 进程即以非零状态退出;同时场景脚本的handleSummary()会把“是否通过”显式写入控制台与 JSON 报告(deploy/loadtest/scenario.js)。
环境变量与默认值
| 变量 | 默认值 | 含义 |
|---|---|---|
LOADTEST_DURATION_SECONDS | 30 | 测试时长(秒) |
LOADTEST_RATE | 2 | 目标每秒请求数(到达率) |
LOADTEST_CONCURRENCY | 4 | 预分配虚拟用户数 |
LOADTEST_MAX_VUS | 2 × concurrency | 虚拟用户上限 |
LOADTEST_SLO_P95_MS | 30000 | p95 延迟严格上限(毫秒) |
LOADTEST_SLO_P99_MS | 60000 | p99 延迟严格上限(毫秒) |
LOADTEST_SLO_ERROR_RATE | 0.05 | 失败请求占比严格上限(5%) |
LOADTEST_SLO_MIN_THROUGHPUT_RPS | 0.5 | 最低达成吞吐量(req/s) |
场景脚本还接受短别名SLO_P95_MS、SLO_P99_MS、SLO_ERROR_RATE、SLO_MIN_THROUGHPUT_RPS(优先读取长名,见 deploy/loadtest/scenario.js 的envValue机制)。
阈值判定逻辑(deploy/loadtest/scenario.js):
const passed = p95 < SLO_P95_MS && p99 < SLO_P99_MS && errorRate < SLO_ERROR_RATE && throughput >= SLO_MIN_THROUGHPUT_RPS;注意差异:延迟与错误率是严格小于上限,吞吐量是大于等于下限。场景同时保留了 k6 内置指标(http_req_duration、http_req_failed、http_reqs)与自定义指标(loadtest_latency_ms、loadtest_errors、loadtest_requests)两套门禁,即使日后修改工作负载,门禁依然可用。
按设备分级调参示例
LOADTEST_SLO_P95_MS=10000 \ LOADTEST_SLO_P99_MS=20000 \ LOADTEST_SLO_ERROR_RATE=0.01 \ LOADTEST_SLO_MIN_THROUGHPUT_RPS=1 \ LOADTEST_RESULT_FILE=/tmp/openmed-laptop-slo.json \ deploy/loadtest/run.sh建议的起步阈值(按设备档位)
文档给出的以下起步值用于帮助按设备档位调参,不是性能承诺;调门禁前应使用稳定且已预热(warmed-up)的服务,并重复运行同一档位配置取稳定结果:
| 设备档位 | p95 | p99 | 错误率 | 最低吞吐 |
|---|---|---|---|---|
| Nano / 受限 CPU | 20,000 ms | 30,000 ms | 5% | 0.1 req/s |
| 手机 / 小型笔记本 | 10,000 ms | 15,000 ms | 2% | 0.25 req/s |
| 笔记本 | 5,000 ms | 10,000 ms | 1% | 0.5 req/s |
| 服务器 | 3,000 ms | 6,000 ms | 1% | 1 req/s |
之所以默认值(30s / 30s p95)明显宽于各档位,是因为仓库默认门禁面向通用 CI 环境;而 OpenMed 本身主打“本地优先、端侧运行”的医疗 AI(临床 NER 与 PII 脱敏),因此按手机、笔记本、服务器等不同算力档位收窄阈值才更有意义。
四、报告产物与 JSON 结构
k6 运行结束时的handleSummary()会在控制台输出一行摘要,同时把结构化报告写入LOADTEST_RESULT_FILE指定的文件。报告为 JSON,核心字段如下(deploy/loadtest/scenario.js):
{ "schema_version": 1, "workload": { "name": "synthetic-mixed-service-traffic", "endpoints": ["/analyze", "/pii/deidentify", "/pii/extract/stream"], "weights": { "analyze": 0.4, "deidentify": 0.4, "stream": 0.2 } }, "requests": 60, "throughput_rps": 2.0, "p95_ms": 1234, "p99_ms": 2345, "error_rate": 0.0, "slo": { "p95_ms": 30000, "p99_ms": 60000, "max_error_rate": 0.05, "min_throughput_rps": 0.5 }, "passed": true }控制台摘要同时输出请求数、吞吐量、p95/p99、错误率与SLO status: PASS / FAIL; k6 will exit non-zero(deploy/loadtest/scenario.js)。
指标以 k6 tagendpoint区分三个端点(deploy/loadtest/scenario.js),因此如需按端点细看延迟分布,可直接在 k6 汇总输出或结合k6的 JSON 扩展查看。
五、失败归因与常见排查
门禁度量的是服务行为,而不是模型质量。一次 FAIL 意味着至少发生以下一种情况(见 docs/serving/load-testing.md):
- p95 或 p99 请求延迟超过配置上限;
- 存在非 2xx 响应;
- 达成吞吐量低于配置下限;
- 服务未就绪(
/readyz未通过)或某个预热路由失败。
排查路径:
- 打开归档的 JSON 报告:对比
p95_ms/p99_ms/error_rate/throughput_rps与slo块中的上限/下限,确定是哪一项击穿; - 查看容器启动日志:
run.sh在等待就绪超时时会自动打印容器最近 80 行日志(deploy/loadtest/run.sh);本地手动排查也可用docker logs <容器名>; - 检查模型是否真正预热:若阈值收紧后吞吐骤降,多半是模型冷加载或 keep-alive 过期后反复重载,可增大
OPENMED_SERVICE_KEEP_ALIVE或确认OPENMED_SERVICE_PRELOAD_MODELS覆盖了场景中用到的模型; - 保持单容器、单节点前提:该压测工作负载按设计是单容器单节点,它不是生产容量测试,也不能替代单元并发测试工具(unit concurrency harness)。要在真实集群容量前,请使用 deploy/k8s/hpa.yaml、docs/deploy/autoscaling.md 与 docs/serving/backpressure.md 等面向生产规模的资料另行设计。
六、夜间 CI 门禁:与 PR CI 隔离的独立工作流
夜间工作流有意与 PR 级 CI 分离(.github/workflows/loadtest.yml):
- 定时触发:每天
17 2 * * *(UTC 02:17); - 手动触发:
workflow_dispatch,可在 GitHub 界面用Run workflow显式调参(duration、rate、concurrency、四个 SLO 输入),调参后立即跑一次验证; - 定时运行的调度触发则使用默认参数(与本地默认一致)。
工作流内容与本地包装器完全一致:检出代码 →grafana/setup-k6-action安装 k6 → 构建镜像并以openmed:loadtest-<sha>为标签 → 运行deploy/loadtest/run.sh→无论 SLO 是否通过都上传loadtest-results/目录,产物名为loadtest-slo-<run-id>,保留 30 天。即使门禁失败,报告也会被归档,便于事后归因。
提示:用Run workflow调参时,只有显式输入的值会被使用,未输入的字段回落为默认值(工作流内用
inputs.xxx || '30'形式处理)。
七、从压测到调参:一份可落地的操作清单
- 在目标机器(本机或 CI Runner)准备好 Docker、curl、k6;
- 首次运行直接
deploy/loadtest/run.sh,用默认门禁验证工具链闭环; - 依据设备档位表选择一组更严格的阈值,通过
LOADTEST_SLO_*或短别名传入; - 用
LOADTEST_RESULT_FILE把报告落到固定路径,重复运行同一档位至少两轮,取稳定值; - 只有拿到稳定、已预热的数据后,再决定是否调整门禁阈值或 keep-alive/预加载配置;
- 将最终档位固化到 .github/workflows/loadtest.yml 的
workflow_dispatch输入或默认值中,纳入夜间门禁持续回归。
通过这套工具链,你可以把“服务在合成混合流量下是否满足延迟与可用性承诺”变成一个可复现、可归档、可进 CI 的硬性门禁,并为手机、笔记本、服务器等不同算力档位分别沉淀各自的 SLO 基线。
相关资源
- 压测场景与指标定义:deploy/loadtest/scenario.js
- 容器包装与参数解析:deploy/loadtest/run.sh
- 服务镜像构建:deploy/docker/Dockerfile
- 夜间 SLO 工作流:.github/workflows/loadtest.yml
- 被测端点实现:openmed/service/app.py
- 请求体 Schema 与校验:openmed/service/schemas.py
- 预加载与 keep-alive 解析:openmed/service/runtime.py
- 服务端压测配套指南:docs/serving/backpressure.md、docs/deploy/autoscaling.md
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考