有人问过我一个问题PHP项目流量上来以后第一件该做的事是什么十次里有八次我的答案是“先把负载均衡搭起来”剩下两次是“先拆库存”。玩笑归玩笑但原理是清楚的PHP这种“每个请求独立执行”的模型天生适合水平扩展而负载均衡算法决定了流量怎么被分到多台机器上。算法选错机器加得再多也可能只是在放大某个节点的压力。这篇文章我不打算只讲理论。我会从“为什么PHP站必须考虑负载均衡”开始把几类常用算法掰开揉碎讲清楚再落到Nginx配置和一轮实测上最后聊一聊PHP应用在负载均衡之后必须跟着改的那些事。适合正在从单机往多机过渡的团队也适合面试前想把算法和工程实践串起来的人。1. 负载均衡在PHP架构里到底要解决什么不是找一台“更快的机器”很多人一开始的理解是把负载均衡当成“换台更强的服务器”其实不对。负载均衡解决的是“单台机器的处理边界”问题而不是性能优化问题。PHP请求的生命周期很短从收到请求到执行完脚本、返回响应通常几百毫秒就结束了但短生命周期恰恰意味着每个请求都要消耗完整的进程资源。你优化MySQL、加OPcache、调PHP-FPM参数都是在跟单机的CPU、内存、进程数上限较劲。1.1 PHP-FPM的进程模型决定“加资源”的边界PHP-FPM启动后会维持一组worker进程默认pm.max_children取决于你给的内存和每个请求平均占用。假设一个worker占用30MB内存一台2C4G的机器撑死也就跑七八十个并发超过了就只能排队。PHP不像Node.js或者Go那样一个进程能扛几千个并发连接它本质上是“一个请求占一个worker”所以横向加机器的收益非常直接worker总量翻倍吞吐量跟着翻倍。负载均衡算法在这里的角色就是把流量相对均匀地分发到多个PHP-FPM节点上。1.2 负载均衡引入后的三大隐性约束状态、存储、时间流量分发只是表象真正的问题是引入多节点之后原来的“单机假设”被打破了状态PHP默认的session文件存在本地磁盘用户第一次请求落在A机器第二次被分发到B机器session就丢了。这是所有PHP负载均衡方案都绕不开的第一道坎。存储用户上传的图片、附件、生成的临时文件如果只写在本地磁盘下次请求落到别的机器上就找不到了。时间某个定时任务如果在多台机器上同时执行会造成数据重复写入、邮件重复发送之类的事故。这三个约束比算法本身更容易让你加班。算法选错了通常只是流量不均而状态存储没处理好会让整个系统看起来“随机抽风”。我在给团队做方案的时候习惯先把这三件事列出来再谈用什么算法因为算法要配合状态方案才能定下来。2. 基础算法逐个拆轮询、加权轮询、最少连接与随机负载均衡算法有一个很简单的判断标准你到底想不想让“同一个用户”总是访问“同一台机器”。如果不关心用户的会话状态那轮询和随机就是最省事的如果关心那就得往IP哈希或cookie哈希上走。这一节先讲不关心会话状态的几个基础算法下一节再说会话保持。2.1 轮询和加权轮询最简单的算法反而最常用轮询RR的逻辑一句话就能说清楚第一次请求给A第二次给B第三次给C然后回到A。Nginx里默认用的就是轮询它假设所有后端机器性能一致。但现实里很少有一模一样的机器所以我们经常用加权轮询WRR给高配置机器更大的权重upstream php_backend { server 10.0.0.11:9000 weight4; server 10.0.0.12:9000 weight2; server 10.0.0.13:9000 weight1; }这里的weight参数不是“绝对请求数”而是“相对比例”。按照上面的配置10.0.0.11每接收4个请求10.0.0.12接收2个10.0.0.13接收1个。整体比例是4:2:1但如果某台机器挂了Nginx会自动把它摘掉剩下的机器按比例消化流量。加权轮询的适用场景非常广几乎所有PHP业务接口都能用它。它有一个小缺点如果请求处理耗时差异很大轮询仍然可能让某台机器积压长请求。这就轮到最少连接算法出场了。2.2 最少连接与随机什么时候真正需要评估“连接数”最少连接算法least_conn看的是每台后端服务器当前的活跃连接数新的请求总是发给连接数最少的那台。听起来很智能但它在PHP架构里有一个隐藏前提你的PHP-FPM请求处理时长必须足够分散比如有的接口10毫秒有的接口5秒这时候连接数才能真实反映负载。如果所有接口耗时都差不多least_conn的效果和轮询差别不大反而多了一点调度开销。随机算法更直白随机挑一台后端。它的问题不在于随机本身而在于“样本少的时候不平均”。比如只有两台机器连续来10个请求很可能6比4甚至7比3。压力测试时随机算法会让你误判某台机器性能差实际上是随机分布不均匀导致的。所以我的观点是随机算法适合请求量特别大、单机处理时间特别短的场景样本足够大之后分布会趋于均匀但中小流量下别用它。2.3 四个算法的耐用性对比表算法原理是否保证会话固定推荐场景坑点轮询 RR按固定顺序轮流分发否接口无状态、机器配置接近慢请求可能积压加权轮询 WRR按权重比例分发否机器配置差异明显权重设置不合理会导致倾斜最少连接 least_conn发给活跃连接数最少的节点否请求耗时差异大需配合长连接监控不能只看进程数随机 random随机选一台否请求量极大且耗时稳定小流量下分布不均这里我多说一句Nginx的least_conn统计的是“当前正在被代理处理的请求数”不是后端机器的负载。如果后端PHP-FPM的队列已经堆了几百个请求Nginx是感知不到“排队数”的它只看到活跃连接数可能很低。所以更稳的做法是用max_conns限流或者配合健康检查及时摘掉问题节点。3. 会话保持类算法IP哈希、URL哈希以及PHP会话漂移的烦恼如果后端PHP应用还在用文件保存Session那么会话保持类算法几乎是必选项。但这类型算法没有听起来那么完美它们解决的是“尽量固定”不是“绝对固定”。3.1 IP哈希用客户端IP换“固定的那台机器”IP哈希算法对客户端IP做哈希计算然后用哈希值与后端服务器数量取模决定请求落在哪台机器。Nginx配置只有一行upstream php_backend { ip_hash; server 10.0.0.11:9000; server 10.0.0.12:9000; server 10.0.0.13:9000; }同一个IP的请求会稳定落在同一台机器看起来session不会丢了。但两个实际问题摆在面前移动网络和NAT出网会让大量用户共享同一个公网IP这些用户会被集中压到一台后端上造成倾斜。后端节点数量变化扩缩容时取模的除数变了大量用户会被重新映射到新节点session还是可能丢失。所以IP哈希更适合内网调用或者固定出口IP的场景。公网用户流量我只能建议配套一个“兜底session方案”别把全部希望押在IP哈希上。3.2 URL哈希与更细粒度的“用户名哈希”URL哈希是对请求URI做哈希而不是对IP做哈希。它的意义主要是解决缓存命中问题同一个URL总是落到同一台机器。对纯API网关来说这个算法可以配合本地缓存使用但PHP后端如果每个节点都有自己的本地缓存数据一致性又会变成新问题。更实际的变体是“业务字段哈希”比如在Lua或者应用层里拦截请求取用户ID或者订单ID做哈希把同一个用户的操作都导到一台机器上。这样做的好处是粒度更细比IP哈希公平得多坏处是需要在代理层或PHP-FPM之前的入口写逻辑没法用一行upstream配置搞定。3.3 PHP会话从本地文件迁到Redis的完整步骤说实话session粘滞方案做得再漂亮也不如“session根本不依赖机器”来得干净。我的建议始终是直接迁移到Redis或者别的集中式存储。这才是根治“会话漂移”的做法。迁移步骤不复杂找一台Redis实例或者复用已有的缓存集群。在PHP的php.ini里改session存储配置session.save_handler redis session.save_path tcp://127.0.0.1:6379重启PHP-FPM观察业务日志确认没有session报错。把session.cookie_secure和session.cookie_httponly按生产环境要求补上。迁移之后集群里随便哪台机器处理请求都能拿到同一个session粘滞算法就可以彻底关掉。唯一需要留意的就是Redis的持久化策略和网络延迟如果Redis和PHP不在同一机房每次session读写多出几毫秒RTT接口耗时会明显上涨。所以我一般建议Redis和Web节点放在同一个内网延迟控制在1毫秒以内。4. 在Nginx里落地PHP负载均衡配置细节与一轮真实测试理论讲了半天最终还是要落到配置上。Nginx的upstream模块是PHP负载均衡最常见的落点我自己也是从一轮真实测试里才确认了“配置里少一个参数故障时有多疼”。4.1 upstream配置的骨架与常用参数一个能上生产的PHP负载均衡upstream大概长这样upstream php_fpm_servers { least_conn; server 10.0.0.11:9000 weight3 max_fails2 fail_timeout10s; server 10.0.0.12:9000 weight2 max_fails2 fail_timeout10s; server 10.0.0.13:9000 backup; keepalive 32; } server { listen 80; server_name api.example.com; root /data/www/api; index index.php; location ~ \.php$ { fastcgi_pass php_fpm_servers; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_read_timeout 60s; fastcgi_send_timeout 60s; fastcgi_next_upstream error timeout http_502 http_503 http_504; } }参数里有几个细节容易被忽略backup标记为backup的机器只在其他机器全部不可用时才参与接收请求很适合做冷备。max_fails和fail_timeout配合使用。10秒内失败2次就把节点标记为不可用之后10秒内不再给它转发请求。这两个值别调得太大否则故障转移要等半天。keepalive 32这一行很多人漏掉。它让Nginx和后端PHP-FPM之间保持长连接避免每个请求重新建TCP连接。对PHP这种短生命周期请求来说连接复用能省下一笔可观的握手开销。4.2 实测轮询与加权轮询的流量分布为了直观看到算法效果我通常在每台后端放一个返回自身标识的PHP脚本echo gethostname() . | . $_SERVER[SERVER_ADDR] . \n;然后执行ab -n 120 -c 12 http://api.example.com/hello.php用轮询算法120个请求会被均匀分给三台机器每台大概40个。改成加权轮询之后权重为3、2、1的机器大致分到60、40、20。如果发现比例和预期差得很远优先排查是不是有某台机器被健康检查摘掉了或者weight写错了位置。这个测试很简单但它能帮你确认配置生效还能在扩容后验证新节点真的在接收流量。4.3 故障转移背后的坑超时、重试和“连环雪崩”负载均衡最值钱的能力是故障转移。一台PHP-FPM挂了Nginx会把请求转给其他节点。但这套机制的坑也不少第一个坑是fastcgi_next_upstream的语义。如果你不加http_502、http_503这些状态码默认只在“连接不上”时转移。PHP进程如果返回500错误请求还是会留在原节点用户会看到报错页。所以我上面的配置里明确加上了这几个状态码。第二个坑是重试放大。fastcgi_next_upstream触发后Nginx会把同一个请求重新发给下一台机器。如果这个请求本身有严重的逻辑问题比如死循环或者内存溢出它会依次打满所有节点。更危险的是如果后端返回502的原因是“数据库挂掉”Nginx的重试会让每一台PHP节点都尝试连接数据库数据库恢复后也会被瞬间涌来的重试流量再次压垮。这时候合理的做法是限制fastcgi_next_upstream_tries为重试一次并且不要把所有超时时间都调得很短。5. PHP应用侧必须同步收拾干净的事Session、上传、定时任务负载均衡配置完成后项目能不能平稳跑起来要看应用侧配合得怎么样。这部分是纯经验活踩过的坑比配置本身多得多。5.1 上传文件与静态资源的共享问题PHP站点最常见的文件存储诉求是用户上传头像、附件、导入文件。多机部署后本地磁盘写文件等于把文件写进了“单节点的黑盒”。解决思路分两步上传目录用共享存储比如NFS或者直接接对象存储服务。改造成本最低的是把upload_tmp_dir指向共享盘再把业务层上传逻辑改成“先暂存后迁移”。如果暂时没有对象存储至少要让Nginx的alias静态文件服务指向共享目录而不是每个节点自己的本地目录。我在实际项目里见过最隐蔽的问题两台机器文件不同步图裂一半靠运气。后来把上传目录切到对象存储才彻底消停。5.2 定时任务避免被多台机器“重复执行”PHP不擅长常驻进程所以很多项目用crontab每分钟跑一个PHP脚本来做队列消费、报表生成或者清理任务。单机时代没问题多机以后每台机器都会执行一遍数据重复插入、邮件重复发送都是这么来的。解决办法有几个层次简单粗暴的方法是只保留一台机器执行定时任务的crontab其他机器全部删掉对应的定时任务。但这样又引入了单点故障。稍好一点的办法是引入分布式锁。用Redis的SET NX EX做一个锁标记只有拿到锁的节点才继续执行任务逻辑执行完释放锁。如果任务本身要从队列里消费最好让任务脚本处理“同一个消息只消费一次”靠业务幂等来兜底。我通常建议团队先做幂等再做锁因为幂等能覆盖更多意外场景比如网络抖动导致的重试。5.3 日志归集与一次排障链路还原多机化之后排障最大的痛点不是代码而是日志各自为政。A机器上有报错B机器上没有你根本不知道用户到底被分到了哪台。解决办法就是把日志统一收集起来至少要做到“按时间线把多台机器的日志串起来”。具体做法通常是PHP-FPM的错误日志写到本地后用filebeat这类采集器发到日志平台。应用里统一打trace_id入口处生成一个随机ID在日志、数据库查询、外部调用里都带上这个ID。我上一次帮一个团队排查五台机器上的偶发502就是靠把所有节点的Nginx访问日志和PHP-FPM慢日志拉到一起按时间对齐才发现问题不在PHP本身而在于其中一台机器的磁盘IO被打满了导致PHP-FPM响应超时。如果没有统一日志靠一台一台机器翻这种偶发问题根本查不出来。从我自己的实操经验看负载均衡算法的选择固然重要但它只是整个多机改造的起点。真正让系统在流量放大后还稳如泰山的是应用层对状态、存储和日志的重新设计。先把session集中化再处理上传目录最后补上分布式任务的兜底这套组合走完你再回过头去调权重、调算法才会觉得顺理成章。
阅读完成 · 觉得有帮助?