一台机器上的三个 PHP 年代:多版本共存怎么管成有边界的资产
一、”能并存”和”能管好”是两件事
近期技术社区里有不少关于面板 PHP 环境配置与多版本切换的实践问答,说的是一件很实在的事:同一台服务器上可以并存多个 PHP 版本,每个站点单独指定使用哪个版本,切换是站点级操作,不会牵连同机其他站点。对存量系统较多的中小企业,这几乎是必需品——多年前写的站依赖旧大版本的扩展和函数行为,今年上线的业务系统要新版本才跑得动,把它们放在一台机器上是成本最低的选择。
麻烦在于面板把安装与切换做成了几步点击,代价是依赖关系全部退到界面后面。几个必须写对的通用机制:
- 扩展按版本编译。redis、pdo、gd、zip 在旧版本上装过,不代表新版本也有。典型现场是切换后首页正常,一进后台报扩展类找不到,或者上传功能直接 500。
- socket 路径跟着版本走。Web 服务器与 PHP 之间通过 socket 通信,切换版本时 pool 与 socket 路径必须和新版本对上。路径没换过来就是 502,而且只坏一个站点,很容易被当成程序 bug 排查半天。
- CLI 与 FPM 不同源。
php -v看到的是命令行版本,站点跑的是另一个。定时任务和队列消费走 CLI,用错版本的表现为”网页一切正常,计划任务静默失败”。 - 缓存要重载。opcache 与真实路径缓存在切换后需要重载,否则新版本还在读旧字节码。
- 顺手升级是最贵的善意。老站点被升到新大版本,废弃函数与类型行为的差异会立刻报错,而且往往出现在冷门路径上。

二、版本台账:把隐性依赖写成显性记录
一台机器上跑着 5.6、7.4、8.2 三个年代的站,靠记忆和界面是管不住的。第一个该建立的东西是台账,粒度到站点,一行一个站,字段不多但缺一不可:
| 站点(示例) | 当前 PHP 大版本 | 所需扩展及版本 | 上次验证时间 | 计划下线时间 |
|---|---|---|---|---|
| 示例 A:展示型官网 | 7.4 | redis 5.x、gd、zip | 随季度备份验证 | 长期保留 |
| 示例 B:老旧商城 | 5.6 | pdo_mysql、gd | 上次功能回归时间 | 已定,等待数据归档 |
| 示例 C:业务后台 | 8.2 | redis、pdo、opcache | 上线即验证 | 长期保留 |
表里的内容是示意,字段本身才是重点。三个容易漏的地方:
- 扩展清单要写到版本。只记”需要 redis”是不够的,扩展自身与新版本 PHP 的兼容矩阵才是切换前真正的判断依据。
- 上次验证时间必须有。一个装了但半年没跑过冷功能的站点,它的可用状态是推测出来的,不是验证出来的。
- 计划下线时间是台账存在的理由。没有这一列,台账只会变长,老站点永远不会少。
台账不是给运维一个人看的,它是切换决策的输入:谁还能升、谁只能原地续命、谁该在到期前迁走,一眼能分。
三、切换的标准动作顺序
顺序比技巧重要。一次没有隔离测试的直接切换,出问题时的排查面是全机所有站点。可控的顺序是这样:
- 查台账,确认目标站点声明的扩展清单,在新版本上逐个装好;拿不准的扩展先在命令行加载验证,不等站点报错才发现。
- 起一个隔离测试站点。同机复制一份代码加一份测试库,指到新版本,让它跑完整功能路径,而不是只打开首页。
- 核对 socket 与 pool 配置指向新版本,测试站点访问正常之后才动生产。
- 备份数据库与站点目录,这是回滚的物质条件,不是可选项。
- 执行生产切换,切换后立刻重载 opcache 与真实路径缓存。
- 当场看两件事:站点错误日志有没有新增的废弃函数告警;命令行版本与站点实际运行版本、定时任务使用的版本是否一致。
- 观察期后再收尾,一小时内不做下一次变更。
其中第 6 步最容易被跳过。废弃函数在新版本上通常先告警后失败,切换当天看起来正常,几周后某个功能才断,那时已经没有任何切换上下文可查。
四、回滚窗口不是”留着旧版本”这么简单
保留旧版本一段时间是必要的,但真正决定回滚能不能用的是三件事是否同时存在:旧版本的编译产物还在(清理脚本可能连带删掉扩展)、站点目录与数据库有切换前的完整副本、以及切换动作本身有记录。缺第三条,回滚之后没人知道当时的改动范围,故障会被归因到”不知道哪里”。
窗口长度取决于站点的业务节奏,不是固定天数。一个每天跑批量任务的站,窗口要覆盖至少一个完整批处理周期;一个只在旺季有流量的站,覆盖到淡季即可。窗口结束的标志也不是时间到期,而是台账里那次验证已经跑过、错误日志干净、批量任务成功执行过一轮。
维护成本随版本数量上升,而且不是线性上升。每多一个大版本,扩展的编译矩阵多一列,同一次全局变更的验证面多一层,出现”一次编译影响全站”这类耦合的概率也在涨。
五、洹拓视角:什么时候继续堆,什么时候该拆
判断不靠感觉,靠可验收的条件。
继续在同一台机器上堆版本,需要同时满足:站点数量少到台账可以逐行验证;版本数还在扩展依赖不互相冲突的范围;每次版本变更都能安排在流量低谷;机器内存与编译耗时留有余量——多版本并存时,编译是内存峰值的高危动作。
该拆机或容器化的信号更具体,出现任意一条就该启动:
- 版本数超过团队能逐站点回归验证的上限,台账里”上次验证时间”开始大面积过期;
- 扩展依赖出现冲突,同一个扩展在不同版本上需要的编译参数不再一致;
- 一次扩展编译或一次全局配置变更的影响面覆盖全站,无法按站点隔离;
- 老站点只剩数据价值,代码已经不再演进——这类站不该继续占版本位,应转成只读归档或静态化;
- 定时任务与站点需要不同版本,CLI 与 FPM 的分离已经靠临时脚本维持。
拆不是把复杂度消掉,是把耦合的复杂度换成可边界化的复杂度。容器化后每个站点自带运行时与扩展清单,版本台账从”一台机器的共享状态”变成”每份声明里的私有状态”,代价是要接受镜像管理与资源开销。对只有两三个站、内存充裕的场景,这笔交易通常不划算;对版本数持续增长、扩展冲突已经出现过的场景,继续在一台机器上堆版本,是在给未来的故障预置现场。