做过几年后端部署的人,大概率都会碰到这种场景:一台服务器上跑着好几个项目,老的问答平台还锁死在PHP 7.4,新接的PhpAsk问答系统则需要PHP 8.1以上才能稳定跑起来。更麻烦的是,你不可能因为这个就拆机器,也不可能把所有项目都强制升级到同一个版本。这时候需要的就是多版本PHP共存——让不同项目各自使用自己依赖的PHP版本,互不干扰。
这篇内容就把整个过程完整拆开讲一遍:为什么需要多版本共存、三种主流方案怎么选、从零编译到Nginx接入的每一步操作、PhpAsk需要的Swoole扩展怎么装,以及实际部署中一定会遇到的几个坑。不管是老运维还是刚接触服务器配置的同学,按着步骤走一遍基本都能搞定。
1. 为什么同一台服务器要装多个PHP版本
1.1 真实场景:老项目锁版本,新项目要新特性
先说个我最近处理的真实例子。一台生产服务器原来是给公司内部一个老问答模块用的,代码基于PHP 7.4写的,里面有大量老函数依赖和第三方SDK,升级到PHP 8.x会产生几百个弃用警告,甚至有几个扩展直接不能用了。但新上线的PhpAsk问答系统不同,它基于Swoole常驻内存模型,需要PHP 8.1以上才能拿到更好的异步IO和协程支持。
如果强行二选一,要么老项目推倒重写,要么新项目降级将就,成本都高得离谱。多版本共存的核心思路就是:每个PHP版本独立安装、独立配置、独立运行,项目A用7.4,项目B用8.1,Nginx这边通过不同的fastcgi端口把请求分发到对应版本。
这种场景在现实中非常普遍:老系统不能迁移、新系统需要高版本、临时验证某个扩展在高版本下的兼容性、或者在测试环境模拟不同的生产版本。就算你不是跑PhpAsk,这套方法对任何PHP项目都适用。
1.2 三种常见思路:Docker、phpbrew、源码编译
多版本PHP共存的方案大致有三条路,各有利弊。
Docker是目前最省心、隔离性最好的方式。把每个PHP版本做成独立容器,数据目录挂载出来,互不污染,上线回滚也方便。但如果你面对的是传统LNMP架构的老服务器,或者项目里大量依赖本机文件、定时任务、共享扩展,全容器化改造的工作量不小,有些老代码还跑在容器外。
phpbrew是一个PHP版本管理器,类似Python的pyenv,一条命令就能编译一个PHP版本,切换也很干净。它适合开发机和个人电脑,但在生产环境上有两个尴尬的地方:一是版本更新维护速度跟不上PHP官方节奏,装PHP 8.3以上的新版可能因为依赖包版本问题编译失败;二是扩展编译管理坑比较多,特别是Swoole这类强依赖环境的扩展。
源码编译是本文选择的主角,也是Linux生产环境的老牌做法。它的逻辑就很朴素:把每个PHP装到独立目录,例如/usr/local/php/7.4、/usr/local/php/8.1、/usr/local/php/8.3,不同版本之间没有任何文件冲突,php-fpm各起各的进程、各监听各的端口,Nginx按项目做分发。缺点就是编译耗时,需要手动管理扩展,但胜在可控性强、性能可以按需定制,出了问题也知道去哪里排查。
我的建议是:个人开发机用phpbrew求快,追求环境隔离和交付一致性用Docker,传统服务器生产环境、特别是需要长期稳定运行的项目,源码编译多版本是最稳的解法。
2. 方案选型:源码编译多版本和Docker/phpbrew怎么取舍
2.1 四种维度横评
把三种方案放在一起对比,看几个最关键的维度:
| 对比维度 | 源码编译多版本 | phpbrew | Docker方案 |
|---|---|---|---|
| 环境隔离程度 | 目录隔离,较彻底 | 同目录切换,隔离性一般 | 完全隔离,最强 |
| 部署速度 | 单版本编译约10-20分钟 | 同样要编译,时间差不多 | 拉取镜像,分钟级 |
| 扩展管理 | 手动编译,但路径明确 | 自动编译,异常较多 | 打包在镜像里,最省事 |
| 生产环境稳定性 | 高,进程模型清晰 | 中,更适合开发机 | 高,但依赖编排 |
| 与现有LNMP融合 | 直接复用Nginx/MySQL | 需要额外整合 | 需要重构网络/存储 |
| 回滚成本 | 低,改端口即可 | 中,切换版本 | 低,切换镜像 |
| 运维排错门槛 | 中,懂路径概念即可 | 低 | 中,需要懂容器 |
Docker在隔离和交付上的优势确实明显,但有一个容易被忽略的问题:如果你接手的是已经跑了好几年的服务器,上面还有一堆手工安装的扩展、定时任务、日志切割脚本,硬往容器里迁很痛苦。源码编译方案在这样环境里的侵入性最小,只需要新增几个目录、几组配置文件,原系统上跑得好好的东西一概不动。
2.2 版本规划:三个版本覆盖常见需求
多版本其实不用贪多,按需求规划就好。这里选了三代代表版本:
- PHP 7.4:老项目的最后一代稳定版,兼容大量遗留代码和旧扩展,很多公司的生产环境存量极大,短期不可能全部升级。
- PHP 8.1:构建PhpAsk等现代应用的主力版本,支持枚举、只读属性、Fibers,性能和类型系统都很完善。
- PHP 8.3:新项目或想体验最新特性的项目可以用,8.3在类型化和性能上又进步了不少,适合做新功能试点。
如果对PhpAsk没有特殊的版本要求,8.1和8.3都可以跑,我更推荐新装直接用8.3,Swoole新版本的兼容性也更好。本篇文章就用7.4、8.1、8.3三个版本作为示例,实际部署时你可以按自己需求调整版本号,安装流程完全一样。
3. 实操:三个PHP版本共存全流程配置
3.1 环境准备:先把编译依赖装齐
在开始编译之前,先把基础依赖装好,不然每装一个版本都要补一轮包,非常浪费时间。以Debian/Ubuntu系为例,可以直接执行:
apt-get update apt-get install -y build-essential autoconf libxml2-dev libssl-dev \ libcurl4-openssl-dev pkg-config libsqlite3-dev \ libonig-dev libzip-dev libsodium-dev libpng-dev \ libjpeg-dev libwebp-dev libfreetype6-dev \ libbz2-dev libreadline-dev libsqlite3-dev如果是CentOS/RHEL系,把包名换成对应的:gcc gcc-c++、autoconf、libxml2-devel、openssl-devel、libcurl-devel、libzip-devel、libsodium-devel、libpng-devel、libjpeg-devel、freetype-devel那些。
装依赖的时候有个容易忽略的点:编译PHP 8.1以上版本,autoconf版本最好在2.69以上,太旧会在生成configure阶段直接报错。另外libxml2-dev和libssl-dev是刚需,PHP 8.x编译时如果检测不到这两个库,会直接死在./configure这一步,后面排查时再细说。
3.2 编译安装三个版本的PHP
接下来进入正题。这里以编译PHP 7.4为例详细展示,8.1和8.3的流程完全一样,只是版本号和配置参数稍微调整。
正式编译前,先到php.net下载源码包,用国内镜像站或者你自己方便的渠道下载都行:
wget https://www.php.net/distributions/php-7.4.33.tar.gz tar zxf php-7.4.33.tar.gz cd php-7.4.33然后运行configure,关键点就是每个版本一定要指定独立的prefix安装目录,并且要打开fpm、连接数据库、常用扩展这三类核心选项:
./configure --prefix=/usr/local/php/7.4 \ --enable-fpm \ --with-fpm-user=www-data \ --with-fpm-group=www-data \ --with-mysqli \ --with-pdo-mysql \ --with-openssl \ --with-curl \ --with-zlib \ --enable-mbstring \ --enable-sockets \ --enable-pcntl \ --enable-posix \ --with-gd \ --with-gettext \ --enable-zip参数说明:
- --prefix=/usr/local/php/7.4:指定PHP安装目录,后续所有可执行文件、配置、扩展都装到这里,这是实现多版本隔离的核心。
- --enable-fpm:编译PHP-FPM,这是和Nginx对接的关键。
- --with-fpm-user和--with-fpm-group:指定FPM进程运行的用户,生产环境用www-data,避免用root跑PHP。
- --with-mysqli和--with-pdo-mysql:连接MySQL的两种接口,PhpAsk这类Web应用基本都需要。
- --enable-pcntl和--enable-posix:Swoole和常驻任务依赖的两个基础扩展。
configure成功之后开始编译安装:
make -j$(nproc) make install如果机器资源允许,可以先用nproc看一下CPU核数,加上-j参数能明显缩短编译时间。7.4版本我实测在4核机器上大约需要8到10分钟。
安装完成之后,把默认的php.ini复制到安装目录的etc下,否则后面PHP会找不到配置文件:
cp php.ini-development /usr/local/php/7.4/etc/php.ini生产环境建议用php.ini-production,不过PhpAsk这类项目在开发调试阶段php.ini-development里很多参数更宽松,可以根据自己情况来。复制好之后,稍微调整几个关键参数,memory_limit给到256M以上,upload_max_filesize看项目需要,timezone设置成Asia/Shanghai。
接下来同样的方式编译PHP 8.1和8.3,只需把版本号替换成对应的8.1.xx和8.3.xx,prefix改成/usr/local/php/8.1、/usr/local/php/8.3。PHP 8.1以上编译时,几个扩展有些小差异,比如libxml不需要额外指定、pdo_mysql默认开启,不过按上面那套参数写下来一般不会出问题。
编译完三个版本后,检查一下目录结构:
ls /usr/local/php/ # 7.4 8.1 8.3 /usr/local/php/8.1/bin/php -v /usr/local/php/7.4/bin/php -v /usr/local/php/8.3/bin/php -v能分别输出对应版本号,就说明最核心的安装成功了。
3.3 配置三个独立的php-fpm实例
PHP装好了,现在要让每个版本都有自己的php-fpm进程。PHP编译安装时,FPM配置文件模板一般在$prefix/etc/php-fpm.conf.default和php-fpm.d/www.conf.default。先把它们复制成正式配置文件:
cp /usr/local/php/7.4/etc/php-fpm.conf.default /usr/local/php/7.4/etc/php-fpm.conf cp /usr/local/php/7.4/etc/php-fpm.d/www.conf.default /usr/local/php/7.4/etc/php-fpm.d/www.conf8.1和8.3重复同样的操作。
关键步骤在这里:修改www.conf中listen参数,让三个版本监听不同地址或不同端口。这里推荐用Unix Socket方式,性能比TCP端口略好,也更安全:
; /usr/local/php/7.4/etc/php-fpm.d/www.conf listen = /run/php-fpm74.sock listen.owner = www-data listen.group = www-data listen.mode = 06608.1设置成/run/php-fpm81.sock,8.3设置成/run/php-fpm83.sock。
当然也可以用TCP端口:
listen = 127.0.0.1:90018.1用9002,8.3用9003。两种方式任选其一,建议统一一种风格。如果这台服务器后续还要加更多版本,用Socket方式不占端口,管理上也清晰。
接着把三个php-fpm分别注册成systemd服务,方便开机自启和统一管理。以8.1版本为例,创建一个/etc/systemd/system/php-fpm81.service:
[Unit] Description=PHP 8.1 FastCGI Process Manager After=network.target [Service] Type=forking PIDFile=/run/php-fpm81.pid ExecStart=/usr/local/php/8.1/sbin/php-fpm -y /usr/local/php/8.1/etc/php-fpm.conf ExecReload=/bin/kill -USR2 $MAINPID PrivateTmp=true [Install] WantedBy=multi-user.target7.4和8.3分别创建类似的service,把路径里的版本号都替换掉。启动测试:
systemctl daemon-reload systemctl start php-fpm74 systemctl start php-fpm81 systemctl start php-fpm83 systemctl enable php-fpm74 php-fpm81 php-fpm83启动后通过ss -lntp或者ls -l /run/php-fpm*.sock确认三个进程都正常监听各自的地址。
3.4 统一CLI入口:让每个版本能快速切换
php-fpm搞定了,命令行也需要方便。三个版本的可执行文件都在各自目录下:
/usr/local/php/7.4/bin/php /usr/local/php/8.1/bin/php /usr/local/php/8.3/bin/php直接敲php的时候,系统到底用的是哪个?取决于PATH环境变量。一个比较干净的做法是在/usr/local/bin下做几个软链接,用带版本号的命令区分:
ln -s /usr/local/php/7.4/bin/php /usr/local/bin/php74 ln -s /usr/local/php/8.1/bin/php /usr/local/bin/php81 ln -s /usr/local/php/8.3/bin/php /usr/local/bin/php83以后想用哪个版本就敲对应命令。需要默认php版本指向某个版本时,可以用update-alternatives统一管理:
update-alternatives --install /usr/bin/php php /usr/local/php/7.4/bin/php 74 update-alternatives --install /usr/bin/php php /usr/local/php/8.1/bin/php 81 update-alternatives --install /usr/bin/php php /usr/local/php/8.3/bin/php 83 update-alternatives --config php这条命令的好处是:系统层面真正维护了一个php软链接的切换开关,随手执行一行配置就能把默认PHP换成任意版本,回滚也方便。
需要特别注意:很多服务器上之前用apt装过PHP,系统自带的php在/usr/bin/php,情况和这个有冲突。这时候建议先dpkg -l | grep php检查一下有没有系统级PHP包,如果有,最简单的是把apt的php卸掉,或者把/usr/local/bin放到PATH最前面,保证自己编译的版本优先生效。
4. 让PhpAsk跑在指定版本上:扩展安装与Nginx接入
4.1 PhpAsk需要哪些扩展
PhpAsk不是一个普通的FPM应用,它依赖Swoole扩展来实现高性能常驻内存模式。Swoole比较特殊,它不是PHP默认自带的扩展,启动方式和传统php-fpm也不一样,需要额外确认环境里有没有装好。
除了Swoole,下面是PhpAsk这类问答系统的基本依赖:
| 扩展 | 用途 |
|---|---|
| swoole | 常驻内存、协程、异步IO核心,PhpAsk运行基石 |
| pdo_mysql | 数据库接入 |
| mbstring | 中文问答内容的字符编码处理 |
| json | PHP 8.x内置,用于接口数据交互 |
| openssl | HTTPS和加密相关 |
| pcntl / posix | 进程控制,Swoole Worker管理依赖 |
| fileinfo | 文件类型识别 |
| curl | 外部API请求 |
| zip | 附件或离线包处理 |
| sockets | 网络通信扩展 |
4.2 给指定版本安装Swoole扩展
以PHP 8.1版本为例,手动编译Swoole扩展的思路是这样的:先进入PHP 8.1的bin目录,确保phpize、php-config都指向8.1版本,然后用pecl或者源码编译的方式安装上Swoole。这里优先推荐直接用pecl来安装Swoole,因为pecl会自动匹配当PHP版本和扩展目录。
先给8.1确认pecl路径:
/usr/local/php/8.1/bin/php -m | grep pecl如果没装pecl,可以找到PHP源码安装包里的pecl脚本,或者用phpize + pecl方式补装。最简单的办法是在编译PHP时打上--with-pear,不过如果当时没打,也可以后面用这个命令安装pecl:
curl -o /usr/local/php/8.1/bin/pecl https://pear.php.net/go-pear.phar /usr/local/php/8.1/bin/php /usr/local/php/8.1/bin/pecl其实在实际部署里,Swoole官方推荐直接从源码编译,下载Swoole源码包后执行:
cd /usr/local/src wget https://github.com/swoole/swoole-src/archive/refs/tags/v5.0.3.tar.gz tar zxf v5.0.3.tar.gz cd swoole-src-5.0.3 /usr/local/php/8.1/bin/phpize ./configure --with-php-config=/usr/local/php/8.1/bin/php-config make -j$(nproc) make install执行完会在终端输出一个扩展安装路径,例如/usr/local/php/8.1/lib/php/extensions/no-debug-non-zts-20210902/,打开/usr/local/php/8.1/etc/php.ini,在文件末尾加一行:
extension=swoole.so然后验证:
/usr/local/php/8.1/bin/php -m | grep swoole能看到swoole就说明扩展加载成功。
这里有个知识点:phpize、php-config、php这三样工具必须来自同一个PHP版本目录,一旦混用了其他版本的,扩展就会装到错误目录里去,导致php -m里看不到扩展。这个问题在下面排查部分还会专门讲。
4.3 Nginx站点配置实例
PhpAsk作为Swoole常驻应用,正统的部署方式是直接让Nginx作为反向代理,把请求转发给Swoole的HTTP服务端口。不过,也有不少场景里PhpAsk是以FPM方式运行的,或者你只是想体验问答系统功能,用传统php-fpm跑也没有问题。两种方式对多版本PHP共存的接入来说,Nginx配置略有差异。
以FPM方式跑PhpAsk为例,Nginx虚拟主机配置文件可以这样写:
server { listen 80; server_name ask.example; root /var/www/phpask/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?s=$uri; } location ~ \.php(.*)$ { fastcgi_pass unix:/run/php-fpm81.sock; fastcgi_index index.php; fastcgi_split_path_info ^(.+\.php)(.*)$; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(js|css|png|jpg|gif|ico)$ { expires max; log_not_found off; } }注意fastcgi_pass这行,写的是usnix:/run/php-fpm81.sock,这就是把该项目的PHP请求分发到8.1版本php-fpm的核心。如果你想让老项目用7.4,就照葫芦画瓢再写一个server块,root指到老项目目录,fastcgi_pass改为unix:/run/php-fpm74.sock。
如果PhpAsk跑在Swoole HTTP服务模式,Nginx配置更简单,只需将80端口反代给Swoole监听的端口:
server { listen 80; server_name ask.example; location / { proxy_pass http://127.0.0.1:9501; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }Swoole模式的好处是性能更优,但需要保证Swoole进程常驻,并且项目代码修改后要reload,对运维的要求也更高。配置完成后:
nginx -t systemctl reload nginx浏览器里用一个测试域名访问,或者用curl指定Host头验证:
curl -H "Host: ask.example" http://127.0.0.1/能出页面就说明链路通了。
5. 常见问题与排查技巧实录
5.1 编译报错集中在依赖和参数上
源码编译最常见的错误就是configure阶段提示找不到各种库,解决办法就是缺什么装什么,然后重新configure。典型几个:
| 报错片段 | 缺的依赖 | 解决 |
|---|---|---|
| configure: error: xml2-config not found | libxml2-dev | apt-get install libxml2-dev |
| configure: error: Please reinstall the libcurl distribution | libcurl4-openssl-dev | apt-get install libcurl4-openssl-dev |
| configure: error: Cannot find OpenSSL's <evp.h> | libssl-dev | apt-get install libssl-dev |
| configure: error: Please reinstall the libzip distribution | libzip-dev | apt-get install libzip-dev |
| make: *** [sapi/cli/php] Error 1 | 编译内存不够 | 关闭部分扩展或增加swap |
还有一类问题容易出现在PHP 8.2以上编译时,提示configure被损坏或者autoconf版本太老。建议更新autoconf到2.69以上版本,实在不行就重新解压源码包再来一遍,不要在一个损坏的源码目录里反复折腾。
5.2 php-fpm启动不了,端口和Socket被占用
两个版本的php-fpm如果配置了相同监听端口或相同Socket路径,后启动的那个必然失败。排查命令:
ss -lntp | grep 9001 ls -l /run/php-fpm*.sock如果确认被占用,找到占用进程并停掉,再修改新版本的listen参数。修改完配置文件后,一定记得执行php-fpm -t验证配置语法:
/usr/local/php/8.1/sbin/php-fpm -t输出提示config test successful再重启服务。
5.3 Swoole扩展装到了错误的PHP版本里
这个问题十次有八次出在phpize路径上。手工编译扩展时,如果直接用系统PATH环境里的phpize,很可能指向了/usr/bin/phpize这个apt版本,和要装扩展的/usr/local/php/8.1完全是两个世界。解决方法是编译前先确认路径:
which phpize /usr/local/php/8.1/bin/phpize -v确保configure时--with-php-config也指向同一版本。装完后,在php.ini中添加extension=swoole.so,再用对应版本的php -m检查。如果加载失败,可以用php -i结合error_reporting查看底层报错,多半是版本不匹配或依赖库缺失。
5.4 Nginx访问返回502 Bad Gateway
502大概率是Nginx连不上php-fpm。先确认两点:第一,php-fpm进程是否活着,systemctl status php-fpm81看看;第二,Nginx配置的fastcgi_pass路径和php-fpm的listen路径是否完全一致,比如Socket文件是/run/php-fpm81.sock,Nginx里写成了/var/run/php-fpm81.sock,那就是一个常见的路径差异问题,检查Symlink指向就行。
还有一个经常被忽略的权限问题:Socket文件的属主和Nginx进程的运行用户不一致时,Nginx拿不到Socket访问权限,也会502。把Socket的listen.owner设置成和Nginx同属一个group即可。
5.5 命令行PHP版本切换后环境变量混乱
如果你用update-alternatives切换了默认php版本,但是系统里有些脚本写死了/usr/bin/php,或者依赖某个PHP版本的扩展,切换后就可能出现找不到函数、扩展缺失之类的报错。我的建议是:不要轻易切换系统默认php,让每个项目通过自己的Web服务socket固定对应版本的php-fpm,而命令行统一用php74、php81这样带版本号的前缀命令,避免影响线上的全局脚本。
6. 一些实操经验与建议
这套多版本方案到处跑下来,我个人有几个感受比较深。
第一,PHP版本隔离的关键不是“用哪个版本”,而是“每个版本的文件和进程都要待在自己独立的边界内”。目录独立、php.ini独立、php-fpm监听独立、systemd服务独立,这四层独立做到位,后面所有问题都引不起大波澜。
第二,别急着一次性装三个版本。如果刚接手一台生产服务器,建议先把一个新的PHP版本编译好,只跑一个测试项目,通了之后再加第二个。我第一次上手时图省事,一股脑把7.4、8.1、8.3同时编译,结果编译失败、扩展装错、Nginx配置漏了,三路一起报错,排查成本翻了好几倍。
第三,生产环境一定要留好回滚路径。每改一个Nginx配置、每装一个新扩展,都要记录旧版本内容。我在做PhpAsk部署时,就出现过Swoole扩展升级后Workder进程异常退出,最后直接切回旧扩展版本秒级恢复的经历。好的部署永远不是方案多漂亮,而是出了问题能不能快速救回来。
第四,做完多版本后,养成一个好习惯:对所有PHP版本和扩展做个清单,哪个项目用的哪个版本、监听哪个Socket、php.ini改过哪些参数,都写进项目部署文档里。你觉得自己能记住,但三个月后回来看,大概率会忘掉一半。这个清单能救你,也能救接手这台服务器的下一个人。
如果后续你还想在这个基础上调整,可以考虑再补充一个PHP 8.2版本专门测试新特性,或者研究一下用systemd定时器让Swoole常驻服务在崩溃后自动拉起,这些都是在多版本共存的框架上非常实用的小扩展。希望这篇内容能帮你少踩几个坑。