真正有效的路径是分层替换阻塞模型、剥离耗时逻辑、让资源复用成为默认行为;必须用Swoole/RoadRunner替代PHP-FPM,开启OPcache+Preload,Redis承担原子并发控制,数据库读写分离+连接池+熔断,并严格传递各层超时。

单靠调优 php.ini 或加几台服务器,撑不住百万级并发。真正有效的路径是分层替换阻塞模型、剥离耗时逻辑、让资源复用成为默认行为。
用 Swoole / RoadRunner 替代 PHP-FPM 是硬性前提
FPM 每请求起一个进程/线程,Linux 默认文件描述符限制(1024)和内存开销直接卡死在几千并发。Swoole 和 RoadRunner 都是常驻内存模型,单进程可持数万 TCP 连接。
- 不要在 FPM 上强行调高
pm.max_children——它只会更快耗尽内存并触发 OOM Killer - Swoole 启动后无须 Nginx 转发,直接监听 80/443;若需共存,用 Nginx 做反向代理到
http://127.0.0.1:9501 - RoadRunner 更轻量,对现有 Laravel/Symfony 项目侵入小,启动命令类似:
rr serve -c .rr.yaml - 注意:Swoole 的
Coroutine\run()内不能用sleep(),必须用Co::sleep(),否则协程调度器会卡住
OPcache + 预加载(Preload)必须开启且配合理
没开 OPcache,等于每次请求都重新编译全部 PHP 文件;不开 Preload,Composer 自动加载仍要走大量文件 I/O —— 这两者在百万并发下会把磁盘 I/O 和 CPU 编译时间推到瓶颈。
- 确认 OPcache 已启用:
php -i | grep opcache.enable输出应为opcache.enable => On - 关键配置项(写入
php.ini):opcache.preload=/path/to/preload.php,并在该文件中用opcache_compile_file()显式加载核心类 - 避免 preload 失败:所有被预加载的文件不能含
__DIR__、__FILE__或运行时动态路径;依赖的扩展(如redis)必须已加载 - Preload 后,
opcache.revalidate_freq=0可关掉运行时校验,但上线新代码需手动 reload PHP 进程
Redis 不只是缓存,更是并发控制的基础设施
当多个请求同时读-改-写同一份数据(如库存扣减),仅靠数据库行锁或乐观锁,在百万并发下会堆积大量等待连接。Redis 的原子操作才是实际承载点。
立即学习“PHP免费学习笔记(深入)”;
- 库存类场景别用
GET + SET,改用DECRBY或EVAL脚本,例如:redis->eval("if redis.call('get', KEYS[1]) >= ARGV[1] then return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end", 1, 'stock:1001', '1') - Session 存 Redis 时,务必设
session.gc_maxlifetime与 Redis key 过期时间一致,否则会积累僵尸 session - 避免用 Redis 做“大对象缓存”:PHP 序列化 + Redis 传输 + 反序列化三重开销明显,高频小字段(如用户权限位图)才适合
- 连接池不是可选——用
predis/predis或phpredis的连接复用机制,禁用短连接
数据库必须读写分离 + 连接池 + 查询熔断
哪怕用了 Swoole,一个慢查询仍会让整个协程挂起。MySQL 单实例扛不住百万 QPS,但更危险的是某条未加索引的 SELECT * FROM logs WHERE user_id = ? 把从库拖垮。
- 读写分离不能只靠中间件(如 MyCat)——应用层也要识别 SQL 类型,
INSERT/UPDATE/DELETE强制走主库,SELECT优先走从库连接池 - 连接池必须带超时和最大连接数限制,例如 Swoole MySQL 连接池设置
max_idle_time=60、max_active=100,防连接泄漏 - 给关键查询加熔断:用
hyperf/circuit-breaker或自研逻辑,连续 5 次查询 > 500ms 就自动降级返回缓存或空数据 - 禁止在循环里查 DB:宁可用一次
IN查 100 条,也不要for循环执行 100 次SELECT
真正卡住百万并发的,往往不是某个技术组件没选对,而是各层之间缺乏超时传递——HTTP 请求设了 3s 超时,但下游 Redis 没设,结果整个协程卡在 redis->get() 上不动。每个 IO 调用都得有明确 deadline,这是最容易被跳过的细节。



















