OpenMed 服务负载测试与延迟 SLO 门禁实战指南
2026/9/18 1:28:36 网站建设 项目流程

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 /analyze40%model_name=disease_detection_superclinicalconfidence_threshold=0
POST /pii/deidentify40%method=maskmodel_name=OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1confidence_threshold=0
POST /pii/extract/stream20%流式参数:chunk_size=128window_chars=256tokenizer_context_chars=64max_entity_chars=128include_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):

  1. deploy/docker/Dockerfile构建镜像,标签默认openmed:loadtest
  2. --detach --rm方式启动临时容器,把宿主机的127.0.0.1:18080映射到容器内8080
  3. 轮询http://127.0.0.1:18080/readyz,最长等待 900 秒(LOADTEST_STARTUP_TIMEOUT_SECONDS),期间每 2 秒探测一次;
  4. 预热三个路由(/analyze/pii/deidentify/pii/extract/stream各发一次合成请求),消除冷启动影响;
  5. 运行 k6 场景并强制执行 SLO 阈值;
  6. 退出时(含出错)强制删除临时容器。

由于容器使用--rm且脚本注册了trap cleanup EXIT,本地运行结束后不会残留容器,报告默认写入系统临时目录,因此本地压测不会在仓库里产生任何文件

常用变体

跳过构建与预热(使用预构建镜像):

LOADTEST_SERVICE_IMAGE=openmed:local \ LOADTEST_SKIP_BUILD=1 \ LOADTEST_WARMUP=0 \ deploy/loadtest/run.sh
  • LOADTEST_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_BINdockerDocker 可执行文件
K6_BINk6k6 可执行文件
LOADTEST_SERVICE_PORT18080宿主机映射端口(须为 1024–65535 的非特权端口)
LOADTEST_CONTAINER_NAME随机临时容器名
LOADTEST_STARTUP_TIMEOUT_SECONDS900就绪等待上限(须为正整数)
OPENMED_PROFILEprod服务 profile,透传给OPENMED_PROFILE
OPENMED_OFFLINE未设置若已设置,则透传给容器开启离线模式

三、SLO 门禁:阈值如何配置、如何判定

k6 通过thresholds强制执行门禁(见 deploy/loadtest/scenario.js),任何一条阈值被击穿,k6 进程即以非零状态退出;同时场景脚本的handleSummary()会把“是否通过”显式写入控制台与 JSON 报告(deploy/loadtest/scenario.js)。

环境变量与默认值

变量默认值含义
LOADTEST_DURATION_SECONDS30测试时长(秒)
LOADTEST_RATE2目标每秒请求数(到达率)
LOADTEST_CONCURRENCY4预分配虚拟用户数
LOADTEST_MAX_VUS2 × concurrency虚拟用户上限
LOADTEST_SLO_P95_MS30000p95 延迟严格上限(毫秒)
LOADTEST_SLO_P99_MS60000p99 延迟严格上限(毫秒)
LOADTEST_SLO_ERROR_RATE0.05失败请求占比严格上限(5%)
LOADTEST_SLO_MIN_THROUGHPUT_RPS0.5最低达成吞吐量(req/s)

场景脚本还接受短别名SLO_P95_MSSLO_P99_MSSLO_ERROR_RATESLO_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_durationhttp_req_failedhttp_reqs)与自定义指标(loadtest_latency_msloadtest_errorsloadtest_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)的服务,并重复运行同一档位配置取稳定结果:

设备档位p95p99错误率最低吞吐
Nano / 受限 CPU20,000 ms30,000 ms5%0.1 req/s
手机 / 小型笔记本10,000 ms15,000 ms2%0.25 req/s
笔记本5,000 ms10,000 ms1%0.5 req/s
服务器3,000 ms6,000 ms1%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未通过)或某个预热路由失败。

排查路径:

  1. 打开归档的 JSON 报告:对比p95_ms/p99_ms/error_rate/throughput_rpsslo块中的上限/下限,确定是哪一项击穿;
  2. 查看容器启动日志run.sh在等待就绪超时时会自动打印容器最近 80 行日志(deploy/loadtest/run.sh);本地手动排查也可用docker logs <容器名>
  3. 检查模型是否真正预热:若阈值收紧后吞吐骤降,多半是模型冷加载或 keep-alive 过期后反复重载,可增大OPENMED_SERVICE_KEEP_ALIVE或确认OPENMED_SERVICE_PRELOAD_MODELS覆盖了场景中用到的模型;
  4. 保持单容器、单节点前提:该压测工作负载按设计是单容器单节点,它不是生产容量测试,也不能替代单元并发测试工具(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'形式处理)。

七、从压测到调参:一份可落地的操作清单

  1. 在目标机器(本机或 CI Runner)准备好 Docker、curl、k6;
  2. 首次运行直接deploy/loadtest/run.sh,用默认门禁验证工具链闭环;
  3. 依据设备档位表选择一组更严格的阈值,通过LOADTEST_SLO_*或短别名传入;
  4. LOADTEST_RESULT_FILE把报告落到固定路径,重复运行同一档位至少两轮,取稳定值;
  5. 只有拿到稳定、已预热的数据后,再决定是否调整门禁阈值或 keep-alive/预加载配置;
  6. 将最终档位固化到 .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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询