资讯动态

分布式压测实战:用Locust协程架构实现十万级并发

发布时间:2026/10/7 4:11:46 来源:尧图企业网站定制
欢迎来到一期聊到极致并发的话题。老读者都知道我折腾压测工具已经有几年时间平时最常用的是 JMeter但只要是涉及十万级用户并发这类需求我会毫不犹豫把 Locust 从工具链里拉出来。原因很简单Locust 基于 Python 的协程机制天然适合模拟大量轻量级用户而且分布式架构做得足够干净Master 负责控制与汇总Worker 负责跑压力脚本。今天这篇就把从目标拆解到分布式部署、脚本编写、梯度加压、问题排查的完整路径从头到尾捋一遍希望能帮你少踩几个我已经替你踩过的坑。先解释一个容易混淆的问题十万级并发到底指什么。在性能测试里并发用户数并不等于同时发出的请求数它通常指“虚拟用户意味着的活跃会话数量”。如果十万个虚拟用户都设置了随机等待时间实际到达后端的 RPS每秒请求数可能只有一两万如果所有用户都无脑循环调用接口没有任何 think time那 RPS 可能冲到几十万这时候被压垮的往往不是业务系统而是压测机自身的网络栈、文件描述符和 CPU。所以要实现十万级并发必须先把业务模型想清楚是十万在线用户、十万长时间连接IM、WebSocket 场景还是十万持续请求业务模型不同分布式架构的规模、脚本写法、监控重点全都不一样。1. 目标拆解十万级并发背后的真实压力和选型逻辑性能测试最容易犯的错误是直接套命令不管底层模型是否匹配。十分钟级并发这个目标听起来很性感但如果不把业务场景拆细后面做分布式部署时会发现要么资源浪费要么压测机本身变成瓶颈。这一节想先把 Locust 的核心模型和分布式架构的逻辑说清楚因为后面所有操作都建立在“为什么这样做”的基础上。1.1 十万级并发是什么概念从业务场景反推测试要求先说一个我常用的估算方式如果目标是十万在线用户每个用户平均每 30 秒执行一次请求那么系统需要承受的 QPS 大约是 100000 / 30 ≈ 3333如果每个用户每 3 秒执行一次QPS 就是 33333。这两个数量级的运维压力完全不同。所以接到“十万并发”需求时我第一件事不是找机器而是问需求方你们说的并发是“在线用户数”还是“活跃连接数”后台有没有采集业务埋点平均请求间隔是多少只有把这些问题落到数字才能规划出合理的压测脚本。比如我遇到过一个 IM 类项目号称要支持十万在线长连接这种场景用户大部分时间处于 idle 状态真正每秒推送消息的高峰期活跃连接也许只有两三千。这种情况下压测的关键就不是把 Locust 的虚拟用户数堆到十万而是模拟大量 idle 连接同时保证少量用户高频收发消息时系统能稳定。因此我用 Locust 脚本时会给不同任务分配权重绝大多数用户执行长连接心跳任务少部分用户执行消息发送任务再配合wait_time控制请求间隔这样得到的十万并发才是贴近真实的模拟。1.2 Locust凭什么能做分布式协程模型和主从架构的底层逻辑Locust 的底层核心是 gevent 协程。每个模拟用户本质上是一个轻量级协程同一个 Worker 进程里可以轻松创建成千上万个并发协程而没有线程那种昂贵的上下文切换开销。这也是 Locust 比某些基于线程的压测工具更适合高并发的原因。但协程不是银弹Python 进程内有 GIL 这一层限制纯粹靠一个 Worker 进程很难把 CPU 压力拉满所以 Locust 把复杂度转移到了进程和机器层面。分布式架构上Locust 采用经典的主从模型。Master主节点负责管理 Web UI、接受测试参数、下发启动指令、汇集 Worker 上报的统计数据真正产生压力的是 Worker工作节点每个 Worker 是一个独立的 Python 进程。Master 默认监听 5557 端口做指令通信5558 端口接收结果数据。启动时通过--master和--worker参数区分角色--master-host、--master-port让 Worker 找到 Master。旧的文档和网上很多文章还在用--slave那是老版本写法新版本已经全部换成--worker了照着老教程配置会直接报参数错误。1.3 单机压测撞墙的四个原因在把目标定为十万级后如果还指望一台 32 核机器跑完通常会在四五个地方撞墙。首先是操作系统文件描述符限制默认进程可以打开的 FD文件描述符数量只有 1024 或 65535 左右而一个用户如果使用独立连接十万个用户就需要十万个连接第一道墙就在这里。其次是端口资源客户端发起大量短连接时会快速消耗本机临时端口如果端口范围不够大会出现Cannot assign requested address或大量TIME_WAIT堆积连接根本建不起来。第三是 Python 进程本身的 GIL即使协程再多10万个协程之间也会有 Python 字节码切换和统计信息上报的成本单个 Worker 进程抗两三万并发还行再往上 CPU 就顶满了。最后是带宽和网络栈压测机到被测系统之间如果只有千兆网打满也就是一秒钟 100MB 级别一旦请求响应体较大很容易把网卡打满造成响应时间虚高。因此单机跑十万级并发基本不现实分布式部署是唯一靠谱的路径。2. 分布式压测环境搭建Master-Worker架构从入门到能跑聊清楚了底层选型就可以动手搭环境了。很多新手第一次搭 Locust 分布式集群容易把 Master 也当作压力节点或者在 Master 上又跑 Web 又跑脚本结果整个控制台卡死数据看板掉线。这一节说说怎么搭出一套干净可靠的分布式压测环境。2.1 最精简的分布式部署一台Master加N台Worker先把最核心的部署架构写出来最小可用集群是一台 Master 加 N 台 Worker。Master 机器配置可以一般2 核 4G 内存都够用但最好单独跑不要在上面启动压测任务因为 Web UI 和所有 Worker 的结果汇聚都会吃 CPU 和内存。Worker 机器的配置才是重点。假设我们准备了 6 台 Worker每台 32 核 64G 内存操作系统是 CentOS 7 或 Ubuntu 20.04。先安装 Locustpip3 install locust然后在压测脚本目录下分别启动# Master 节点 locust -f loadtest.py --master --expect-workers6 --web-port8089 --hosthttps://api.example.com # Worker 节点6台机器都执行 locust -f loadtest.py --worker --master-host192.168.1.10 --master-port5557 --processes4这里有几个关键点。--expect-workers6告诉 Master 预计有多少个 Worker 接入当 Worker 数量达不到这个值时Web UI 会显示等待状态方便在启动前检查集群是否全部就绪。如果省略这个参数Worker 陆续连上来也能跑但测试启动时可能因为部分 Worker 掉线造成并发分配不均。--processes4表示在单台 Worker 机器上额外创建 4 个独立进程结合机器的 32 核每个 Worker 进程大约占用 8 核算是比较合理的配置。启动顺序上我习惯先启动 Master再启动所有 Worker然后在 Web UI 或通过命令行确认所有 Worker 处于“Ready”状态后再点击“Start”。如果用命令行启动压测可以locust -f loadtest.py --master --expect-workers6 --run-time 30m -u 100000 -r 2000 --headless这里-u 100000是虚拟用户总数-r 2000是每秒启动用户数--headless表示不启动 Web UI纯命令行模式适合自动化 CI。2.2 按CPU核数和业务模型规划Worker数量Worker 数量怎么定不能拍脑袋。先要压测脚本平均每个虚拟用户占用的 CPU 开销我的经验是 FastHttpUser 场景下一个 32 核 Worker 进程不额外开--processes跑 1.5 万到 2 万虚拟用户CPU 会到 70% 左右。如果加--processes 4单台机器可以尝试跑到 5 万虚拟用户但这时候要给 CPU 预留 20% 余量防止因为调度波动导致响应时间失真。所以我常用的估算是十万虚拟用户一台 32 核 Worker 承担 3 万左右至少需要 4 台 Worker如果业务场景包含大量解析 JSON、加密签名等 Python 逻辑这个容量要再减半可能要 6 到 8 台。对于 IO 密集但 CPU 逻辑少的纯转发场景一台 Worker 可以再多扛一些。计算方式如下单个 Worker 进程能稳定支撑的虚拟用户数 单进程可用的 CPU 核心算力 / 每个用户任务的 CPU 消耗系数这个系数只能靠小规模预压测得出没有万能公式。如果你手头机器有限还可以在一台机器上开多个--processes相当于把多核 CPU 利用起来。但要注意进程数不是越多越好每个 Worker 进程都会建立独立的连接池进程数过多会造成连接堆积和端口耗尽反而触发网络栈问题。32 核机器建议--processes 6到--processes 8以内具体可以压测过程中观察 CPU 使用率来调整。2.3 网络拓扑和云上部署的几个注意点分布式压测比单机压测多了一个压测机与压测机之间、压测机与业务系统之间的网络规划。如果 6 台 Worker 在同一个内网带宽资源可以共享但要注意别把压测流量打成 P2P 把内网带宽吃满。我一般要求被测系统部署在独立网段压测集群和被测系统之间至少保留千兆以上带宽如果压测机在云上建议使用同一区域、同一 VPC 内固定带宽避免跨公网导致数据包延迟和丢包最后把性能数据污染了。另一个容易忽略的是时间同步。Master 和 Worker 的系统时间要一致否则统计响应时间、汇总 RPS 数据时会出现错误的数据对齐。简单粗暴的做法是在每台压测机上执行sudo ntpdate -u ntp.aliyun.com或者在云上直接用云平台提供的 NTP 服务。时间同步虽然看起来不影响压测本身但实验做完回看数据的时候你不想看到一台 Worker 的统计延迟了 10 秒吧。3. 写一个能扛住十万级并发的Locust压测脚本环境搭好之后核心就是压测脚本。一个写不好的脚本会在十万虚拟用户场景下把压测机资源直接耗尽或者在业务侧造成大量重复无效请求。所以脚本必须围绕并发模型和业务场景精细设计。3.1 HttpUser还是FastHttpUser并发场景下的选择如果你是第一次接触 Locust很可能直接用传统HttpUser。它基于requests库实现API 友好能比较方便地处理登录、Cookie、重定向等常见需求。但requests在协程环境下每次请求都要做很多 Python 层面的封装并发上去后CPU 开销非常明显特别是在十万量级下Worker 的 CPU 会被requests库大量消耗。因此只要不是必须用到复杂的请求跟踪、自定义代理或特定重定向逻辑我都推荐直接用FastHttpUser。它底层使用geventhttpclient连接复用率更高请求开销更小在高并发时能把更多 CPU 留给实际的数据处理任务。下面是一个最小示例from locust import FastHttpUser, task, between class ApiUser(FastHttpUser): wait_time between(0.01, 0.05) task def get_order(self): with self.client.get(/api/order/1, headers{Authorization: Bearer xxx}, catch_responseTrue) as resp: if resp.status_code ! 200: resp.failure(status code error)注意FastHttpUser的self.client返回对象与requests的响应对象有些差别。比如判断响应内容时需要调用resp.text或resp.content大文件下载场景要使用streamTrue方式处理。如果在脚本里出现resp.json()后又被忽略返回值也会白白增加 CPU 开销。3.2 账号池、数据分片和用户身份隔离十万虚拟用户如果都用一个账号去压测业务系统大概率会因为单账号 Token 刷新或 Session 覆盖而返回大量 401。所以压测脚本里必须处理用户身份并且要在分布式 Worker 之间做数据隔离。我的做法是准备一个数据文件比如 CSV 格式的账号列表里面包含用户名、密码、用户ID然后启动每个 Worker 时通过环境变量传入两个参数LOCUST_WORKER_INDEX和LOCUST_WORKER_COUNT。脚本启动时读取这些变量再把自己负责的账号数据从总数据集中切片取出来。这样每个 Worker 只使用自己分到的账号既能避免多个协程同时修改同一个账号状态又可以最大化模拟真实用户。示例脚本片段import os import pandas as pd worker_index int(os.environ.get(LOCUST_WORKER_INDEX, 1)) worker_count int(os.environ.get(LOCUST_WORKER_COUNT, 1)) accounts pd.read_csv(accounts.csv)[account].tolist() my_accounts accounts[worker_index-1::worker_count] class RealUser(FastHttpUser): def on_start(self): account my_accounts.pop() if my_accounts else fallback_user # 使用该账号进行登录刷新 token 到 self.token注意on_start会在每个虚拟用户启动时被调用一次如果十万个用户全部重新登录会对认证系统造成巨大的瞬时分流压力。所以我一般会预生成一批登录 Token或者在on_start里直接建立会话并缓存。压测脚本的登录逻辑越重压测系统自身的误差就越大。3.3 关键配置项和启动命令参数对照我平时不太记全部参数但有几个关键配置会写在小本本上这里整理成一个对照表参数/配置作用我的常见值--master以主控模式启动Master 节点--worker以负载生成模式启动Worker 节点--expect-workers等待指定数量 Worker 接入30 或 40与 Worker 数一致--processes单机额外创建进程数按核数决定一般 4 到 8--run-time设置压测持续时间30m 到 2h--spawn-rate每秒启动用户数1000 到 5000视系统承受能力--headless无 Web UI 模式CI 或自动化脚本常用--skip-log-setup关闭默认日志减少 IO压测高峰时开启--csv定时导出指标 CSV便于后续分析--web-host绑定 Web UI 地址0.0.0.0方便调试启动压测时--spawn-rate尤其重要。十万用户如果一开始就设成-r 100000所有协程会在几秒内瞬间创建压测机内存飙升Master 和 Worker 之间的消息也会出现积压数据统计直接失真。我实测下来的稳妥做法是先用-r 2000或-r 5000让系统慢慢爬坡观察稳定后再通过 Web UI 增加用户数。如果一定要一次到位可以先调低预期等集群稳定后再二次调整。3.4 脚本本身的性能陷阱脚本不是写完就能跑我有几个踩过很多次的坑在这里一次性说透。第一不用FastHttpUser时requests.Session默认会做重定向和历史记录每次请求都在 Python 层维护 Cookies。压测逻辑中如果不需要重定向尽量显式设置allow_redirectsFalse减少额外请求。第二wait_time不要设置成固定值比如wait_time 5这会让所有用户节奏完全一致产生“波浪效应”请求会集中在一瞬间打过来。推荐使用between(1, 5)这类范围或者用自定义函数模拟业务真实思考时间。第三catch_responseFalse是默认行为响应内容会被丢弃实际上会好一些但如果需要用with client.get(...) as resp去判断响应体就相当于开启响应读取响应体越大内存和 CPU 开销越高。压测接口如果响应体动辄几百 KB脚本的解析开销会反过来限制并发能力。这种情况下建议在业务测试方案里约定只压测核心逻辑或者把断言简化成status_code判断不要用正则表达式全量扫描响应体。4. 从5000并发到100000并发的执行路径与指标观测环境搭好了脚本写好了接下来就是真正压上十万并发。这不是一次start按钮就完事的事情而是一个爬坡、观察、调整、再爬坡的过程。4.1 梯度加压如何安全地把并发拉上去我负责的大多数压测项目都会遵循一个爬坡节奏。假设最终目标是 100000 虚拟用户那么我会先在 Web UI 或命令行设置 5000运行 3 到 5 分钟观察系统表现确认稳定后再一次性调整到 20000观察 RPS、错误率、内存变化再到 50000 稳定后如果一切正常最后加到 100000。为什么不能直接从 0 拉到十倍一方面是业务系统可能因为缓存预热不够前期错误率特别高容易掩盖真正问题另一方面是 Locust 的 Worker 需要创建大量协程和连接池瞬间创建十万个协程对 Master 的广播和 Worker 的内存回收都是压力。我曾遇到一次直接设 10 万用户结果 Master 的 Web UI 直接超时十几秒低响应最后只能重启集群。后来我习惯在脚本里用LoadTestShape自定义阶段爬坡策略比如from locust import LoadTestShape class StepShape(LoadTestShape): 每 10 分钟加 20000 用户直到 100000 def tick(self): run_time self.get_run_time() step int(run_time // 600) user_count (step 1) * 20000 if user_count 100000: return None return user_count, 1000自定义 Shape 的好处是压测过程完全自动化不需要人工盯着 Web UI 反复点击增加用户尤其在过夜或者远程执行时特别有用。4.2 看哪些指标才能判断压测有效并发人数只是表象真正能说明系统瓶颈的指标是 RPS、响应时间分位数、错误率和资源利用率。我在压测过程中最关注的三个数字是RPS每秒请求数代表系统当前实际处理能力95 分位响应时间p95比平均值更能反映用户真实体验错误率如果错误率超过 0.5%即使 RPS 很高测试结果也不算成功。Locust 的 Web UI 自带这些统计但如果你的压测机上没有浏览器访问可以通过--csv参数定期导出 CSV 文件。示例locust -f loadtest.py --master --expect-workers10 --headless -u 100000 -r 5000 --run-time 60m --csvresult --csv-full-history执行后每 2 秒会生成一个以result为前缀的 CSV 文件包含当前用户数、请求数、失败数、中位数响应时间、95 分位响应时间等非常方便结束后做趋势分析。我一直觉得--csv-full-history这个参数很实用它保留全部历史数据不会因为滚动窗口丢失早期信息。4.3 用Grafana把压测数据可视化如果压测时间长、节点多光看 CSV 和 Web UI 不过瘾。我通常会用 Locust 插件把统计指标推到 InfluxDB再用 Grafana 拉出一张实时大盘。插件安装方式很简单pip3 install locust-plugins然后在脚本里注册事件监听器把每个阶段的用户数、RPS、响应时间、异常数写入 InfluxDB。Grafana 中配置对应的数据源后可以自定义面板比如按 Worker 分组查看每个节点的 CPU 和网络指标。这个方案的好处是压测时不用频繁切换页面任何人只要打开仪表盘就能实时看到瓶颈出现在哪一层。需要注意的是如果压测目标是十万并发放级别Grafana 和 InfluxDB 本身的采样频率不要设得太高否则监控系统也会成为资源消耗者。一般 5 秒采样一次就足够了重点看趋势而不是瞬时的毛刺。5. 高并发实测中的常见问题与排查实录再完美的规划也会在实践中翻车。这一节我把自己真实遇到的排查经历整理成速查表希望能让你少走弯路。5.1 file descriptor耗尽导致ConnectionError第一次跑高并发时Worker 进程突然报大量[Errno 24] Too many open files随后请求失败率直线上升。排查过程很直接ulimit -n显示只有 65535而每个虚拟用户如果建立独立连接那十万用户就需要十万个文件描述符单机六个 Worker 进程加起来已经超过系统限制。解决办法是提高进程级文件描述符上限。启动 Worker 前执行ulimit -n 1048576同时在/etc/sysctl.conf中调高全局限制fs.file-max 2097152 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 net.core.somaxconn 65535改完执行sysctl -p生效。这里要特别提醒net.ipv4.ip_local_port_range的作用是让客户端可以同时发起更多短连接如果压测使用的是大量短连接默认的 32768 到 61000 范围很可能不够。但这个参数也不能无限调大端口范围过大反而会造成系统维护大量TIME_WAIT状态占用内存。5.2 Worker之间并发分配不均的真实原因分布式压测还有一个迷惑现象Master 显示总用户数已经到了 100000但每个 Worker 的实际用户数差异巨大。有的 Worker 跑了 18000有的只有 8000。翻看日志发现是因为--expect-workers设置有误一个 Worker 反复重连后没有完成握手Master 仍然把用户数平均分配给已连接的 Worker导致已连接节点压力上涨。另一种常见原因是 Worker 机器资源不同但 Locust 默认是不感知机器处理能力的Master 只会按 Worker 数量平均分配用户。如果一台机器配置较弱它很快 CPU 打满请求响应变慢反而拉低了整轮测试的有效性。我建议在压测前对 Worker 机器做一次规格摸底如果你混合使用不同规格的机器可以通过分组或手动指定不同启动参数来达到均衡负载。比如弱机器减少--processes数强机器增加--processes数不要让它们裸奔在默认进程数量下。5.3 RPS和响应时间虚高先查压测机自己有次压测系统表现特别好RPS 高得吓人响应时间也低得离谱。仔细一查原来响应体里大部分是静态缓存业务层根本没走到数据库和核心链路。这类问题属于测试设计目标不明确。但还有一类更隐蔽的问题压测机自身上限导致被测系统并没有收到全部请求。当压测机 CPU 打满或者网络带宽打满时Worker 的发送队列会堆积Locust 统计的响应时间就包含了在客户端排队等待的时间这会让 p95 虚高。反过来如果请求在客户端还没发出去系统侧可能观察到的 RPS 并没有那么高。所以每次压测结束后我都会看一眼压测机的 CPU、内存、网卡流量曲线确认客户端自身没有成为瓶颈。如果 Worker node 的 CPU 持续在 90% 以上我会增加 Worker 机器或者降低用户数而不是一味修改被测系统。5.4 被测系统顶不住时从Nginx到数据库并发锁当并发真正拉到十万级别压测机往往还没出问题被测系统先崩了。最常见的是 Nginx 配置限制。Nginx 的最大并发连接数约等于worker_processes * worker_connections默认worker_connections只有 1024只要并发的 TCP 连接超过这个值客户端就会收到502或Connection reset by peer。分布式压测前一定要先和运维确认被测系统 Nginx、网关服务的连接数参数已经被放大比如worker_connections 65535否则压力根本打不到后面的应用层。另外数据库并发锁是更靠后的坎。比如用户表执行UPDATE user SET last_login? WHERE id?十万个用户同时更新行锁竞争会让数据库的 QPS 卡在一个很低的水平。这时候不要急着骂 Locust而要先定位锁等待时间。通过SHOW ENGINE INNODB STATUS或数据库慢查询日志一般能看到大量锁等待。解决思路是减少热点行更新或者把同一条 SQL 分散到多个分片如果只是压测场景可以考虑在测试数据中把用户按分片表分布减少对同一行记录的竞争。这是被测系统侧的优化但对于性能测试从业者来说你必须能一眼分辨出瓶颈是在压测端还是被压端。5.5 压测结束后的内核参数恢复与结果归档压测完成后有一个经常被省略的步骤恢复压测机的内核参数。如果你在 Worker 上临时调高了fs.file-max和端口范围压测结束后最好把配置改回默认值避免把高并发的临时配置带到生产环境或影响后续其他任务。我一般会先把当前配置快照保存下来压测结束后用快照回滚。结果归档同样重要。压测报告里除了放 RPS 和响应时间表格还要附上集群拓扑、Worker 机器规格、脚本版本、被测系统版本。有次我们为了定位一个诡异的内存泄漏回看几个月前的报告发现当时用的脚本和现在完全不一样浪费了大半天。如果你也遇到类似问题就会明白“把压测环境和脚本版本一起归档”这件事有多重要。最后再分享一个小技巧压测十万级并发不是一次性的任务更建议把整套启动命令、脚本、数据文件、内核配置做成一个 shell 脚本每次执行时自动检查 Worker 机器数量、文件描述符上限、端口范围、带宽情况。我在实际项目里就用这个方式把原本需要半小时的手动准备压缩到一分钟而且出错概率低很多。对你来说最终的目标不只是一次成功的十万并发压测而是能把这次经验沉淀成一个可复用的测试资产以后任何时候需要压测高并发系统都能一键拉起一套稳定的分布式压测环境。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑