首页 / 资讯中心 / 文章详情

PHP 接口压力测试实战:用 wrk 从零搭建性能基线

PHP 接口压力测试实战:用 wrk 从零搭建性能基线 ★ FEATURED ARTICLE
很多同学第一次给 PHP 项目做压力测试第一反应都是找个工具把接口打爆看看会不会挂。我以前也这么想直到有次新功能上线前我用 wrk 随手压了一套接口发现 QPS 连 200 都没到RT 的 P99 却已经飙到 1.5 秒这才意识到一件事没有一份可靠的 QPS、RT、CPU、内存基线数据你根本没法判断系统是变好了还是变差了。这篇文章就把我从零开始用 wrk 对 PHP 系统做压测、记录基线的完整过程写下来适合完全没碰过压测的 PHP 开发同学照着操作也能帮想搭一套压测流程的人省掉不少弯路。1. 压测前先想明白你测的不是极限是一份可用基线1.1 为什么选 wrk轻量、快、不折腾压测工具不少常见的有 ab、JMeter、wrk、k6 这几类。我给 PHP 项目做日常基线测试首选就是 wrk原因很简单它是一个单文件编译的 C 语言工具没有图形界面没有插件体系一条命令就能跑完一轮压测。对比下来大概是这个样子工具上手成本适合场景主要限制ab很低临时快速看一眼接口吞吐高并发下线程模型不如 wrk数值容易偏低JMeter高复杂业务流程、分布式压测、报告美化配置麻烦跑少量线程时本地资源占用就不小wrk低单接口/多接口的并发压测、基线回归玩法要靠 Lua 脚本层级报告需要自己整理我说的不折腾是有实际体会的。之前我试过在两台测试机上各跑一套压测一边用 ab一边用 wrk同样打到同一个 PHP 接口ab 的 QPS 明显低于 wrk。原因是 wrk 基于非阻塞事件模型可以用少量线程维持大量并发连接压测机自身的 CPU 消耗低很多。换句话说wrk 更容易让压力真正落到被测的 PHP 服务上而不是先把自己的进程拖垮。1.2 压测要记录的四个数字各自代表什么很多人把压测等同于测 QPS但一份合格的基线报告至少要有四个维度的数据QPS每秒请求数系统在特定并发下每秒钟能处理多少个请求这是吞吐量指标。RT响应时间单个请求从发出到收到响应的时间通常看平均值和高分之位比如 P99。CPU 使用率压测期间这台机器到底忙不忙是代码计算密集还是空等 IO。内存占用PHP-FPM 是常驻内存的进程池随着并发上升worker 数量增加内存曲线非常值得盯。这四个数字必须放在一起看缺一个都容易误判。比如 QPS 高了但 CPU 已经 95% 以上说明系统到上限了QPS 没涨内存却一直在涨可能是 PHP-FPM worker 在不断创建或者测试接口本身有泄漏。只有四者对齐才能说这条基线的水位是健康的。1.3 开始之前先确认测试环境是干净的这也是我第一次压测踩过的坑。当时我在本地开发环境上直接压机器上还跑着编译器、数据库脚本、一堆开发工具测出来的 QPS 忽高忽低完全没法当参考。现在我的习惯是压测前必须确认几件事被测代码是同一个稳定分支并记录 commit 号测试机只跑 nginx PHP-FPM 必要的数据库后台监控进程全部关掉压测机的网络和被测机之间没有额外限速或代理PHP 的 opcache 保持与服务一致的开关状态。宁可多花十分钟清理环境也不要拿一份脏数据当基线后面做优化对比时你会感谢当时的谨慎。2. 从零搭环境装好 wrk准备一个能压的 PHP 接口2.1 安装 wrk 的两种常规姿势如果你的测试机是 Linux 系统最简单的办法是直接用包管理器装。以常见的 Debian/Ubuntu 系为例sudo apt install -y wrk wrk --version安装完直接验证版本能输出 wrk 的版本号就说明装好了。有些发行版仓库里没有这个包或者你想用最新版本可以走编译安装sudo apt install -y build-essential libssl-dev git clone wrk 官方源码仓库地址 cd wrk make sudo cp wrk /usr/local/bin/编译之前务必保证有 openssl 的开发头文件wrk 默认会编出支持 HTTPS 的版本如果缺了 libssl-devmake 阶段会报错报错信息通常会直接指向 openssl 相关的头文件。我第一次编译时就在这卡过所以单独提一句。这里多说一句很多 PHP 项目部署时是 Windows 开发环境 Linux 生产环境如果你只在 Windows 上开发建议直接在 Linux 测试机上跑 wrk不要试图在 Windows 下折腾编译。wrk 本身的设计目标就是 Linux/macOSWindows 下的兼容方案既费时间又容易遇到网络栈差异。2.2 一个足够简单又不失真实的测试接口压测接口选什么很有讲究。我的做法是准备两层目标第一层是一个最简单、不查数据库、不调第三方接口的 JSON 返回接口用来测纯 PHP 运行时 PHP-FPM 网络栈的底子第二层再测真实业务接口比如带一个简单的数据库查询观察额外损耗。第一层接口可以长这样?php // test_api.php $data [ code 0, msg ok, data [ user_id 10086, time time(), ], ]; echo json_encode($data);为什么先拿这种接口打底因为压测是为了定位瓶颈。如果连一个不查库的接口 QPS 都很低那问题大概率出在 PHP-FPM 配置、opcache、CPU 频率或中间件上没必要上来就怀疑 SQL。把这个底子摸清楚之后再去压真实接口两者对比就能算出业务逻辑本身消耗了多少吞吐量。部署上我建议走 nginx PHP-FPM 的标准组合监听 127.0.0.1:8080 这样的内网地址避免在压测过程中被防火墙或路由影响。测试机 IP 用 127.0.0.1 还是内网 IP 都行但如果你想让压测机和被测机分离就要用被测机的内网 IP 访问。2.3 第一条压测命令和输出解读把接口部署好之后先用 curl 手动确认返回正常然后再上 wrk。第一条命令建议放低并发跑通流程wrk -t2 -c10 -d10s --latency http://127.0.0.1:8080/test_api.php其中-t2表示用 2 个线程-c10表示维持 10 个并发连接-d10s表示持续 10 秒--latency会在结束时输出更完整的延迟分布。正常跑完会看到类似这样的输出Running 10s test http://127.0.0.1:8080/test_api.php 2 threads and 10 connections Thread Stats Avg Stdev Max /- Stdev Latency 4.82ms 2.31ms 45.12ms 86.18% Req/Sec 1.05k 102.34 1.41k 70.31% Latency Distribution 50% 4.50ms 75% 5.20ms 90% 6.80ms 99% 14.20ms 10520 requests in 10.01s, 2.03MB read Requests/sec: 1051.12 Transfer/sec: 207.43KB这里最容易被新手误读的地方是 Thread Stats 里的 Req/Sec 和最后的 Requests/sec。Thread Stats 的 Req/Sec 是每个线程自己的平均吞吐如果你的线程数是 4那么这个数值大致是总 QPS 分割后的水平而最后的Requests/sec才是整体 QPS也就是我们基线里要记的那个数字。把这两行搞混的人不少我第一次看输出时也愣了半天。3. 正式压测并发要加梯度数据要取多轮3.1 从低并发起步找到吞吐量的拐点压测不是一上来就-c500那样你只能得到一个打爆了的结果却不知道系统是从哪个并发开始扛不住的。我的标准操作是用一组并发梯度比如-c10、-c50、-c100、-c200、-c500每个梯度跑 30 秒记录对应的 QPS 和 RT。这个过程本质上是寻找拐点一开始并发上升QPS 会跟着涨因为同时能处理的请求变多了到某个点之后再往上加并发QPS 不再明显增长甚至开始回落同时 RT 快速拉高。这个拐点对应的并发量就是系统当前配置下的合理服务水位。举个实际例子我之前压一个 PHP 8.2 opcache 的简单 JSON 接口-c10时 QPS 约 1800-c50时涨到 4200-c100时是 5100-c200的时候只有 4600 了RT 的 P99 从 18ms 涨到 120ms。这就说明 100 左右并发已经接近这台机器的服务上限继续压只会让用户体验变差。3.2 平均 RT 会欺骗你P99 才是真实用户体验wrk 输出里的 Latency 一栏Avg是平均响应时间但平均值的迷惑性很大如果 90% 的请求都是 5ms剩下 10% 是 500ms平均值会被拉到一个看起来很温和的数字。这也是为什么压测一定要加--latency参数看延迟分布里的 50%、75%、90%、99% 分位。P99 的含义是99% 的请求都低于这个值剩下的 1% 是最慢的尾部请求。对线上服务来说用户体验往往由尾部延迟决定。我在做基线记录时RT 这一栏至少记两个数平均值和 P99。后续做优化时如果平均值下降了但 P99 在上涨我会立刻警惕——这通常意味着某些慢请求变得更极端了可能存在于锁竞争或进程池打满的问题。3.3 记录多轮结果取稳定而不是最高的一轮压测数值天然有波动尤其是 PHP-FPM 场景下进程复用、opcache 冷热、系统定时任务都可能让某一轮结果偏高或偏低。我的做法是同一个并发梯度至少跑三轮每轮之间停 5 秒把三轮结果都记下来然后取中位数或者波动最小的那一轮作为基线值。这里有个反直觉的点最高值往往不值得记录。比如某轮跑到 5000 QPS下一轮只有 4100我会直接怀疑第一轮是不是正好命中了 opcache 预热完成的窗口或者系统 GC 还没触发。真正的基线应该是可复现的那一组数字不是偶尔出现的峰值。如果接口需要 POST 请求wrk 也能通过 Lua 脚本解决。基础写法大概是这样-- post.lua wrk.method POST wrk.headers[Content-Type] application/json wrk.body {user_id:10086,page:1}然后用wrk -t4 -c100 -d30s -s post.lua http://...启动即可。这个用法不难但注意脚本里的 body 是所有并发连接共用同一个请求体如果业务要求每个请求参数不同就得用 Lua 脚本在请求时动态生成复杂度会高不少一般做基线时不需要一步到位。4. 压测时盯服务器CPU 和内存基线的采集方法4.1 压测期间要开的几个监控命令wrk 跑起来只是整个压测的一半另一半是被测服务器的资源状态。我通常会在压测开始前先在被测机上开一个后台记录脚本把压测期间的数据落盘这样压测结束后可以回放当时的资源曲线。最常用的命令有这几个# 每2秒输出一次系统概况共记录30次 vmstat 2 30 # 每2秒输出一次每个CPU核心的使用率 mpstat -P ALL 2 30 # 查看每个进程的CPU和内存占用 pidstat -u -r 2 30 # 内存概况 free -m如果不想同时开这么多终端可以用一个简单的循环脚本把关键数据拼进同一个日志文件#!/bin/bash # load_monitor.sh interval2 end$((SECONDS60)) while [ $SECONDS -lt $end ]; do echo $(date %F_%T) /tmp/load_test.log top -bn1 | head -15 /tmp/load_test.log free -m /tmp/load_test.log sleep $interval done注意pidstat这个命令来自 sysstat 工具包如果系统里没有需要先apt install sysstat。mpstat 也一样。另外这些监控命令本身也会消耗少量 CPU在低配机器上可能影响测试结果所以记录完后记得把这些进程都杀掉再取最终数据。4.2 从监控数据反推瓶颈在哪个环节拿到资源数据后重点看三个地方一是 CPU 的 us/sy 比例。us高说明用户态代码在忙比如 PHP 脚本逻辑复杂、循环密集sy高说明系统态开销大比如频繁上下文切换、网络包处理、系统调用太多。如果 sy 高于 us往往不是 PHP 代码本身的问题而是并发模型或网络配置有瓶颈。二是 CPU idle 和等待 IO 的时间。vmstat里有个 wa 列代表 IO 等待。wa 明显偏高的时候QPS 上不去多半是磁盘拖后腿比如 nginx 访问日志疯狂写盘或者 PHP slowlog 开着。我之前确实遇到过 CPU 只有 20%、QPS 却卡在 800 的情况检查后发现是访问日志写到了普通机械盘上IO 等待直接打满。三是内存里 PHP-FPM 的占用。用ps aux --sort-%mem | head -20看看是否有 worker 进程内存涨得异常。PHP-FPM 的每个 worker 在默认配置下可能占用几十到一两百 MB如果并发开得高几十个 worker 同时驻留内存很容易超预期。内存基线就是用来记录这个并发水位下FPM 进程池到底吃掉了多少内存。4.3 把服务端数据和 wrk 输出对齐到同一时间线压测结束后你会得到两堆数据一堆来自 wrk 的汇总值一堆来自监控日志的每秒采样。很多人直接把这两堆数字拼在一起写报告实际上不对齐时间线的基线是没法定位问题的。我的做法是压测开始前在监控日志里手动打一条标记 start wrk 结束后再打一条 done 这样压测窗口内的资源采样就有明确的起止边界。后续做对比时只看窗口内的采样平均值和峰值窗口外的数据直接丢弃。然后我习惯建一张映射表把每次压测的命令参数、wrk 结果、资源监控结果放在同一行这样一旦某次基线异常可以立刻倒推是哪一层的变量发生了变化。5. 把基线写成表格一份能长期复用的记录模板5.1 基线表该有哪些字段跑完一堆压测之后如果只是把数字记在聊天软件里过两周就找不到了。我推荐用一张表格管理所有基线字段包括日期代码版本测试接口并发QPSRT 均值RT P99CPU%内存占用环境说明2026-06-01某提交号test_api.php100510018ms46ms68%1.8Gopcache开启FPM动态10-802026-06-02某提交号test_api.php200460088ms120ms66%2.4G同上超负荷区间2026-06-03某提交号user_info.php5062076ms210ms35%2.1G带一次DB查询这张表里每一行就是一条基线。环境说明这栏特别重要因为 opcache 开关、PHP 版本、FPM 参数都会直接影响结果不写清楚三周后再看数据等于瞎猜。5.2 什么时机需要重新测基线基线不是测一次就永久有效的。按我的经验以下变化发生时必须重新采集PHP 版本升级、框架或核心依赖有大版本变更、php-fpm 池配置调整pm.max_children、pm.start_servers、nginx 的 keepalive 或 gzip 策略变化、测试接口本身的代码逻辑改动、测试机硬件规格变化。如果出现同一接口、同一代码版本QPS 却和上周记录差很多的情况我也会重新测一遍基线而且会特意检查是不是监控脚本在后台偷跑、是否有人改了系统参数。这个习惯帮我排查过好几次假象——有一次 QPS 突然掉了一半最后发现是某台机器上被同事装了定时任务压测窗口正好撞上了大规模日志清理。5.3 用基线做容量预估的简单算法基线最有价值的用法不是看数字好看而是用来做容量预估。公式不复杂需要实例数 (预估峰值 QPS × (1 冗余系数)) / 单机基线 QPS举个例子基线表里单机在 100 并发下 QPS 是 5000如果业务方预测大促峰值请求是 15000 QPS目标 CPU 水位控制在 70% 左右那么单机预留 30% 冗余最终需要的实例数就是15000 × 1.3 / 5000 ≈ 4台。如果只按 QPS 峰值算而不留冗余一旦流量抖动所有实例都会冲到 90% 以上的 CPU尾部延迟会很难看。需要注意的是这种线性预估只适用于无状态、可水平扩展的 PHP 接口。如果接口严重依赖数据库或第三方服务那么数据库侧的连接数、负载也是容量边界单纯按单机 QPS 算会低估风险。遇到这种情况我会同时把数据库的 CPU、连接数也拉进基线表。6. 实操中最容易翻车的五个坑6.1 压测机先崩了测出来的数就不可信wrk 虽然很轻量但也不是无限跑。如果你用一台 2 核小机器去压一个 8 核的服务端很可能会出现压测机 CPU 先到 100%而服务端还很从容的情况。这时的 QPS 数字反映的是压测机的上限不是被测系统的上限。判断方法很简单压完看压测机的top如果 wrk 进程吃满了 CPU 或者系统出现大量 TCP 连接排队就说明压力工具先到极限了。解决方式一是减少 wrk 线程数通常-t设置为压测机 CPU 核心数左右即可没必要开太多二是把 wrk 放到另一台闲置机器上跑让压测机和被测机分离。6.2 PHP-FPM 配置和 opcache 偷偷影响成绩PHP 场景下最容易被忽略的两个变量是 opcache 和 FPM 进程池参数。opcache 关闭时每个请求都要重新编译一遍 PHP 脚本CPU 消耗会明显上升QPS 直接打折opcache 开启后前几个请求还会触发编译缓存所以正式压测前一定要先打几发请求做预热。FPM 这边pm.max_children如果设置太小并发一旦超过进程池上限请求就开始排队QPS 停滞、RT 暴涨如果设置太大内存会被 worker 吃满。我做基线时会把 FPM 配置快照写进环境说明这样后续调整参数后对比才公平。另外注意pm.max_requests它是让 worker 处理多少请求后自动回收的机制设得太小会导致进程频繁创建销毁CPU 忽高忽低。6.3 长连接与并发数设置会带来假数据wrk 默认使用 HTTP/1.1 keep-alive 长连接这能大幅提高 QPS因为省去了大量 TCP 握手开销。但真实用户往往不是这种访问模式连接会不断建立和断开。所以压测结果必须注明是否 keep-alive。如果你想知道断开连接场景下的表现可以在 wrk 里加一个请求头wrk -t4 -c100 -d30s -H Connection: close http://127.0.0.1:8080/test_api.php你会看到 QPS 明显下降这是正常的别慌。关键是基线表里要写清楚当时是哪种连接模式否则拿 keep-alive 的基线去对比短连接场景你会以为系统退化了。6.4 日志和磁盘 IO 拖垮整个压测压测期间nginx 的 access log 默认每接收一个请求就写一行。QPS 高的时候日志写入本身就是巨大的磁盘压力。更隐蔽的是 PHP-FPM 的 slowlog一旦触发慢日志就会在请求处理路径上产生额外 IORT 反而被日志拖得更慢。我的建议是做纯性能基线时先把 access_log 临时关闭或在配置里指向内存盘压测完再改回来。这个操作不算什么高级技巧但特别实用。我第一次压测 2000 QPS 时就发现磁盘 wait 占到 40%所有并发一上去 CPU 就在等 IO关掉访问日志后 QPS 直接翻了一倍。基线测试测的是系统处理能力不应该让日志写入变成隐形成本。6.5 压完不收拾现场下一次基线直接失真压测结束后的收尾工作同样重要。需要做的包括停掉压测期间的监控脚本确认没有残留的压测进程在跑清掉压测产生的日志文件和临时脚本如果临时改过 FPM 参数或 nginx 日志开关记得恢复原状把压测时记录的代码 commit 号和配置快照归档。这些看起来琐碎但直接影响下一次压测是否可信。我之前有一回压完就撤了第二天再测时发现系统后台还挂着监控脚本测出来的 CPU 基线高了十几个百分点白白排查了一下午。从那之后我就养成了压测结束三分钟收尾的习惯宁可慢一点也不能让基线数据带伤入库。最后再分享一个经验压测基线不是给上级看的一张表它是你自己理解系统的一把尺子。同一个接口在不同并发下的表现、不同配置下的差异、代码提交前后的吞吐变化都藏在基线的对比里。把这套流程跑顺之后你会发现自己对系统到底能扛多少量这件事终于有了基于数据的底而不是靠猜。
阅读完成 · 觉得有帮助?
咨询建站