本文目录导读:

对于PHP项目的压测,单纯地增加并发数(如只压一个API)往往无法发现真实环境的性能瓶颈。复刻线上流量分布特征的核心在于:还原真实请求的多样性、时序分布及链路依赖。
以下是几种从易到难、从粗到细的复刻方案,适用于不同规模的PHP项目(如传统MVC框架或现代Swoole/FPM架构)。
数据驱动:分析Nginx/Apache日志,提取流量“骨架”
这是最基础也是最关键的一步,你需要基于线上日志,统计出以下三个维度的分布:
-
接口调用比例(API Mix)
- 统计过去7天所有API的请求次数占比(
/api/user/info占40%,/api/order/list占30%,/api/search占20%,静态资源占10%)。 - 注意: 排除爬虫、恶意攻击等非正常流量。
- 统计过去7天所有API的请求次数占比(
-
参数值分布(Parameter Distribution)
- PHP应用的性能瓶颈常与参数相关(如不同的
user_id导致数据库缓存命中率不同,或某商品ID下的查询逻辑特别复杂)。 - 统计高频参数的Top N。
/api/product/{id}中的id值分布,或者/search?q=keyword中的关键词热点分布。
- PHP应用的性能瓶颈常与参数相关(如不同的
-
时序特征(Temporal Pattern / 波峰波谷)
- 将一天24小时划分为分钟级切片,统计每个时间切片内的QPS(每秒查询数)。
- 凌晨4点QPS=100,晚上8点峰值QPS=5000,压测时需要通过阶梯式或正弦波模型来模拟这种变化,而不是恒定压力。
工具推荐:
GoAccess(轻量级)、Elasticsearch + Kibana(重量级)、Python Pandas(处理日志CSV文件)。
模型构建:使用流量录制与回放工具(最推荐)
对于复杂的PHP项目(特别是涉及Session、Cookie、POST表单的完整业务流),手动编写脚本难以模拟真实的“用户会话”。
方案A:流量回放工具(L7层,适合全链路)
-
GoReplay — 推荐 这是最简单、侵入性最低的方式,它可以捕获线上网卡的HTTP流量,并转发到压测环境。
# 在线上机器抓取80端口流量,放大2倍,并发送到压测服务器 sudo ./gor --input-raw :80 --output-http "http://staging-server:8080|200%" --http-rewrite-url /api/v1/(.*):/test/v1/$1
- 优势: 100%还原请求头、Cookie、Body及请求顺序。
- 劣势: 可能涉及敏感数据(需配置
--input-raw-track-response时谨慎,或做脱敏处理)。
-
Jmeter + Badboy 录制 使用Badboy或JMeter自带的HTTP(S) Test Script Recorder,设置浏览器代理,手动操作一遍典型业务,适合模拟用户登录 -> 浏览商品 -> 加入购物车 -> 下单的完整链路。
方案B:调用链采样(适合请求链路依赖)
如果你的PHP项目使用了OpenTracing(Jaeger/Zipkin),可以从链路追踪系统中抽取真实的Trace数据,还原请求的上下游顺序和耗时分布。
环境与数据一致性
PHP应用对数据状态极为敏感(读缓存,写DB,Session在Redis/File中的状态)。
-
数据库与缓存预热
- 不要使用空数据库压测,应从线上备份一份脱敏数据到压测环境。
- 关键点: 缓存(Redis/Memcache)也必须预热,直接拷贝线上的RDB/AOF文件到压测Redis,否则第一波请求会全部击穿到数据库,导致压测结果失真。
-
用户身份与状态模拟
- 如果线上依赖Cookie/Session,压测工具(如JMeter)需要添加 HTTP Cookie Manager。
- 对于登录态,不要只压一个登录用户,应准备一个用户池(例如1000个不同
user_id),压测时随机选取,因为如果只用一个用户,PHP的Session锁、数据库的行锁可能不会暴露。
压力模型执行:量化时序特征
假设你的线上流量特征如下:
- 日常QPS:800
- 峰值QPS:3000(持续5分钟)
- 凌晨低谷:50
- 接口比例:
/user/info50%,/order/list30%,/static/*20%
压测脚本示例(使用Locust.io,Python描述):
from locust import HttpUser, task, between
import random
class WebsiteUser(HttpUser):
wait_time = between(1, 3) # 模拟用户思考时间
def on_start(self):
# 随机从一个用户池中选取ID
self.user_id = random.choice(range(1000, 2000))
@task(50) # 权重50%
def user_info(self):
self.client.get(f"/api/user/info?uid={self.user_id}")
@task(30) # 权重30%
def order_list(self):
self.client.get(f"/api/order/list?page={random.randint(1,10)}")
@task(20) # 权重20%
def static_resources(self):
self.client.get("/static/asset.js")
执行策略: 使用 阶梯增加 模型(而非直接100个并发):
- T0-T5min: 启动 100 个用户,模拟日常QPS
- T5-T10min: 逐步增加至 500 个用户
- T10-T15min: 维持峰值(3000 QPS)
- T15-T20min: 逐步下降,观察内存泄漏
PHP特有的压测陷阱与应对
| 线上特征 | 离线/小规模压测常见错误 | 复刻解决方案 |
|---|---|---|
| PHP-FPM 进程数 | 本地只有2个Worker | 压测环境必须与线上Worker数一致(如 pm.max_children=50) |
| OpCache | 没有预热OpCache,每次请求都编译 | 压测前先发几十个请求预热,或直接开启opcache.file_cache |
| Session 锁 | 只用一个用户压,Session无竞争 | 模拟多用户并发操作写Session的接口(如购物车),暴露锁冲突 |
| DB 查询缓存 | 连续压同一个SQL,MySQL完全缓存 | 参数随机化(不同user_id、page),模拟真实SQL命中率 |
| 慢查询 | 只压小数据量 | 压测库的数据量必须与线上一致(几千万行),否则索引不生效 |
| 外部API依赖 | 压测时报错不断 | 依赖Mock或Wiremock,或使用流量回放工具自动处理 |
三步走策略
- 数据采集: 使用
GoAccess或ELK导出线上接口比例、参数分布、QPS时序曲线。 - 工具选型:
- 简单场景(单接口变参):
wrk+ Lua脚本(参数随机化)。 - 中复杂场景(多接口比例):
Locust/JMeter(配置CSV Data Set Config模拟用户池)。 - 极其复杂场景(完整请求还原):
GoReplay,直接回放线上流量。
- 简单场景(单接口变参):
- 校验: 压测时,对比压测环境的API耗时分布(P50/P90/P99)是否与线上接近,如果P99远高于线上,说明模型失真(通常是数据量或缓存未正确预热)。
强烈推荐采用 全量流量回放 方案(GoReplay),它能无死角还原包括请求头、UA、Cookie、POST参数、甚至WebSocket升级握手在内的所有线上特征,且对PHP代码0侵入。