早年做跨境电商项目,服务器跑在 AWS 上,Apache + PHP 的经典组合。流量上来之后服务器经常 504,排查了一圈发现是 Apache 配置和 PHP 运行模式的问题。今天把那次调优的完整过程记录下来,给还在用 Apache + PHP 的朋友做个参考。
一、问题现象
项目是雅宝路中俄跨境电商平台,跑在 AWS 小型集群上,前端 Apache,后端 PHP + MySQL。大促期间 QPS 到 500 左右就开始大量 504,服务器 load average 飙到 20+。
先看了下 Apache 的错误日志:
很明显,Apache 的 worker 数不够用了。但这只是表象,直接调大 MaxRequestWorkers 可能会让问题更严重——内存不够用导致 OOM。
二、MPM 模式选择
Apache 有三种 MPM(Multi-Processing Module)模式:prefork、worker、event。
2.1 三种模式对比
| 模式 | 进程模型 | 线程模型 | PHP 兼容性 | 适用场景 |
|---|---|---|---|---|
| prefork | 多进程 | 无线程 | 最好(非线程安全) | 稳定优先,低并发 |
| worker | 多进程 | 多线程 | 需要 ZTS 版本 | 中高并发 |
| event | 多进程 | 多线程 + 异步 | 需要 ZTS 版本 | 高并发,长连接多 |
我们的项目用的是 mod_php,PHP 是非线程安全版本,所以只能用 prefork 模式。这也是性能瓶颈的根源之一——prefork 每个请求占一个进程,进程创建和销毁的开销大,内存占用也高。
2.2 prefork 模式配置
先看当前配置:
| |
MaxRequestWorkers 只有 150,QPS 500 的情况下肯定不够。但直接调大之前,得先算一下内存够不够。
2.3 计算合理的 MaxRequestWorkers
先看单个 Apache 进程的内存占用:
| |
我们的环境单个 httpd 进程大约占 80MB(因为 mod_php 把 PHP 解释器嵌进去了)。服务器内存 8GB,系统和 MySQL 占了 3GB,留给 Apache 的大约 4GB。
等等,这不对,当前配置是 150,但实际内存没爆。因为不是所有进程都同时在处理请求,空闲进程的内存占用会低一些。实际峰值时同时活跃的进程数大约是 80-100。
合理的配置应该是:
| |
关键点:
StartServers调高到 10,避免启动后瞬间创建进程的开销MaxRequestWorkers调到 256,留一定余量,但不能超过内存承载能力MaxConnectionsPerChild设为 1000,每个进程处理 1000 个请求后自动重启,防止内存泄漏
MaxConnectionsPerChild 这个参数很重要。PHP 有内存泄漏的老问题,长时间运行的进程内存会慢慢涨,设个上限自动回收是最稳妥的方案。
三、PHP-FPM 模式迁移
prefork + mod_php 的组合内存占用太高,后来我们迁移到了 Apache + PHP-FPM 的模式。PHP-FPM 是独立的进程池,Apache 只负责处理 HTTP 请求,PHP 解析交给 FPM。
3.1 配置 Apache 使用 FastCGI
| |
3.2 PHP-FPM 进程池配置
| |
PHP-FPM 的进程数计算和 Apache 类似,但单个 FPM 进程的内存占用比 mod_php 低一些(大约 50-60MB),因为不需要加载 Apache 的模块。
pm.max_requests = 1000 和 Apache 的 MaxConnectionsPerChild 同理,防止内存泄漏。
slowlog 和 request_slowlog_timeout 是排查慢请求的利器,超过 3 秒的请求会记录调用栈,能快速定位性能瓶颈。
3.3 迁移后的效果
迁移到 PHP-FPM 后:
- 单个 Apache 进程内存从 80MB 降到 15MB(只处理静态资源和反向代理)
- 同样 4GB 内存,Apache 可以跑到 200+ 并发连接
- PHP-FPM 进程池独立管理,PHP 崩溃不影响 Apache
- 整体 QPS 承载能力从 500 提升到 1200+
四、Opcache 优化
PHP 是解释型语言,每次请求都要重新编译 PHP 脚本为 opcode 再执行。Opcache 把编译后的 opcode 缓存到共享内存,避免重复编译,性能提升非常明显。
4.1 Opcache 配置
| |
关键参数解释:
memory_consumption=256:Opcache 共享内存大小,项目大的话给 256MBmax_accelerated_files=10000:最多缓存多少个 PHP 文件,要大于项目文件总数revalidate_freq=60:多少秒检查一次文件是否更新,生产环境可以设大一些validate_timestamps=1:是否检查文件更新时间,开发环境必须开,生产环境可以关
4.2 验证 Opcache 生效
写个 opcache.php 查看状态:
| |
命中率应该在 95% 以上才正常。如果命中率低,可能是 max_accelerated_files 不够,或者 revalidate_freq 太短。
4.3 Opcache 踩坑
1. 代码更新不生效
生产环境如果 validate_timestamps=0,代码更新后 Opcache 不会自动刷新,需要重启 PHP-FPM 或者调用 opcache_reset()。我们的发布流程里加了一步:发布完成后调用 opcache_reset() 清缓存。
2. 内存不足导致缓存失效
memory_consumption 设太小,Opcache 内存满了之后会清空所有缓存重新开始,命中率会骤降。一定要根据项目大小给足够的内存。
3. CLI 模式不要开 Opcache
opcache.enable_cli=0,CLI 模式下每次执行都是独立进程,Opcache 没有意义,反而占内存。
五、其他优化项
5.1 Apache 配置优化
| |
5.2 PHP 配置优化
| |
5.3 MySQL 连接优化
PHP 应用里 MySQL 连接池很重要。用 PDO 持久连接或者中间件(如 ProxySQL)减少连接创建开销:
| |
但持久连接要小心事务和连接状态泄漏,使用不当会出诡异的问题。我们后来用了 ProxySQL 做连接池,更稳定。
六、调优前后对比
| 指标 | 调优前 | 调优后 | 提升 |
|---|---|---|---|
| 最大 QPS | 500 | 1200+ | 140% |
| P99 延迟 | 1200ms | 350ms | 71%↓ |
| 内存使用率 | 85% | 65% | 20%↓ |
| 504 错误率 | 5% | 0.1% | 98%↓ |
| Opcache 命中率 | 未开启 | 97% | - |
七、总结
Apache + PHP 的性能调优,核心就几个点:
- MPM 模式选对:prefork 配 mod_php 简单但内存占用高;PHP-FPM 模式更灵活,资源利用率更高
- 进程数算清楚:根据内存和单进程占用计算合理的 MaxRequestWorkers / max_children,不是越大越好
- Opcache 必开:PHP 性能提升最明显的手段,没有之一,命中率要保证 95%+
- 防止内存泄漏:MaxConnectionsPerChild / max_requests 设上限,进程自动回收
- 静态资源优化:gzip 压缩 + 浏览器缓存,减少动态请求压力
这套方案虽然是几年前的经验,但 Apache + PHP 的组合现在还有很多团队在用,核心原理是相通的。后来迁移到 Nginx + PHP-FPM 后,性能又上了一个台阶,但 Apache 的调优经验帮我打下了扎实的基础。
性能调优是个持续的过程,没有终点。每次业务迭代、数据量增长,都可能带来新的瓶颈。保持对监控数据的敏感,定期 review,是每个后端工程师的基本功。