
1. 先判断你的PHP项目处于哪个需要负载均衡的阶段我见过太多团队一上来就急着上负载均衡结果架构搭得挺漂亮问题却一个没解决。也有反过来的服务器已经卡成幻灯片了还在死撑单机觉得上负载均衡太麻烦。我的经验是先判断清楚自己处于什么阶段再决定要不要做、怎么做。1.1 单机扛不住的三个典型信号PHP应用不管是ThinkPHP、Laravel、WordPress还是裸写原生代码在单机上跑资源消耗会依次出现三个瓶颈。你不需要等到全部出现才行动但至少要能识别出它们CPU持续打满用top或htop观察如果CPU使用率持续在85%以上而MySQL、Redis都在同一台机器上说明PHP-FPM的进程调度已经接近极限。这时候加一台服务器做负载均衡比单纯调pm.max_children更治本。PHP-FPM进程池耗尽访问php-fpm.status需要在php-fpm配置里开启pm.status_path如果max_children经常被占满listen queue不断上涨说明进入的请求已经超过单机处理能力。此时会开始出现502 Bad Gateway这是PHP负载均衡最直接的触发点。响应时间分层恶化同一个接口在凌晨和晚上响应时间差出好几倍说明白天高峰时段计算资源被抢。这种时好时坏的体验比直接报错更伤用户信任。这三个信号里第一个和第三个其实是慢第二个是挂。负载均衡能同时缓解这两种情况但它不是万能药——如果你的瓶颈在MySQL单库本身负载均衡只是帮你把问题往后推了一步而已这点后面会细说。1.2 无状态是福也是坑PHP负载均衡的特殊性对比Java常驻进程、Node.js事件循环这类长生命周期服务PHP-FPM是典型的短请求模型——一个请求进来PHP-FPM进程执行完就释放进程本身不保留跨请求的上下文。这个特性对负载均衡特别友好理论上任何请求都可以被任意一台PHP-FPM处理因为PHP进程本身没有状态。但理论上三个字就是坑的开始。PHP进程没状态不代表应用没状态。坐在浏览器对面的用户是有状态的登录了没有、购物车放了什么、上传到一半的文件存哪了。这些状态PHP默认存在哪里答案会让你冷汗直流的——本地文件系统。Session默认写到/tmp或session.save_path对应的服务器本地目录上传的临时文件写到upload_tmp_dir日志写到本地磁盘Opcache缓存也是每个PHP-FPM进程池各自独立的一份。所以当你从1台机器扩到3台机器如果没有做配套改造用户的请求被负载均衡分发到B机器而Session却在A机器上——用户明明登录了却永远处于未登录状态。这就引出一个核心结论PHP负载均衡技术表面上是流量分发问题本质上是状态共享问题。文章后面大半篇幅都在处理这个事。2. 选型对比DNS、LVS、HAProxy、Nginx该用哪一层很多初学者把负载均衡理解成多搞几台服务器前面加个东西转发但加个东西有四种常见做法工作在完全不同的网络层次各有各的适用场景。我直接给你一张对比表这是我在项目里反复权衡后整理出的PHP场景参考。方案工作层次核心原理PHP场景适用性主要局限DNS轮询应用层域名解析同一个域名解析到多个IP浏览器轮流访问低只能做粗糙分流无法感知后端存活缓存TTL导致切换慢LVS四层传输层在内核态改写IP报文转发到后端高适合超大流量入口不支持应用层判断配置门槛略高HAProxy四层/七层用户态代理可做TCP或HTTP级分发高控制精细、健康检查强大自身需高可用保护Nginx七层应用层反向代理按HTTP内容分发最高与PHP-FPM协作天然大并发时自身CPU消耗高于LVS2.1 四层与七层的本质区别四层负载均衡LVS工作在TCP/UDP层面它拿到一个连接请求看到的是IP和端口直接决定把这个TCP连接转给哪台后端。它不关心你传的是HTTP还是WebSocket也不看URL路径。七层负载均衡Nginx/HAProxy则要拆开HTTP请求读到URL、请求头、Cookie这些内容再决定转发策略。它的好处是可以做精细化路由——比如/api/*转给A组PHP-FPM/admin/*转给B组坏处是每个请求都要在用户态多解析一层性能天花板低于LVS。用生活化的方式理解四层像快递分拣中心只看包裹上写的城市码按城市扔到对应的传送带七层像门店导购还得拆开包裹看看里面是什么商品再决定送哪个柜台。分拣效率肯定是前者高但后者能做的事多得多。2.2 PHP场景下的选型建议我在实际项目里的取舍经验如下请求量在每秒几百到一两千直接用Nginx就够了。这是90%以上的PHP项目所处的区间。Nginx既可以做用户入口的反向代理又可以直接用fastcgi_pass把请求转给PHP-FPM少一层组件就少一份维护负担。请求量明显过万或者Nginx自身成为瓶颈时前端再加LVS。LVS的转发在内核完成吞吐量远高于Nginx适合做最顶层的流量入口。此时架构变成LVS → Nginx集群 → PHP-FPM集群。如果后端不是纯PHP比如混合了Java服务、需要按内容做精细切换HAProxy比LVS更灵活。HAProxy的use_backend规则能按URL前缀、请求头甚至Cookie内容分流运维起来也更直观。DNS轮询基本只适合做多机房入口这种粗粒度分流不适合单独充当PHP集群的负载均衡器因为DNS缓存会让故障切换变得不可控。记住一个原则架构的层级不是越多越好。每多加一层就多引入一个故障点和一份延迟只有当现有层级确实成为瓶颈时才考虑加新的一层。3. 最常见方案落地Nginx反向代理负载多台PHP-FPM如果你打算从单机往集群走我强烈建议先把Nginx这套吃透。它是最常用、最容易排错、也最能覆盖业务初期的方案。3.1 最基本的upstream配置在Nginx配置里负载均衡的核心是upstream块。看一个标准例子upstream php_backend { server 192.168.1.10:9000 weight3 max_fails2 fail_timeout10s; server 192.168.1.11:9000 weight1 max_fails2 fail_timeout10s; keepalive 32; } server { listen 80; server_name www.example.com; location ~ \.php$ { include fastcgi_params; fastcgi_pass php_backend; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /data/wwwroot/example$fastcgi_script_name; } }这里有几个参数需要你真正理解它的含义weight权重3:1指的是每4个新请求里大约3个给第一台、1个给第二台。权重应该按机器实际性能设置不要拍脑袋。max_fails与fail_timeout在10秒内如果这台服务器失败2次Nginx会在接下来的10秒内认为它宕机不再转发请求。这是最基础的健康检查机制。keepalive 32保持和后端的空闲长连接数。这个对PHP-FPM的listen队列压力有明显改善因为省去了频繁创建TCP连接的开销。但注意keepalive要和PHP-FPM侧配合PHP-FPM需要开启listen.backlog同时你的fastcgi_pass参数不能漏掉fastcgi_keep_conn on;。3.2 fastcgi_pass与proxy_pass两种转发路径这是PHP负载均衡里特别容易让人困惑的点。Nginx转发给PHP-FPM有两种做法第一种Nginx直接连接PHP-FPM就是上面示例的样子fastcgi_pass指向php_backend这个upstream里的9000端口。适用于后端服务器只装了PHP-FPM、没有额外Nginx的情况。此时Nginx负责处理静态文件.php请求直接交给PHP-FPM执行。第二种Nginx代理到后端Nginx后端每台服务器自己装一套完整的LNMPNginxPHP-FPM前端Nginx用proxy_pass把请求原封不动转发给后端Nginx由后端Nginx再通过自己的fastcgi_pass交给本地PHP-FPMupstream web_backend { server 192.168.1.10:80 weight1; server 192.168.1.11:80 weight1; } location / { proxy_pass http://web_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }第二种多了跳转发延迟略高但好处很明显后端Nginx可以在本机继续承担静态文件服务、或者做后端自己的安全规则前后端配置解耦。我以前在一个老项目里用第一种方案后来需要在后端加访问白名单加在php-fpm层面很别扭最后切换到第二种才理顺。我的建议是初期用第一种结构简单、链路短、问题好定位后端服务器数量超过5台、或者需要每台后端独立做Nginx层规则时换第二种。3.3 分流策略与健康检查怎么配Nginx upstream默认是轮询round-robin还有几个常用策略upstream php_backend { least_conn; # 转发给当前连接数最少的后端 # ip_hash; # 同一IP固定到同一后端 server 192.168.1.10:9000; server 192.168.1.11:9000; }轮询适合后端机器配置大致相同的场景实现简单。least_connPHP-FPM请求耗时有长有短如果出现某些慢请求占据进程least_conn能避免新请求全堆到一台机器上。我个人在PHP场景下更偏好这个策略。ip_hash同一客户端IP固定转发到同一后端。它能解决Session问题但副作用是某个IP下的用户会被绑死在一台机器上这台机器挂了这部分用户就完全不可用。只建议在无法改造Session、且后端机器强依赖本机状态的过渡期使用。关于健康检查还有个容易踩的细节max_fails只能感知连接失败和超时不能感知PHP进程活着但应用层已经挂了。比如PHP-FPM可以正常接受连接但业务代码因为数据库故障导致每个请求都报500Nginx依然认为节点是健康的。要让负载均衡真正智能需要给upstream配置主动探活。Nginx开源版没有内置HTTP主动健康检查最简单的方案是加一层HAProxy或者在每台后端机器上放一个/health.php探活脚本再用定时任务或外部监控来摘除故障节点。4. 高可用形态LVS/HAProxy Keepalived架构实测当单台Nginx也顶不住流量或者你希望负载均衡器自身不会成为单点就得引入LVS或HAProxy并且给它们配上高可用。4.1 LVS DR模式部署要点LVS有NAT、DR、TUN三种模式PHP场景下DR模式最常用。DR模式的核心原理是LVS只改写请求报文的目标MAC地址后端服务器直接响应客户端响应流量不经过LVS因此吞吐量极高。代价是后端服务器需要配置VIP虚拟IP并关闭对外广播。部署时几个关键点LVS调度器配置以ipvsadm为例# 在两台LVS机器上都配置VIP ip addr add 192.168.10.100/24 dev eth0 # 在主导调度器上配置转发规则 ipvsadm -A -t 192.168.10.100:80 -s wrr ipvsadm -a -t 192.168.10.100:80 -r 192.168.1.10:80 -g -w 3 ipvsadm -a -t 192.168.10.100:80 -r 192.168.1.11:80 -g -w 1后端Real Server上配置VIP并抑制ARP# 在Loopback接口绑定VIP ip addr add 192.168.10.100/32 dev lo # 关闭ARP广播防止后端机器抢答VIP echo 1 /proc/sys/net/ipv4/conf/lo/arp_ignore echo 2 /proc/sys/net/ipv4/conf/lo/arp_announce echo 1 /proc/sys/net/ipv4/conf/all/arp_ignore echo 2 /proc/sys/net/ipv4/conf/all/arp_announce这两个抑制ARP的命令是LVS DR模式下最常见的配置失误点少了一条后端的ARP应答会干扰VIP的正常访问现象就是访问时通时不通。Real Server的Nginx端口LVS转发的目标端口是80意味着每台后端机器都得有Nginx在80端口监听。哪怕你的PHP业务只走9000端口DR模式下LVS还是会把HTTP请求交给后端的80端口Nginx再由它转发给PHP-FPM。4.2 HAProxy在PHP场景的亮点配置HAProxy是我个人最喜欢用在PHP场景的负载均衡器因为它健康检查做得非常成熟。除了TCP层面的端口探测还能做HTTP层面主动探活defaults mode http timeout connect 5s timeout client 30s timeout server 30s frontend php_in bind *:80 default_backend php_pool backend php_pool balance roundrobin option httpchk GET /health.php HTTP/1.1\r\nHost:\ www.example.com server php1 192.168.1.10:80 check inter 3s fall 3 rise 2 server php2 192.168.1.11:80 check inter 3s fall 3 rise 2option httpchk GET /health.php让HAProxy每3秒请求一次后端Nginx上的health.php连续失败3次就摘除节点连续成功2次恢复。这个逻辑比Nginx自带的被动健康检查精准得多建议新建集群时优先考虑HAProxy。health.php建议写得实用一些?php try { $pdo new PDO(mysql:host127.0.0.1;dbnametest, user, pass, [PDO::ATTR_TIMEOUT 2]); echo ok; } catch (Throwable $e) { http_response_code(500); echo db error; }注意探活脚本不只是我活着而是要检查关键依赖是否可用。最典型的就是数据库——如果MySQL挂了但探活脚本只看PHP进程存活负载均衡还会持续往这台机器打流量用户看到的错误率并不会因为加了集群而改善。4.3 Keepalived实现调度器自身高可用无论LVS还是HAProxy调度器自己都不能是单点。Keepalived的职责是在两台调度器之间漂移一个VIP正常情况下VIP落在主节点上主节点挂了副节点自动接手两台之间通过VRRP协议心跳通信。一个最小可用的Keepalived配置长这样global_defs { router_id LVS_MASTER } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass yourpass } virtual_ipaddress { 192.168.10.100 } }副节点配置几乎相同只需把state改成BACKUP、priority降到90。这里要提醒一个小细节advert_int默认1秒如果两台机器之间有丢包抖动可能触发频繁切换。生产环境建议把心跳间隔调到2秒配合nopreempt参数避免主节点恢复后反复抢占导致连接抖动。5. 负载均衡上线前必须完成的PHP配套改造很多团队踩坑不是在负载均衡本身而是忽略了PHP应用的隐藏状态。我按优先级排序这四项改造必须在流量切过去之前做完Session集中化、上传共享、代码同步、缓存整合。5.1 文件Session在多机环境下的灾难与Redis接管PHP默认的Session是文件存储文件写到session.save_path指定目录。单机没问题一旦负载均衡把请求分到多台机器问题立刻暴露第一台机器写入了Session文件第二台机器读不到用户就被当成未登录了。最彻底的解决方式是把Session搬到一个所有PHP进程都能访问的集中存储里。现在主流方案是Redis; php.ini 配置 session.save_handler redis session.save_path tcp://192.168.10.50:6379?timeout2read_timeout2database0 session.gc_maxlifetime 1440Laravel、ThinkPHP等框架也都有自己的Session驱动以ThinkPHP6为例在config/session.php里配置type redis, store redis, host 192.168.10.50, port 6379, prefix tp_session_,如果你用的是宝塔面板或小皮面板这类可视化面板Redis扩展也可以直接在面板里一键安装和配置原理一样只是界面化了。这里有个容易忽略的坑Redis本身可能也是单点。Session集中到Redis后Redis挂了等于所有用户集体掉线。生产环境务必给Redis做主从哨兵或者直接用云厂商的高可用Redis实例。5.2 上传文件与临时目录的共享方案负载均衡之后上传文件会是第二大的坑。用户上传的图片、附件如果写到了A机器的本地磁盘下一次请求被分到B机器时就读取不到。PHP的$_FILES临时文件也存在本地upload_tmp_dir同样面临跨机问题。实用的方案有三种按成本从低到高对象存储图片等静态资源直接传到OSS/COS这类对象存储PHP代码里凡是用到上传和读取的地方改成走对象存储的SDK。这是最推荐的做法尤其是新项目。NFS/GlusterFS共享存储把上传目录比如/data/wwwroot/example/uploads挂载为共享文件系统所有PHP-FPM服务器访问同一份数据。实现简单但NFS单点故障风险较高且高并发下IO会成为瓶颈适合中小规模项目。业务层改造老项目如果代码里写死了本地路径短期内可以通过数据库记录文件所在节点读取时做二次请求转发。这个方案能兜底但不优雅建议只作为过渡。需要特别提醒临时文件目录也不能忽视。如果代码里有move_uploaded_file但临时目录不一致还会出现上传成功但文件不存在这种诡异问题。检查一下php.ini里的upload_tmp_dir或者干脆把它也挂到共享目录。5.3 Opcache、APCu与代码发布的配合PHP 8时代Opcache已经是默认标配它能显著提升性能但多机环境下会带来一个反直觉的问题代码发布后各机器的Opcode缓存不一致。举个例子你更新了一段代码发布了A机器B机器还没来得及同步文件但B机器的Opcache里还缓存着旧代码。如果负载均衡正好把请求分给B机器用户就会看到新旧代码混合运行的结果——页面时而新版时而旧版排查起来极其痛苦。解决思路分两块尽量缩短代码同步的时间窗口用rsync inotify、Git部署钩子、或者Docker镜像版本化发布构建新镜像再滚动更新不要手动到每台机器上拖文件。发布后主动清理Opcache写一个发布脚本在每个节点更新完代码后执行opcache_reset()或者调用php -r opcache_reset();。生产环境不建议用opcache.validate_timestamps0永久关闭时间戳验证那样虽然性能最好但代码更新后必须手动清缓存一旦忘了就是诡异Bug。再说APCu它存的是用户态缓存数据默认是本机内存。多机环境下每台机器的APCu各存各的如果业务用APCu做统一缓存会出现数据不一致。想要共享缓存就用Redis或者Memcached。APCu只能用来存节点自身的局部数据不能当全局缓存用。6. 集群上线后的踩坑与排查链路前面讲的是部署之前要做的事接下来这部分是流量真正切过去之后你大概率会遇到的三类问题。我把完整的排查思路写出来每个都是我在项目里亲历过的。6.1 用户总是自动登出现象用户登录成功后刷新页面又变成未登录状态有时多刷新几次又正常了。这个问题在负载均衡上线后几乎必现除非你事先做了Session改造。排查链路先确认Session方案。在phpinfo里看session.save_handler如果是files而你的后端有多台机器那基本就定位了。再确认请求是否真的被分散了。用浏览器开发者工具看请求响应对比两次访问是落到同一台机器还是不同机器可以在后端临时写一个phpinfo页输出gethostname()方便区分。最后确认Redis连接是否稳定。如果Session写Redis成功但偶发失败要重点看Redis的timeout和maxmemory配置内存满了触发淘汰策略也会让Session丢失。6.2 图片上传偶尔失败现象上传图片有时候成功有时候报无法写入或文件不存在但服务器硬盘看起来是满的。排查链路看upload_tmp_dir指向的目录。如果它指向各节点本地磁盘而不是共享存储上传过程并发一高就容易出问题尤其是文件较大的时候。检查PHP的post_max_size和upload_max_filesize。集群环境下这两个值必须每台机器保持一致不然同一份配置在不同节点上行为不同用户会用脚投票的。如果用了NFS注意NFS默认协议版本和锁机制。mount -t nfs4 -o vers4.2配合noatime能明显改善写入性能另外NFS服务端千万别开防火墙把端口全封了那会引起时好时坏的网络超时。6.3 节点宕机但流量依然照打现象某台机器已经挂了但负载均衡还是把请求转过来用户体验到间歇性502。排查链路如果用的是Nginx开源版它默认只有被动健康检查。前面说过只要TCP能建立连接Nginx就认为节点活着。所以先看你的upstream配置里有没有max_fails和fail_timeout这俩值配小了起不到防护作用。检查健康检查的频率与超时设置。以HAProxy为例inter 3s fall 3 rise 2表示3秒探测一次坏3次摘除、好2次恢复。如果你线上配置的是inter 30s那最坏情况下故障节点要90秒才能被摘除用户已经骂完一轮了。更深一层确认你的探活脚本真的在检查业务可用性而不是只回一个echo ok。用上一节那个带数据库检测的health.php能提前发现半死节点。6.4 压测指标到底看什么集群上线后一定要压测但别只盯着QPS。我在项目里习惯看四个指标缺一不可吞吐量QPS和响应时间RT这两个是常规指标注意要结合看QPS高了但RT也飙了说明集群在硬扛。各节点的错误率分布如果某个节点报错率明显高于其他节点优先排查这台机器的配置是否一致而不是怀疑负载均衡规则。PHP-FPM的listen queue每个节点单独看。如果某台节点listen queue持续积压说明它的请求分配不均或者处理能力不足这时再调整weight或者切到least_conn。后端数据库和Redis的负载负载均衡把压力从PHP层分散了但下游数据库往往会成为新的瓶颈。很多集群改造上线后反而是数据库先垮。所以压测时一定要同时观察MySQL的Threads_running和Redis的connected_clients。我实操时习惯写一个简单的压测脚本循环对每个后端节点单独打流量再经过负载均衡器整体打流量把两种结果对比。如果直连单节点能抗住的量经过负载均衡后反而崩了那问题出在负载均衡器配置——比如worker_connections太小、keepalive没配、后端连接数限制没调。7. 从负载均衡到弹性伸缩下一步该想什么把负载均衡跑稳之后架构团队往往很快就会遇到新的问题流量有峰谷固定数量机器在低谷期浪费资源高峰期又不够用。这时候的演进方向是从人工加机器走向按需加机器。以Docker为例把PHP-FPM打包成镜像配合编排平台做水平伸缩已经是比较成熟的做法。核心是把之前讲的配套改造在容器层面重新落实一遍Session继续走Redis容器随时销毁重建也不受影响上传文件继续走对象存储或共享存储不能依赖容器本地磁盘代码通过镜像版本发布彻底告别每台机器rsync代码的运维模式Opcache在每个容器里独立存在新容器启动时重新编译缓存反而省掉了清理旧缓存的步骤。但我也要泼一盆冷水弹性伸缩不是负载均衡的必然升级它是业务规模和管理能力双重驱动的结果。如果你的业务高峰比较稳定、团队运维人手有限固定几台后端机器加上负载均衡反而比复杂的容器集群更容易维护。先把负载均衡这层吃透等到容器化的收益明显大于维护成本时再动是所有过来人会给你的建议。回到最初的话题PHP负载均衡技术本质上是一场状态治理工程——流量分发只是入口Session、缓存、上传文件、代码发布这些看似琐碎的配套才是决定集群能不能真正扛住业务的地方。把这套东西想清楚了从单机到多机其实没有想象中那么难。