资讯动态

多版本PHP共存配置实战:Nginx路由与PHP-FPM隔离

发布时间:2026/10/22 19:29:04 来源:尧图企业网站定制
前阵子给 PhpAsk 做功能升级代码里要用上 PHP 8.2 的枚举和 match 表达式可线上某个历史模块还留在 PHP 5.6 上。直接把整站切过去不现实同一台服务器又不能同时跑两个版本然后让它们互相打架。折腾了几天后我把“多版本 PHP 共存”这件事彻底想明白了路径分开、进程分开、配置分开、请求路由分开剩下的都是细节。这篇文章就是把完整的配置思路和操作步骤写出来给同样需要给 PhpAsk 配置多版本 PHP 共存环境的朋友做个参考。1. 为什么同一个项目会需要多个 PHP 版本先看 PhpAsk 的真实处境1.1 一个问答系统里的“新老拉锯”PhpAsk 本身是一个基于 PHP 的问答系统代码量不算小历史包袱也不轻。早期模块用的是老框架依赖的第三方库只支持 PHP 5.6后来新增的接口和后台功能又用了不少 PHP 8.x 的新语法。如果强制把所有代码统一到同一个 PHP 版本要么老框架跑不起来要么新功能没法上线两边都难受。这种处境在真实项目里非常普遍尤其当你维护一个超过三年的项目时。“干脆把老模块重写”听得容易做起来是几周甚至几个月的工程量。相比之下让两个版本在同一台机器上同时存在、互不干扰是投入产出比最高的过渡方案。1.2 多版本共存的四个典型场景我梳理了实际会用到这套方案的场景不止 PhpAsk 一个项目适用本地联调电脑上同时有多个项目一个用了 PHP 7.1另一个用了 PHP 8.2每次手动切换 PATH 非常容易忘事直接共存最省心。线上灰度迁移新老代码需要跑在同一台服务器上通过 Nginx 把部分路径或子域名的请求转发到新版本对应的 PHP-FPM老路径继续走旧版本。CI 多版本测试给开源项目跑兼容性矩阵需要在同一台 Jenkins/GitLab Runner 上依次执行多个 PHP 版本共存环境比逐个安装虚拟机轻量得多。临时排障线上某老接口报错需要本地用旧 PHP 版本复现又不想影响正在开发的新功能。1.3 “共存”和“切换”是两码事先把目标定义清楚很多人一开始会把“多版本共存”和“多版本切换”搞混这直接导致后面的配置文件改得一团糟。“切换”是指同一个解释器入口在不同版本之间轮换典型做法是改 PATH 环境变量、用 update-alternatives 或各种版本管理工具。它的特点是某一时刻全局只有一个默认 PHP 生效。适合命令行工具开发但不适合 Web 服务长期同时运行。“共存”则是让多个 PHP 版本以独立的进程、独立的配置文件、独立的监听端口同时运行。Nginx 收到请求后可以根据站点、路径或参数把请求派发给不同的 PHP-FPM 实例。这才是生产环境真正需要的方案。本文讲的以共存为主命令行切换只是辅助。2. 拆解 PHP 版本这把“多合一”瑞士军刀2.1 一个 PHP 版本并不只是一个可执行文件想把共存配置做对先得知道一个“PHP 版本”到底由哪些部分组成。用表格列一下最直观组成对象常见默认位置共存时的冲突风险PHP 解释器二进制/usr/bin/php、/usr/local/bin/php命令行敲 php 时调错版本PHP-FPM 进程/usr/sbin/php-fpm多个 FPM 同时启动监听端口冲突php.ini 配置文件/etc/php/7.x/、/usr/local/etc/不同版本读了同一套配置行为全乱扩展动态库/usr/lib/php/、ext 目录版本不匹配扩展加载失败FPM 的 PID 和日志文件/run/php-fpm.pid、/var/log/php-fpm.log进程管理互相覆盖日志串线Session 目录/var/lib/php/sessions两个版本共用登录态串号你可以把一个 PHP 版本理解成一条完整的工作流水线可执行文件只是入口配置、扩展、进程管理文件都是这条流水线的一部分。共存的关键不是“装了两个 php 命令”而是把这条流水线上所有会碰撞的东西全部隔离开。2.2 CLI 和 FPM 是两个完全独立的进程入口新手最容易困惑的一点在服务器上执行php -v看到的是 8.2但浏览器访问站点时执行的还是 5.6为什么因为 CLI命令行接口和 FPMFastCGI 进程管理器是两套独立的入口。php -v使用的是命令行解释器路径由 PATH 环境变量决定而 Nginx 处理 PHP 请求时通过fastcgi_pass把请求转发给某个 PHP-FPM 的监听地址跟 PATH 一点关系都没有。做 Web 开发时真正对请求负责的是 PHP-FPM 实例。配置多版本共存时重心应该放在 FPM 的监听地址和配置隔离上其次才是命令行工具的便利性。2.3 影响“哪个版本生效”的三个关键配置点PATH 环境变量决定你在 shell 里敲php、composer、phpize时调用哪个目录下的程序。PHP 二进制编译参数--prefix、--with-config-file-path、--with-config-file-scan-dir决定了这个 PHP 默认去哪里找 php.ini 和扩展配置。这是版本隔离的根本。FPM 的 listen 配置决定 Nginx 的fastcgi_pass指向哪里是 127.0.0.1:9000 还是 127.0.0.1:9005是 TCP 端口还是 Unix Socket。后面所有操作都围绕这三件事展开。3. Linux 服务器实操让 PHP 5.6 和 PHP 8.2 在同一台机器上和平相处3.1 编译安装前的“分身”设计我先说结论最稳妥的做法是两个版本都从源码编译安装到完全不同的前缀目录。以--prefix/usr/local/php56和--prefix/usr/local/php82为例这样从二进制文件、php.ini 到扩展目录天然就是两套独立的文件树。以 Ubuntu 22.04 为例先准备编译环境apt update apt install -y build-essential libxml2-dev libssl-dev libcurl4-openssl-dev \ libsqlite3-dev libonig-dev libzip-dev libgd-dev libpng-dev libjpeg-dev \ libfreetype-dev systemd pkg-config然后下载并编译 PHP 5.6.40。要注意PHP 5.6 是很多年前发布的在较新的系统上编译会遇到 OpenSSL 1.1、libxml2 等兼容问题这是正常现象。如果只是为了快速复现可以直接跳到后面的 Docker 方案。下面是源码方式cd /usr/local/src curl -O https://www.php.net/distributions/php-5.6.40.tar.gz tar xzf php-5.6.40.tar.gz cd php-5.6.40 ./configure \ --prefix/usr/local/php56 \ --with-config-file-path/usr/local/php56/etc \ --with-config-file-scan-dir/usr/local/php56/etc/conf.d \ --enable-fpm \ --with-fpm-userwww-data --with-fpm-groupwww-data \ --with-mysqlmysqlnd --with-pdo-mysqlmysqlnd --with-mysqlimysqlnd \ --with-gd --with-jpeg-dir --with-png-dir --with-freetype-dir \ --with-openssl --with-curl --enable-mbstring --enable-zip make -j$(nproc) make installPHP 8.2 的编译参数和老版本不一样不能直接照抄。PHP 8 以后不再使用--with-mysql、--with-jpeg-dir这类老式参数cd /usr/local/src curl -O https://www.php.net/distributions/php-8.2.15.tar.gz tar xzf php-8.2.15.tar.gz cd php-8.2.15 ./configure \ --prefix/usr/local/php82 \ --with-config-file-path/usr/local/php82/etc \ --with-config-file-scan-dir/usr/local/php82/etc/conf.d \ --enable-fpm \ --with-fpm-userwww-data --with-fpm-groupwww-data \ --with-pdo-mysqlmysqlnd --with-mysqlimysqlnd \ --with-gd --with-openssl --with-curl --enable-mbstring --enable-zip make -j$(nproc) make install编译参数里最关键的是--prefix、--with-config-file-path和--with-config-file-scan-dir。前两个决定了程序装到哪里、去哪个目录找 php.ini第三个决定了额外加载的 .ini 片段目录。只要这三项分开后面就不会因为配置文件互相干扰而头疼。3.2 两个 PHP-FPM 的监听地址与服务管理安装完成后两个版本都带了自己的 php-fpm 可执行文件但默认监听地址很可能都是 127.0.0.1:9000。如果直接启动后启动的那个一定会报“Address already in use”。解决办法是修改各自 pool 配置里的 listen 参数。PHP 5.6 配置在/usr/local/php56/etc/php-fpm.d/www.conf; /usr/local/php56/etc/php-fpm.d/www.conf user www-data group www-data listen 127.0.0.1:9005PHP 8.2 配置在/usr/local/php82/etc/php-fpm.d/www.conf; /usr/local/php82/etc/php-fpm.d/www.conf user www-data group www-data listen 127.0.0.1:9008如果你更习惯用 Unix Socket也可以改成listen /run/php56/php-fpm.sock和listen /run/php82/php-fpm.sock记得提前创建对应目录并设置好权限Nginx 所在的用户需要能访问。为了方便 systemd 管理我建议把两个 FPM 注册成两个服务。创建/etc/systemd/system/php-fpm-56.service[Unit] DescriptionPHP 5.6 FPM for PhpAsk legacy Afternetwork.target [Service] Typeforking PIDFile/usr/local/php56/var/run/php-fpm.pid ExecStart/usr/local/php56/sbin/php-fpm --fpm-config /usr/local/php56/etc/php-fpm.conf ExecReload/bin/kill -USR2 $MAINPID PrivateTmptrue [Install] WantedBymulti-user.target同理创建php-fpm-82.service把路径替换成/usr/local/php82、PIDFile 换成/usr/local/php82/var/run/php-fpm.pid。PID 文件路径一定不能共用否则 systemd 会读到错误的进程号重载或重启时误杀另一个 PHP-FPM。启动并设置开机自启systemctl daemon-reload systemctl enable --now php-fpm-56 php-fpm-823.3 每个版本各用各的 php.ini 和扩展目录编译完成并安装后记得从源码包复制生产版 php.inicp /usr/local/src/php-5.6.40/php.ini-production /usr/local/php56/etc/php.ini cp /usr/local/src/php-8.2.15/php.ini-production /usr/local/php82/etc/php.ini然后分别修改两个 php.ini 里容易冲突的项。以下以 8.2 为例5.6 对应路径替换即可; /usr/local/php82/etc/php.ini extension_dir /usr/local/php82/lib/php/extensions/no-debug-non-zts-20230831 session.save_path /var/lib/php82/sessions date.timezone Asia/Shanghaisession 目录分开这一点很容易被忽略。两个版本共用同一个 session 目录的后果是同一个域名下登录状态可能在两个版本之间来回串用户刚登录好刷新一次就掉了。正确做法是提前建好目录并授权mkdir -p /var/lib/php56/sessions /var/lib/php82/sessions chown -R www-data:www-data /var/lib/php56 /var/lib/php82 chmod 733 /var/lib/php56/sessions /var/lib/php82/sessions同时把 conf.d 目录建好mkdir -p /usr/local/php56/etc/conf.d mkdir -p /usr/local/php82/etc/conf.d以后给某个版本单独加配置时往对应 conf.d 里放 .ini 文件就行互不影响。3.4 快速验证一套命令确认环境正常配置完成后验证要分两层命令行层和 FPM 层。# 命令行层 /usr/local/php56/bin/php -v | head -n 1 /usr/local/php82/bin/php -v | head -n 1 # FPM 进程层 ps aux | grep php-fpm # 端口监听层 ss -tlnp | grep -E 9005|9008命令行层验证的是两个二进制是否能独立运行。FPM 层验证的是两个进程是否正确监听不同端口。最后再用浏览器或 curl 访问两个测试页面分别写一个phpinfo()文件放到不同版本的站点根目录下看输出的 PHP Version 是否和预期一致。如果 curl 输出不对优先检查 Nginx 的fastcgi_pass是否指向了正确的端口不要一上来就怀疑 PHP 安装有问题。4. PhpAsk 站点路由Nginx 把请求送到正确的 PHP 版本4.1 不同站点走不同 PHP 版本server 级配置装上两个 PHP 版本后Nginx 是调度器的角色。最简单的情况是PhpAsk 主站跑 PHP 8.2历史遗留的管理后台跑 PHP 5.6分别用不同域名或不同 server 块。主站配置server { listen 80; server_name phpask.local; root /var/www/phpask; index index.php; location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9008; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }老后台配置server { listen 80; server_name legacy.phpask.local; root /var/www/phpask/legacy; index index.php; location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9005; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }这里唯一的实质区别就是fastcgi_pass指向的端口。整个 Nginx 配置里绝大多数内容都一样版本切换只发生在最终转发这一步理解这一点后排查问题思路会清晰很多。4.2 同一个站点按 URL 前缀切到另一个版本有时候不想再开一个域名而是希望同一个 PhpAsk 站点下/m/开头的路径走老版本 PHP其他路径走新版本。可以用 location 前缀匹配实现server { listen 80; server_name phpask.local; root /var/www/phpask; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9008; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ^~ /m/ { alias /var/www/phpask/legacy/; index index.php; location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9005; fastcgi_param SCRIPT_FILENAME $request_filename; } } }要注意alias和嵌套location的组合很容易踩坑SCRIPT_FILENAME用$request_filename比拼接$document_root$fastcgi_script_name更可靠。改完配置后记得nginx -t做语法检查再执行systemctl reload nginx。这种“同站不同路径不同版本”的模式非常适合上线前的灰度验证。先在/m/前缀下跑新版本代码观察日志和错误率确认稳定后再逐步扩大范围。4.3 CLI 侧怎么配合PATH、alias 和 Composer 的版本指定Web 请求走 FPM但日常维护和部署时你还是要用命令行。我的习惯是给两个版本做显式别名而不是频繁改 PATHalias php56/usr/local/php56/bin/php alias php82/usr/local/php82/bin/php如果希望打开 shell 时默认使用某个版本的 php就把它放在 PATH 的最前面export PATH/usr/local/php82/bin:$PATH需要特别注意 Composer。Composer 本身是一个 PHP 脚本它用哪个版本的 PHP 执行完全取决于你调用它时的解释器。用错了版本会出现“Composer 检测到 PHP 5.6但项目需要 PHP 8.2”之类的情况。正确的做法是显式指定/usr/local/php82/bin/php /usr/local/bin/composer install还可以在composer.json中设置 platform 检查让 Composer 在解析依赖时按指定版本判断/usr/local/php82/bin/php /usr/local/bin/composer config platform.php 8.2.0这个配置写成后即使当前 shell 的默认 php 是 5.6Composer 也会按 8.2 的平台要求去解析依赖避免装到不支持当前语法的老版本库。5. Windows 与 macOS 开发机的轻量级共存方案5.1 Windows多个 php-cgi 进程加 Nginx 端口分流Windows 上没有原生的 php-fpm开发环境通常用 php-cgi 配合 Nginx 使用。思路和 Linux 完全一致不同版本占不同端口。去 PHP 官网下载两个非线程安全版本的压缩包分别解压到C:\php56和C:\php82。把压缩包里的php.ini-development复制为php.ini并启用必要扩展extension_dir C:\php82\ext extensionmysqli extensionpdo_mysql extensioncurl extensionmbstring然后开两个命令窗口分别启动C:\php56\php-cgi.exe -b 127.0.0.1:9005 -c C:\php56\php.ini C:\php82\php-cgi.exe -b 127.0.0.1:9008 -c C:\php82\php.iniNginx 配置里的fastcgi_pass指向对应端口即可。要注意的是 php-cgi 是阻塞型进程命令窗口不能关而且 Windows 下路径不能有空格老 PHP 版本的配置文件名也可能不一样解压后先看看目录里实际是什么文件名。5.2 macOS用版本管理工具减少重复编译macOS 下的思路有两个方向。一个是直接用 phpbrew 这类版本管理工具它能把不同版本 PHP 编译到独立目录并通过命令切换 PATH。基本流程是phpbrew install 5.6.40 default fpm phpbrew install 8.2.15 default fpm phpbrew use php-5.6.40 phpbrew use php-8.2.15phpbrew 切换的是命令行默认版本FPM 的启动仍要手动控制。多个 FPM 同时运行时依然要注意监听端口和 PID 文件冲突。macOS 上编译老版本 PHP 的 OpenSSL 兼容问题比 Linux 更明显如果遇到编译失败不要死磕直接用 Docker 方案更快。5.3 用容器跑多版本一套 Compose 定义多个 FPM不管是 Windows、macOS 还是 LinuxDocker 都是目前最省心的多版本方案。最核心的好处是版本隔离做到极致不用编译、不会污染宿主机删掉容器后环境完全干净。假设 PhpAsk 代码在./www目录用一个docker-compose.yml定义两个 PHP-FPM 容器和一个 Nginx 容器services: php56: image: php:5.6-fpm-alpine volumes: - ./www:/var/www/html networks: - phpask php82: image: php:8.2-fpm-alpine volumes: - ./www:/var/www/html networks: - phpask nginx: image: nginx:1.24-alpine ports: - 8080:80 volumes: - ./www:/var/www/html - ./nginx/phpask.conf:/etc/nginx/conf.d/default.conf depends_on: - php56 - php82 networks: - phpask networks: phpask:容器内的 Nginx 不再通过127.0.0.1访问宿主机端口而是直接通过容器服务名解析到对应 PHP-FPM 容器比如fastcgi_pass php56:9000。两个容器的 9000 端口互不冲突因为它们在各自的网络命名空间里。Nginx 配置示例server { listen 80; server_name _; root /var/www/html; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ ^/legacy/.*\.php$ { include fastcgi_params; fastcgi_pass php56:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ \.php$ { include fastcgi_params; fastcgi_pass php82:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }启动命令就一行docker compose up -d如果容器里的 PHP 缺少某些扩展可以在 Dockerfile 里用docker-php-ext-install安装或者在php56和php82服务的build字段里指定各自镜像。每套环境都是独立的不会因为装一个扩展把另一个版本搞崩。6. 多版本环境里我踩过的五个坑含排查方法6.1 php -v 对不上PATH 优先级陷阱现象打开终端执行php -v显示的版本号和自己预期不一样但which php看到的路径明明是自己刚安装的目录。原因系统里可能同时存在/usr/bin/php、/usr/local/bin/php、/etc/alternatives/php等入口shell 执行命令时按 PATH 顺序查找不一定能找到你最新安装的那个。排查方法先执行which php看实际调用的路径再执行echo $PATH看目录顺序最后检查是否有软链接指向了其他版本的二进制。确认无误后再清理软链接或调整 PATH。6.2 扩展加载失败phpize 版本不匹配和 extension_dir 错误现象用源码安装扩展时./configure报错或安装成功后php -m里看不到扩展。原因调用了系统自带的phpize它属于另一个 PHP 版本生成的扩展文件自然加载不进当前版本。排查方法编译扩展前确认你用的是目标 PHP 自带的工具链/usr/local/php82/bin/phpize ./configure --with-php-config/usr/local/php82/bin/php-config make make install查看扩展目录是否匹配/usr/local/php82/bin/php -i | grep extension_dir6.3 9000 端口被占FPM 启动失败现象第二个 PHP-FPM 启动时一条错误日志提示端口已被占用。原因两个版本默认都监听 127.0.0.1:9000。排查方法先看端口占用情况ss -tlnp | grep 9000然后确认两个 pool 配置里的 listen 是否已经改成不同端口。如果之前启动过其中一个 FPM还要检查它的子进程是否残留先停掉再改配置重启。6.4 Composer 依赖检查不过platform 版本不对现象执行composer install时提示当前 PHP 版本不满足依赖要求但明明已经切换到新版 PHP 了。原因Composer 的记录缓存或 platform 配置里锁定了旧版本。排查方法看composer config platform.php输出的是什么。如果显示 5.6说明 platform 配置被写死了改成实际目标版本composer config platform.php 8.2.0如果只是临时安装不想改配置也可以跳过平台检查composer install --ignore-platform-reqphp这个方法只适合本地临时复现问题线上部署不要依赖它否则装出来的依赖可能和线上环境不兼容。6.5 session 和缓存目录串线登录态和配置互相污染现象同一个域名下一会儿能登录、一会儿又提示未登录另一个版本的配置改动莫名影响到了这个版本。原因两个 PHP 共用同一个 session.save_path或者 opcache 配置写在同一个文件树里。排查方法分别查看两个版本的 session 路径/usr/local/php56/bin/php -i | grep session.save_path /usr/local/php82/bin/php -i | grep session.save_path如果不一致按 3.3 节的方法建目录、改 php.ini、授权并重启对应 FPM。opcache 方面不同 PHP 版本的 opcache 是独立编译的扩展不会互相覆盖文件但如果两个版本共享同一个 opcache 目录配置也会出现奇怪问题建议让opcache.file_cache指向各自前缀下的独立目录。6.6 额外提醒日志文件也要分开最后补一个容易忽略但很影响排查效率的点两个 PHP-FPM 的错误日志、慢请求日志最好也分开。FPM 的error_log和 pool 里的slowlog都指向各自前缀目录。否则排查问题时日志里混着两个版本的报错完全分不清是哪一个进程吐出来的。把日志分隔清楚后续排障效率能提升一大截。这套方案我在 PhpAsk 的测试环境用了三个多月最大的体会是从一开始就把“身份区分”刻在命名和配置里比事后补救省心得多。新建一个版本时照着这套流程把前缀、端口、PID、日志、session 目录全部错开二十分钟左右就能再跑一套环境。多版本 PHP 共存其实没有太多高深技巧无非就是确认每一项公共资源都有唯一的归属然后把 Nginx 这个调度器的指路牌立清楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑