本文目录导读:

在 PHP 中实现 Web 节点的水平扩展,核心思路是将 PHP 应用从“有状态”变为“无状态”,然后通过负载均衡器将流量分发到多个对等的服务器节点上。
以下是详细的实施指南,分为架构设计、代码改造和基础设施三个层面。
核心架构原则(无状态化)
水平扩展的前提是:任何一台服务器都能处理任何一次请求。 必须将原本存在本地的“状态”转移到外部共享存储中。
| 状态类型 | 原存放位置 | 扩展后存放位置 | 主要技术栈 |
|---|---|---|---|
| Session(会话) | 本地文件 | Redis 或 Memcached | php-redis 扩展 |
| 上传文件 | 本地磁盘 | 对象存储 (OSS/S3) 或 NFS | 云厂商 SDK |
| 日志文件 | 本地磁盘 | ELK 或 Loki | Monolog 等 |
| 定时任务 | Cron 本地触发 | 消息队列 或 分布式调度 | RabbitMQ, Sidekiq |
代码层面的关键改造
修改 Session 存储(最关键)
使用 Redis 存储 Session,这样用户无论请求落到哪个节点,都能读取到同一个会话。
// php.ini 配置文件修改(推荐)
session.save_handler = redis
session.save_path = "tcp://10.0.0.1:6379?database=1"
// 或者在代码中初始化(不推荐,容易造成并发问题)
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://10.0.0.1:6379');
代码中的“无状态”编码规范
避免在代码中使用静态变量存储用户数据:
// ❌ 错误示例:存储在本地节点内存,无法跨节点共享
class UserCache {
public static $currentUser = null;
}
UserCache::$currentUser = $db->find($id);
// ✅ 正确示例:使用 Redis 或 Memcached
$cache = new Redis();
$user = $cache->get('user_'.$id);
文件上传处理
必须将上传的文件推送至云存储,不要保存在 public/uploads/ 目录下。
定时任务处理
假设代码中有一个秒杀脚本,只在节点 A 上运行,扩展后,必须确保该脚本不会在节点 B 上重复运行。
- 方案:使用 Redis 分布式锁。
$lockKey = 'cron:send_email'; $lock = $redis->set($lockKey, '1', ['NX', 'EX' => 600]); if ($lock) { // 执行任务 echo "执行中..."; } else { echo "其他节点正在执行,跳过"; }
基础设施层部署方案
方案 A:经典 Nginx 负载均衡(最常用)
适用于传统 FPM 架构(PHP-FPM)。
架构:
用户 -> 负载均衡器 (Nginx/LB) -> Node1 (Nginx + PHP-FPM)
-> Node2 (Nginx + PHP-FPM)
-> Node3 (Nginx + PHP-FPM)
Nginx 配置示例(负载均衡器):
upstream php_nodes {
# 负载均衡算法:ip_hash 可解决部分 Session 问题,但推荐使用 Redis 后改用 round-robin
ip_hash;
server 10.0.0.10:80 weight=1;
server 10.0.0.11:80 weight=2;
}
server {
listen 80;
location / {
proxy_pass http://php_nodes;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
方案 B:容器化与 K8s(现代化)
使用 Docker 构建镜像,通过 Kubernetes(K8s)或 Docker Swarm 编排。
架构:
用户 -> K8s Ingress -> Service 负载均衡 -> Pod (PHP容器)
-> Pod (PHP容器)
优点:
- 自动扩缩容(
HPA):CPU 达到 80% 自动增加 Pod。 - 自动重启:节点宕机自动拉起新 Pod。
方案 C:应用层长连接问题(Swoole/Workerman)
如果你使用 Swoole 或 Workerman 常驻内存模式,由于连接是 TCP 长连接,必须有进程间通信。
- 数据同步:不能使用全局变量跨 Worker 进程传数据,必须用 Redis/MySQL。
- Redis 订阅:使用
publish/subscribe或Redis Stream处理分布式任务。
必须解决的问题清单(避坑指南)
数据库连接数瓶颈
- 问题:原来 10 个节点,每个节点 20 个 PHP-FPM 进程,峰值时会建立 200 个数据库连接。
- 解决:必须引入 数据库连接池(如
ProxySQL/PgBouncer)或提升数据库最大连接数。
缓存不一致
- 问题:节点 A 写了
file_put_contents(本地缓存),节点 B 读不到。 - 解决:强制杜绝使用本地文件做业务缓存,全部使用 Redis。
队列任务重复消费
- 问题:订单超时关闭任务,在多个节点被重复执行。
- 解决:
- 使用 RabbitMQ 的
ack机制(手动确认)。 - 使用 Redis 的
Lua 脚本保证原子性。
- 使用 RabbitMQ 的
部署流程
- 严禁手动
git pull拉扯代码到各节点。 - 解决:使用
Jenkins/GitLab CI/GitHub Actions将代码打包为 Docker 镜像,然后推送到节点运行。
配置示例:完整的最小集群部署(LNMP)
假设你有 2 台应用服务器 (168.1.10) 和 (168.1.11) 和 1 台数据库/Redis (168.1.100)。
应用节点 nginx.conf 配置:
server {
listen 8080; # 使用内部端口,不直接暴露公网
root /var/www/html/public;
index index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000; # 本机 PHP-FPM
include fastcgi_params;
}
}
Redis Session 配置(所有节点一致):
# 修改所有节点的 php.ini session.save_handler = redis session.save_path = "tcp://192.168.1.100:6379?auth=YourPassword"
通过外部 Nginx 负载均衡(避免单点):
upstream backend {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
关键总结:扩展极限与瓶颈
随着节点增加,你需要处理以下瓶颈的排序:
- 数据库(MySQL/PostgreSQL):最先到达瓶颈,需做主从复制或分库分表。
- Redis:如果请求量极大,Redis 会先于 MySQL 达到瓶颈,需部署 Redis Cluster(集群模式)。
- PHP-FPM 进程数与 CPU 权衡:水平扩展不是无限加机器,当单机 PHP-FPM 进程数超过 CPU 核心数太多时(1:2 或 1:1),效率反而下降。
- 文件系统:如果坚持使用本地磁盘存储
storage/,必须挂载 NFS 或改为云盘。
实操建议
- 第一步:先改造代码,确保 100% 无状态(Session、日志、上传)。
- 第二步:部署 Redis,并修改相应配置。
- 第三步:用 Nginx 负载均衡把所有流量分发到 2 台节点,验证功能。
- 第四步:接入 CI/CD,实现自动化部署。
- 第五步:引入 Kubernetes 实现自动弹性伸缩(应对高峰期)。