PHP 怎么水平扩展Web节点

wen PHP项目 4

本文目录导读:

PHP 怎么水平扩展Web节点

  1. 核心架构原则(无状态化)
  2. 代码层面的关键改造
  3. 基础设施层部署方案
  4. 必须解决的问题清单(避坑指南)
  5. 配置示例:完整的最小集群部署(LNMP)
  6. 关键总结:扩展极限与瓶颈
  7. 实操建议

在 PHP 中实现 Web 节点的水平扩展,核心思路是将 PHP 应用从“有状态”变为“无状态”,然后通过负载均衡器将流量分发到多个对等的服务器节点上。

以下是详细的实施指南,分为架构设计代码改造基础设施三个层面。


核心架构原则(无状态化)

水平扩展的前提是:任何一台服务器都能处理任何一次请求。 必须将原本存在本地的“状态”转移到外部共享存储中。

状态类型 原存放位置 扩展后存放位置 主要技术栈
Session(会话) 本地文件 RedisMemcached php-redis 扩展
上传文件 本地磁盘 对象存储 (OSS/S3) 或 NFS 云厂商 SDK
日志文件 本地磁盘 ELKLoki 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/subscribeRedis Stream 处理分布式任务。

必须解决的问题清单(避坑指南)

数据库连接数瓶颈

  • 问题:原来 10 个节点,每个节点 20 个 PHP-FPM 进程,峰值时会建立 200 个数据库连接。
  • 解决:必须引入 数据库连接池(如 ProxySQL / PgBouncer)或提升数据库最大连接数。

缓存不一致

  • 问题:节点 A 写了 file_put_contents(本地缓存),节点 B 读不到。
  • 解决强制杜绝使用本地文件做业务缓存,全部使用 Redis。

队列任务重复消费

  • 问题:订单超时关闭任务,在多个节点被重复执行。
  • 解决
    • 使用 RabbitMQack 机制(手动确认)。
    • 使用 RedisLua 脚本 保证原子性。

部署流程

  • 严禁手动 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;
}

关键总结:扩展极限与瓶颈

随着节点增加,你需要处理以下瓶颈的排序:

  1. 数据库(MySQL/PostgreSQL):最先到达瓶颈,需做主从复制或分库分表。
  2. Redis:如果请求量极大,Redis 会先于 MySQL 达到瓶颈,需部署 Redis Cluster(集群模式)。
  3. PHP-FPM 进程数与 CPU 权衡:水平扩展不是无限加机器,当单机 PHP-FPM 进程数超过 CPU 核心数太多时(1:2 或 1:1),效率反而下降。
  4. 文件系统:如果坚持使用本地磁盘存储 storage/,必须挂载 NFS 或改为云盘。

实操建议

  • 第一步:先改造代码,确保 100% 无状态(Session、日志、上传)。
  • 第二步:部署 Redis,并修改相应配置。
  • 第三步:用 Nginx 负载均衡把所有流量分发到 2 台节点,验证功能。
  • 第四步:接入 CI/CD,实现自动化部署。
  • 第五步:引入 Kubernetes 实现自动弹性伸缩(应对高峰期)。

抱歉,评论功能暂时关闭!