使用Locust对PaddlePaddle模型服务进行压力测试的完整指南

发布时间:2026/6/30 3:44:58

使用Locust对PaddlePaddle模型服务进行压力测试的完整指南 1. 项目概述从模型部署到压力测试的最后一公里在AI模型工程化的链条里把模型用PaddlePaddle训练好、封装成Docker镜像并成功部署往往只算走完了前半程。真正考验这个服务是否“扛得住”的是上线后面对真实、并发用户请求时的表现。这就是压力测试要解决的问题。我们不可能等到半夜流量高峰时让真实的用户去“测试”我们的服务会不会崩那代价太大了。所以必须在预发布或测试环境用工具模拟出高并发的请求来“压一压”我们的服务看看它的极限在哪里瓶颈在何处。这个项目要做的就是针对一个已经打包好的PaddlePaddle模型服务镜像使用Locust这个开源的负载测试工具来模拟海量用户请求对其进行全面的压力测试。你可能会问为什么是Locust市面上压测工具不少比如JMeter、wrk、abApacheBench等。Locust的核心优势在于其“代码即脚本”的理念。它允许你用纯Python编写用户行为脚本这意味着你可以极其灵活地定义复杂的请求逻辑、处理动态参数比如每次请求携带不同的输入数据、以及对响应结果进行断言和提取。这对于测试AI模型API尤其重要因为我们的请求体request body往往是一个结构化的JSON里面包含了需要模型推理的文本、图像特征向量或其他复杂数据这不是一个简单的URL GET请求能搞定的。简单来说这个项目的价值在于它能帮助开发者和运维同学在模型服务上线前清晰地回答以下几个关键问题我这个PaddlePaddle服务镜像在单台宿主机上每秒最多能处理多少请求QPS它的响应时间P99 P95在多少并发下会开始恶化服务的内存、CPU使用率随着压力增长的变化曲线是怎样的会不会在某个压力点出现请求失败、超时甚至服务崩溃的情况通过Locust模拟请求进行压力测试我们不仅能得到这些问题的量化答案还能定位到性能瓶颈是模型推理本身慢还是Web服务框架如PaddleServing、FastAPI的问题亦或是资源CPU/内存不足。2. 核心思路与工具选型解析2.1 为什么选择Locust进行模型API压测压力测试工具的选择直接决定了测试场景的逼真度和结果的参考价值。对于PaddlePaddle模型服务通常通过HTTP/RESTful API或gRPC接口暴露我们需要一个能高度定制请求内容、易于集成到CI/CD流程、并且能提供清晰可视化报告的工具。灵活性至上Python脚本定义一切Locust最大的魅力在于测试场景完全由Python代码描述。一个典型的AI模型推理请求其HTTP POST的body可能是一个复杂的JSON。使用Locust你可以轻松地用Python的requests库或locust内置的HTTP客户端来构建这个请求甚至可以从一个文件或数据库中循环读取不同的测试用例作为输入模拟真实场景下用户提交不同数据的情况。这是很多基于UI配置的压测工具难以做到的。分布式与可扩展性Locust天生支持分布式运行。一台机器模拟的并发用户数用户量有限受限于本机网络和端口资源。当你需要模拟成千上万的并发用户时可以启动一个Master节点和多个WorkerSlave节点。Master负责协调和收集数据Worker们负责真正地发出请求。这让我们可以轻松地发起大规模压力测试。清晰的结果展示与实时监控Locust提供了一个基于Web的实时监控界面默认运行在http://localhost:8089。在这个界面上你可以动态地调整并发用户数、查看实时的RPS每秒请求数、响应时间分布、失败率等关键指标。测试结束后还能生成HTML格式的报告方便留存和分享。这种实时反馈对于交互式地寻找服务瓶颈点非常有用。对比其他工具JMeter功能强大但基于GUI的配置对于复杂JSON请求和逻辑处理有时显得笨重且脚本JMX文件版本管理不如纯代码友好。wrk/ab轻量级性能极高但主要用于测试简单的HTTP端点对于需要携带复杂body的POST请求支持不够方便定制能力弱。k6新兴工具脚本用JavaScript编写性能很好但生态和社区目前相对于Locust的Python生态稍弱。综合来看对于需要深度定制请求内容、且团队熟悉Python的AI项目Locust是进行模型服务压力测试的绝佳选择。2.2 PaddlePaddle服务镜像的常见形态与测试准备在我们开始编写Locust脚本之前必须明确我们要压测的“靶子”——PaddlePaddle服务镜像——是以何种形式提供服务的。常见的有以下几种基于PaddleServing的镜像这是PaddlePaddle官方的服务化部署框架。它会启动一个或多个服务端通常通过HTTP端口9393或gRPC端口提供预测接口。请求和响应有固定的格式例如使用key: value格式传递多个输入Tensor。我们需要根据其API文档来构造请求。基于FastAPI/Flask Paddle Inference的镜像很多开发者喜欢用轻量级Web框架如FastAPI包裹Paddle Inference的预测库自行定义API接口。这种方式接口更灵活可能是一个简单的/predict端点接收一个JSON返回一个JSON。基于Triton Inference Server的镜像NVIDIA的Triton是高性能推理服务化框架也支持PaddlePaddle模型后端。它有自己的一套客户端APIHTTP或gRPC。测试前的关键准备步骤确定API端点与协议首先你需要知道你的服务镜像启动后访问地址是什么如http://localhost:9393或http://localhost:8000以及具体的预测端点路径如/uci/prediction或/v2/models/{model_name}/infer。获取API接口规范最好能找到该服务的API文档Swagger/OpenAPI明确请求方法POST/GET、请求头Content-Type等以及请求体Request Body的精确结构。准备测试数据集压力测试不应该只用一条数据反复请求那样可能会触发服务的缓存优化导致测试结果不真实。应该准备一批有代表性的测试数据几十到几百条在Locust脚本中随机或轮询使用以模拟真实流量。启动待测服务使用Docker命令将你的PaddlePaddle镜像运行起来。确保服务健康并能通过curl等工具正常访问。docker run -d -p 9393:9393 --name paddle-serving your-paddle-serving-image:tag # 或者 docker run -d -p 8000:8000 --name paddle-fastapi your-fastapi-paddle-image:tag3. 构建Locust压力测试脚本详解3.1 脚本骨架与用户行为建模一个Locust脚本的核心是定义一个或多个继承自HttpUser用于HTTP测试或User用于自定义协议的用户类。在这个类里你会定义wait_time用户等待时间和tasks用户执行的任务集。让我们从一个最简单的例子开始假设我们的PaddlePaddle服务提供了一个/predict的POST接口。from locust import HttpUser, task, between import json class PaddleModelUser(HttpUser): # 模拟用户在每个任务执行后等待1-3秒 wait_time between(1, 3) # 在类初始化时加载测试数据 def on_start(self): # 这里可以是从文件读取或硬编码一些测试数据 self.test_data_pool [ {input: 这是一条正面评价产品非常好用。}, {input: 体验很差客服态度恶劣不会再买了。}, # ... 更多数据 ] # 使用task装饰器定义一个任务权重为1 task(1) def predict_sentiment(self): # 从数据池中随机选取一条数据 import random data random.choice(self.test_data_pool) # 构造请求头通常AI模型API需要指定Content-Type headers {Content-Type: application/json} # 使用self.client发起请求这是一个locust内置的HttpSession # 它会自动记录请求的成功/失败、响应时间等 with self.client.post(/predict, jsondata, headersheaders, catch_responseTrue) as response: # catch_responseTrue允许我们自定义成功/失败的判断逻辑 if response.status_code 200: resp_json response.json() # 可以在这里对响应内容进行断言例如检查返回的label或score if resp_json.get(label) in [positive, negative]: response.success() else: response.failure(fUnexpected response: {resp_json}) else: response.failure(fStatus code: {response.status_code})关键点解析wait_time between(1, 3)这定义了模拟用户在每次任务执行后的思考时间。对于压力测试我们通常希望持续施压所以这个值可以设得很小比如between(0.1, 0.5)甚至使用constant(0)来不间断请求。但在探索性测试或模拟更真实用户行为时设置等待时间是有意义的。task(weight)weight参数定义了任务的权重。如果类中有多个taskLocust会根据权重比例来随机选择执行哪个任务。权重越高被选中的概率越大。self.client这是Locust封装好的HTTP客户端它自动将请求的耗时、结果汇总到统计数据中。务必使用它而不是直接用requests库。catch_responseTrue与自定义断言这是Locust的精华功能之一。仅仅根据HTTP状态码200判断成功是不够的。对于模型API可能因为输入不合法等原因服务返回了200但body里包含错误信息。我们需要解析响应体根据业务逻辑判断请求是否真正成功。3.2 适配PaddleServing的请求格式如果你的镜像是基于PaddleServing的那么请求格式会有所不同。PaddleServing的HTTP接口通常期望数据以特定的key-value形式传递其中key是你在服务配置中定义的feed_var名称value是经过处理后的数据列表。假设我们有一个房价预测模型输入特征是[面积 房间数]在服务端配置的feed_var名为x。from locust import HttpUser, task, constant import json import random class PaddleServingUser(HttpUser): # 压力测试时我们可能不需要等待时间用constant(0) wait_time constant(0) task def predict_house_price(self): # 模拟生成测试数据面积在50-200平米房间数1-5间 area random.uniform(50, 200) rooms random.randint(1, 5) # PaddleServing HTTP接口的标准请求格式 request_data { x: [ # ‘x’ 对应服务端的feed_var名称 [area, rooms] # 注意这里是双层列表因为可以一次预测多条 ] } headers {Content-Type: application/json} # PaddleServing的预测接口通常是 /{模型名}/prediction 或直接 /prediction with self.client.post(/uci/prediction, jsonrequest_data, headersheaders, catch_responseTrue) as response: if response.status_code 200: resp_json response.json() # PaddleServing返回的数据在特定的key下例如 ‘result’ if isinstance(resp_json.get(result), list): # 可以进一步检查预测值是否在合理范围 prediction resp_json[result][0][0] # 获取第一个结果的第一个值 if prediction 0: # 假设房价预测值为正 response.success() else: response.failure(fInvalid prediction: {prediction}) else: response.failure(fUnexpected result format: {resp_json}) else: response.failure(fHTTP Error: {response.status_code})注意PaddleServing的请求和响应结构严格依赖于服务端配置serving_server_conf.prototxt。务必与模型部署的同学确认好准确的feed和fetch变量名。错误的key会导致请求失败。3.3 实现更真实的复杂用户场景一个优秀的压力测试脚本应该尽可能模拟真实场景。这包括动态变量与关联有时下一个请求依赖于上一个请求的响应。例如先调用一个“预处理”接口再调用“预测”接口。task def multi_step_prediction(self): # 步骤1: 上传或处理数据获取一个task_id with self.client.post(/preprocess, data..., name/preprocess) as prep_resp: if prep_resp.ok: task_id prep_resp.json()[task_id] # 步骤2: 使用上一步的task_id进行预测 with self.client.get(f/predict?task_id{task_id}, name/predict) as pred_resp: ... # 处理预测结果这里使用了name参数它可以将不同的URL归类统计。否则每个不同的task_id都会被视为一个独立的请求路径导致统计数据混乱。测试数据从文件读取对于大规模测试应该将测试数据放在外部文件如JSON、CSV或TXT中。import csv class PaddleModelUser(HttpUser): def on_start(self): self.test_cases [] with open(test_data.csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: self.test_cases.append(row) task def predict(self): data random.choice(self.test_cases) # 根据CSV列名构造请求体...设置请求超时在self.client的请求方法中可以设置timeout参数。这对于测试服务在长时间无响应时的表现很重要。with self.client.post(/predict, jsondata, timeout3.0, catch_responseTrue) as response: # 如果3秒内没收到响应locust会标记为Timeout异常4. 执行测试与结果深度分析4.1 启动Locust与配置压测参数编写好脚本保存为locustfile.py后就可以启动Locust了。有两种主要运行模式1. Web UI 模式交互式推荐用于调试和探索在终端中进入脚本所在目录运行locust -f locustfile.py --hosthttp://localhost:9393-f: 指定你的Locust脚本文件。--host: 指定待测服务的基础URL。脚本中的相对路径如/predict会拼接到这个host后面。运行后打开浏览器访问http://localhost:8089你会看到Locust的Web界面。在这里你需要输入Number of users (peak concurrency)模拟的最大总用户数并发数。注意这不是每秒请求数RPS而是同时“在线”的虚拟用户数。Spawn rate (users started/second)每秒启动多少个用户直到达到峰值用户数。Host如果启动命令没指定这里可以再填。点击“Start swarming”就开始压测了。你可以实时看到RPS、响应时间、失败率等图表。2. 无头模式Headless用于自动化/CI/CD如果你不需要UI或者想在命令行中运行测试并生成报告可以使用locust -f locustfile.py --hosthttp://localhost:9393 --headless -u 100 -r 10 -t 2m --htmlreport.html--headless: 启用无头模式。-u 100: 设置峰值用户数为100。-r 10: 设置每秒生成10个用户。-t 2m: 设置运行时间为2分钟也可以是10s,1h30m等。--htmlreport.html: 测试结束后生成HTML报告。4.2 监控服务端资源与定位瓶颈在Locust疯狂发送请求的同时你必须同时监控PaddlePaddle服务镜像所在容器的资源使用情况。这是定位性能瓶颈的关键。打开另一个终端使用docker stats命令docker stats 你的容器名或ID你会看到实时的CPU、内存、网络I/O、块I/O使用率。如何关联分析CPU瓶颈如果容器的CPU使用率持续接近100%尤其是单核100%如果你的服务是单线程/进程而Locust的RPS上不去响应时间却很长那么瓶颈很可能在模型推理的计算环节。这可能意味着模型较复杂或者需要优化推理配置如开启MKLDNN/ONEDNN加速、使用TensorRT、或增加服务进程数。内存瓶颈观察内存使用量是否持续增长并接近限制OOM。PaddlePaddle模型加载会占用较大内存如果并发请求导致多个线程/进程同时进行大张量运算内存可能吃紧。如果内存耗尽服务会被操作系统杀死Locust会看到大量连接失败。网络与I/O通常对于模型推理服务网络I/O不是主要瓶颈除非你的请求/响应数据包非常大例如传输高清图片。docker stats中的网络流量数据可以帮你确认这一点。更深入的监控可以进入容器内部使用top,htop,nvidia-smi如果使用GPU等命令查看是哪个进程占用了资源。对于PaddleServing它可能是多个worker进程。4.3 解读Locust测试报告测试结束后或在Web界面点击StopLocust会提供一份详细的统计报告。你需要关注以下几个核心指标Requests/s (RPS)每秒完成的请求数。这是衡量服务吞吐量的核心指标。随着并发用户数增加RPS会增长到一个峰值后趋于平缓甚至下降这个峰值就是当前配置下的最大吞吐能力。Response Times (ms)Average平均响应时间。参考价值一般容易被极端值影响。Median (50%)中位数响应时间一半的请求快于此值。95th percentile (P95)95%的请求响应时间低于此值。这是评估服务体验的关键指标。比如P95200ms意味着95%的用户感觉服务很快但剩下5%的用户可能经历了较慢的响应。99th percentile (P99)99%的请求响应时间低于此值。用于评估长尾延迟对高要求场景非常重要。Failure %请求失败率。任何非零的失败率都需要仔细排查原因。点击“Failures”标签可以查看具体的失败请求和错误信息如超时、连接被拒、断言失败等。分析图表Total Requests per SecondRPS随时间变化的曲线。健康的曲线应该是随着用户数爬升而上升然后在一个区间内稳定波动。如果出现剧烈下跌可能表示服务出现了问题如崩溃、重启。Response Times (percentiles)响应时间百分位随时间变化的曲线。理想情况下它们应该保持相对平稳。如果随着测试进行P95/P99时间线明显上扬说明服务可能出现了排队、资源竞争或内存泄漏等问题。Number of Users虚拟用户数曲线用于确认压测负载是否符合预设。5. 高级策略与常见问题排查5.1 分布式压测与参数化进阶当单台压测机无法产生足够压力或者想避免压测机自身成为瓶颈时就需要分布式运行Locust。启动Master节点locust -f locustfile.py --hosthttp://target-service:port --masterMaster节点本身不模拟用户只负责分发任务和收集结果。启动Worker节点在一台或多台机器上locust -f locustfile.py --hosthttp://target-service:port --worker --master-hostMASTER_IP你需要将locustfile.py和测试数据文件同步到所有Worker机器上。所有Worker会连接到Master接收指令并发送请求。参数化数据避免重复在分布式场景下如果所有Worker都读取同一个数据文件并随机选择可能会导致不同Worker同时发送相同数据降低了测试的随机性。一个更好的做法是使用os.environ或启动参数为每个Worker分配一个唯一ID然后让它们读取数据文件的不同部分。import os worker_id int(os.environ.get(LOCUST_WORKER_NUM, 0)) total_workers int(os.environ.get(LOCUST_WORKER_COUNT, 1)) # 假设有1000条测试数据 all_data load_all_data() my_data_segment all_data[worker_id::total_workers] # 切片分配数据5.2 典型问题排查实录在实际对PaddlePaddle镜像进行压力测试时我踩过不少坑这里总结几个最常见的问题和排查思路问题1Locust报告大量 “ConnectionResetError” 或 “Connection refused” 错误。可能原因1服务端连接数被占满。PaddleServing或Web框架如uvicorn有默认的最大并发连接数限制。当Locust的并发用户数超过这个限制新的连接就会被拒绝。排查检查服务端的配置。对于PaddleServing查看serving_server_conf.prototxt中的workflow_concurrency等参数。对于FastAPIUvicorn检查启动命令中的--limit-concurrency和--backlog参数。解决调整服务端配置增加最大并发数并确保宿主机和Docker容器的文件描述符限制ulimit -n足够高。可能原因2压测机端口耗尽。Locust作为客户端每个模拟用户尤其是在HttpUser使用短连接时都会占用一个本地端口。当并发数极高时可能会耗尽压测机的临时端口范围net.ipv4.ip_local_port_range。排查在压测机上运行netstat -an | grep TIME_WAIT | wc -l查看处于TIME_WAIT状态的连接数。如果非常多接近端口范围上限就是这个问题。解决优化Locust脚本使用HttpUser的client保持持久连接默认就是keep-alive减少端口占用。或者修改系统参数扩大临时端口范围sysctl -w net.ipv4.ip_local_port_range1024 65535。问题2响应时间随着测试时间推移越来越长但CPU/内存并未饱和。可能原因服务内部资源泄漏或竞争。例如在推理代码中不小心创建了全局变量并持续增长或者线程池/连接池配置不当。排查这种问题最难定位。需要结合更细致的监控。可以尝试降低并发数看响应时间是否稳定。如果低并发下稳定高并发下恶化可能是竞争问题。检查服务日志是否有警告或错误信息。使用py-spy等性能分析工具对服务进程进行采样查看时间都花在哪里了。解决审查服务端代码检查是否有不合理的全局状态。对于PaddleServing确保model_toolkit配置正确engine配置合理。问题3失败率间歇性出现错误信息多样Timeout, ReadTimeout, 500错误。可能原因服务不稳定或存在毛刺。可能是由于宿主机资源争抢其他进程、Docker容器资源限制CPU配额、内存限制或模型推理本身存在波动如首次推理慢、缓存未命中。排查观察docker stats看是否在出现错误的时间点容器的CPU或内存使用率达到极限。检查宿主机整体资源使用情况htop。查看服务端和容器的日志docker logs -f container_id寻找ERROR或WARNING级别的日志。解决为Docker容器分配更充裕的资源--cpus,--memory。优化模型服务例如使用预热warm-up来避免首次推理延迟。对于Paddle Inference可以尝试开启各种计算优化选项。5.3 将压测集成到CI/CD流水线对于追求工程效能的团队可以将Locust压力测试作为CI/CD流水线中的一个自动化关卡。基本思路是构建与部署在流水线中首先构建PaddlePaddle服务镜像并将其部署到测试环境。健康检查等待服务健康端点如/health返回成功。执行无头压测使用locust --headless命令运行一个短时间的基准测试例如1分钟50个并发用户。结果断言解析Locust的输出或生成的JSON/HTML报告对关键指标进行断言。例如平均响应时间 100msP95响应时间 300ms失败率 0%RPS 50生成报告与归档如果测试通过继续后续流程如果失败则中止并通知负责人同时将详细的Locust HTML报告和资源监控截图作为附件保存方便排查。可以使用Python的locust.plugins模块或者解析Locust的退出码和日志来实现自动化断言。这样每次代码更新或镜像重建后都能自动验证其性能是否满足基线要求有效防止性能退化。压力测试不是一劳永逸的事情它应该随着模型迭代、服务架构调整和流量预估变化而定期执行。通过将Locust与PaddlePaddle镜像的结合我们建立了一个可重复、可量化的性能评估体系这是确保AI服务稳定、可靠上线的重要保障。在实际操作中耐心分析测试数据结合系统监控才能真正找到优化方向让服务从容应对未来的真实流量挑战。

相关新闻