首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WordPress 站点升 PHP 主版本:兼容性检查、灰度发布和回滚怎么做

WordPress 站点升 PHP 主版本:兼容性检查、灰度发布和回滚怎么做

原创
作者头像
用户10345225
发布于 2026-11-01 21:57:03
发布于 2026-11-01 21:57:03
1860
举报

WordPress 站点的 PHP 版本升级,出问题的往往不是代码本身,而是你以为已经换掉、实际上还在跑旧版本的那个进程。

一个晚上把镜像切到新 PHP,页面 500。php -v 显示新版本,FPM 的 pool 里还是旧的——这在容器化的 WordPress 里很常见,因为 Web 走 php-fpm,CLI 走 php,队列 worker 是 supervisor 拉起来的另一个进程,三者的镜像或 ini 完全可以不一样。

下面按「动手前确认什么 → 怎么扫代码 → 非生产怎么验 → 回滚和灰度怎么做 → 出事看哪里」的顺序写。

先分清 SAPI:你在终端敲的 php 和浏览器访问到的不是同一个进程

CLI 和 FPM 是两套配置,容器里还可能是两个镜像。

代码语言:bash
复制
php -v
php --ini
php-fpm -v                        # 有些镜像里 fpm 是独立包,版本未必跟 CLI 同步
php-fpm -i | grep -E 'Loaded Configuration|Scan this dir|PHP Version'

Web 侧别靠猜,让 WordPress 自己报。放一个 mu-plugin:

代码语言:php
复制
<?php
// wp-content/mu-plugins/00-runtime-info.php
add_action('init', function () {
    if (!isset($_GET['rt']) || !current_user_can('manage_options')) {
        return;
    }
    header('Content-Type: text/plain; charset=utf-8');
    echo 'PHP_VERSION=' . PHP_VERSION . "\n";
    echo 'PHP_SAPI=' . PHP_SAPI . "\n";
    echo 'PHP_BINARY=' . PHP_BINARY . "\n";
    echo 'ini=' . php_ini_loaded_file() . "\n";
    echo 'opcache=' . (ini_get('opcache.enable') ? 'on' : 'off') . "\n";
    exit;
});

登录管理员后访问 /?rt=1。PHP_SAPI 应该是 fpm-fcgi;如果显示 cli 或者 apache2handler,说明你改的地方根本不是实际处理请求的那一层。

容器和 K8s:

代码语言:bash
复制
docker ps --format '{{.Names}}\t{{.Image}}' | grep -i -E 'wp|php|wordpress'
docker exec -it <container> php -v
docker exec -it <container> sh -c 'readlink -f /proc/1/exe'   # 1 号进程到底是不是 php-fpm
docker inspect <container> --format '{{.Config.Image}}'       # 镜像 tag 是不是你以为的那个

kubectl exec deploy/wp-web -- php-fpm -v
kubectl get deploy wp-web -o jsonpath='{.spec.template.spec.containers[*].image}'

镜像 tag 用 latest 的话,这一层就没法确认了。改成固定 tag(git sha 最好),回滚才有东西可切。

把旧运行时的扩展清单抄下来,再动手

「我记得装了 gd」是升级后最常见的翻车来源之一。切换前先在旧运行时上导出清单,进仓库:

代码语言:bash
复制
mkdir -p runtime
php -m | sort > runtime/php-ext-old.txt
php -r 'foreach (get_loaded_extensions() as $e) { printf("%s %s\n", $e, phpversion($e) ?: "-"); }' \
  | sort > runtime/php-ext-old-full.txt

新运行时上再导一次,然后比对:

代码语言:bash
复制
php -m | sort > runtime/php-ext-new.txt
comm -23 runtime/php-ext-old.txt runtime/php-ext-new.txt     # 旧有新无,逐个确认
diff -u runtime/php-ext-old-full.txt runtime/php-ext-new-full.txt

WordPress 跑起来至少要看这几个:mysqli(或 pdo_mysql)、mbstring、curl、gd 或 imagick、zip、xml、fileinfo、intl、exif、opcache。用 WP-CLI 直接问更快:

代码语言:bash
复制
wp --info
wp eval 'foreach (["mysqli","mbstring","curl","gd","imagick","zip","intl","fileinfo"] as $e) {
    printf("%-10s %s\n", $e, extension_loaded($e) ? "ok" : "MISSING");
}'

intl 和 imagick 这类扩展在大版本之间换过底层库,从系统包安装时要确认对应的 so 文件能加载——php -m 里没有、php -i | grep -i warning 里有 "Unable to load dynamic library",就是编译或链接没对上。这种情况一般要重装扩展包,把旧环境里的 .so 拷过去通常没用。

用静态分析扫一遍主题和插件

代码层的问题绝大多数能被 PHPCompatibility 扫出来,别等到运行时才发现。

代码语言:bash
复制
composer require --dev \
  phpcompatibility/php-compatibility \
  dealerdirect/phpcodesniffer-composer-installer

TARGET_PHP="$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')"

vendor/bin/phpcs -p --standard=PHPCompatibility \
  --runtime-set testVersion "$TARGET_PHP-" \
  wp-content/themes/my-theme \
  wp-content/plugins/my-plugin

testVersion 传的是目标版本区间,上面这行从当前 CLI 的版本推出来,避免手抄错。报告里优先处理 ERROR 级别,WARNING 里重点看「deprecated since」这类——它们在当前版本还能跑,下一个小版本就可能变成移除。

再补一轮粗暴但有效的文本搜索,覆盖被移除或改行为的函数:

代码语言:bash
复制
grep -rnE '\b(each|create_function|ereg|eregi|split|mysql_[a-z_]+|get_magic_quotes_gpc|utf8_encode|utf8_decode)\s*\(' \
  wp-content/themes wp-content/plugins \
  --include='*.php' | head -50

还有两类不报错但会出问题:

  • 字符串里的 ${var} 写法,新版本会抛废弃提示,日志一多就把真的错误淹了。
  • 给未声明属性的对象赋值(动态属性),新版本也会抛废弃提示,主题和插件的老代码里常见。

WordPress 自己的几个兼容点

主题和插件里 functions.php、inc/ 下的老代码是重灾区,先扫。

drop-in 文件要单独看:wp-content/object-cache.php、advanced-cache.php、db.php 在 WordPress 核心加载之前就执行,它们出问题会直接白屏,而且报错位置看着跟 WordPress 无关。

插件逐个启用,不要一次性全开——全开出问题时你没法定位是哪一个。

代码语言:bash
复制
wp plugin list --status=active --fields=name,status --format=table

在 staging 上先全部停用,核心跑通后按依赖顺序一个个启回来。

CLI、队列和定时任务:最容易漏的几个入口

Web 通过 /?rt=1 验完,不代表另外几条路也换了。

代码语言:bash
复制
php -v                                   # 你 ssh 进去时的 CLI
wp --info | grep -i php                  # WP-CLI 实际用的解释器
which -a php                             # 有没有多个 PHP 并存
supervisorctl status                     # 队列 worker 是哪个进程在跑

supervisor 配置里把解释器路径写死的情况非常多:

代码语言:ini
复制
[program:wp-queue]
command=/usr/local/bin/php /var/www/html/wp queue-worker --max-time=3600
directory=/var/www/html
autostart=true
autorestart=true

镜像升级后 /usr/local/bin/php 可能指向新版本而 web 容器还是旧的,或者反过来。统一成镜像内的绝对路径,并且在部署脚本里断言一次:worker 启动时把 PHP_VERSION 打一行进日志。

定时任务有两条路:

代码语言:bash
复制
crontab -l
which -a php

php /var/www/html/wp-cron.php 走 CLI 的版本;curl http://127.0.0.1/wp-cron.php 走 FPM 的版本。这两条要分别确认。容器环境里如果有独立的 cron sidecar,它也是自己的一份 PHP。

Composer 和部署产物的一致性

本地用旧版本跑 composer install 再把 vendor/ 传上去,是把旧平台的依赖解析结果带进新运行时——某些包会按 platform.php 的配置装不同分支。

代码语言:bash
复制
# 按目标运行时锁平台
composer config platform.php "$(php -r 'echo PHP_VERSION;')"
composer update --lock

CI 里构建产物就用目标版本的镜像跑:

代码语言:bash
复制
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction

构建出来的镜像 tag 跟 git sha 对齐,latest 只做别名。这样回滚就是换 tag,不用重建。

回滚要分三层准备,数据库单独算

升级之前先做一次数据库转储,别指望回滚代码能顺带回滚数据:

代码语言:bash
复制
TS=$(date +%F-%H%M)
mysqldump --single-transaction --quick --default-character-set=utf8mb4 \
  -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" \
  | gzip > "backup/pre-php-upgrade-$TS.sql.gz"

三层分开看:

  • 镜像与代码:旧 tag 必须还在 registry 里。很多仓库有保留策略,几天就把旧 tag 清了,出事时没得切。
  • 运行时配置:php.ini、FPM pool、opcache 参数都随镜像走,不要临时在宿主机上改——回滚时改过的东西不会跟着回去。
  • 数据库:升级 PHP 本身不动 schema,但同一窗口里升了插件就会。插件建的表、加的行、写进去的新格式(比如序列化结构变了)旧代码未必认。

最省事的做法是把 PHP 版本升级和插件升级拆成两个维护窗口。如果业务上必须一起做,记下窗口开始时间,回滚时把窗口内的新写入单独导出来留档:

代码语言:bash
复制
mysqldump --single-transaction --where="post_modified_gmt >= '2026-11-01 02:00:00'" \
  "$DB_NAME" wp_posts > rollback-window-writes.sql

升级完之后要不要重载 FPM?要。opcache.validate_timestamps=0 的环境里,不重载的话新代码根本不生效,你会以为升级失败。

代码语言:bash
复制
php-fpm -t                 # 先验配置语法
systemctl reload php-fpm   # 或者 docker restart <container>

站里如果没人能接这次升级,AumCreate 做 WordPress 站的部署和定制开发,运行时版本、扩展清单、插件依赖这几项是在交付前一起过一遍的内容,范围和报价写在 aumcreate.com/services。有人手自己上的话,上面这些脚本留在仓库里更省事。

灰度:先把一小部分流量放到新池

按权重切:

代码语言:nginx
复制
upstream wp_php {
    server 127.0.0.1:9000 weight=9;   # 旧池
    server 127.0.0.1:9001 weight=1;   # 新池
}

更稳的是按 cookie 分流,让内部人先全量走新池:

代码语言:nginx
复制
upstream wp_php_old { server 127.0.0.1:9000; }
upstream wp_php_new { server 127.0.0.1:9001; }

map $cookie_php_pool $wp_upstream {
    default  "wp_php_old";
    "new"    "wp_php_new";
}

server {
    location ~ \.php$ {
        fastcgi_pass $wp_upstream;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

权重按 9:1 → 1:1 → 全量推进,每一步至少观察一个完整的定时任务周期。对账、日结、清理任务往往是半夜跑的,白天看不出问题。

观察要看的东西:

代码语言:bash
复制
tail -F /var/log/php-fpm/*.log | grep -E 'PHP (Fatal|Parse) error|Uncaught'
  • Nginx 的 5xx 比例,按 upstream 分开看,别混在一起。
  • 慢日志里新增的堆栈。
  • opcache 命中率:wp eval 'var_dump(opcache_get_status()["opcache_statistics"]);'。新池刚启动命中率天然低,看趋势不看绝对值。

白屏或 500 时,先分清是哪一层挂了

代码语言:bash
复制
curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/
  • 502 / 504:FPM 没起来、池子满了、或者 upstream 地址写错。先 php-fpm -t 验配置,再看 docker logs / systemctl status。这种情况 PHP 代码根本没执行,去翻代码是白费时间。
  • 500:PHP 执行了并抛了错误。看 FPM 的 error_log、Nginx 的 error_log,以及 WordPress 自己的 wp-content/debug.log。

打开 WordPress 调试前先关掉页面输出,免得报错被访客看到:

代码语言:php
复制
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

如果全站白屏且日志干净,两个方向:单个文件语法错误,或者 opcache 里还是旧的编译结果(重启 FPM 而不是 reload)。

代码语言:bash
复制
find wp-content/plugins wp-content/themes -name '*.php' -print0 \
  | xargs -0 -n1 -P4 php -l 2>&1 | grep -v 'No syntax errors'

升级本身不复杂,难的是把「到底哪个进程在跑哪个版本」这件事确认清楚。做完一遍之后,把那几个检查脚本留在仓库里,下次升级照着跑就行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先分清 SAPI:你在终端敲的 php 和浏览器访问到的不是同一个进程
  • 把旧运行时的扩展清单抄下来,再动手
  • 用静态分析扫一遍主题和插件
  • WordPress 自己的几个兼容点
  • CLI、队列和定时任务:最容易漏的几个入口
  • Composer 和部署产物的一致性
  • 回滚要分三层准备,数据库单独算
  • 灰度:先把一小部分流量放到新池
  • 白屏或 500 时,先分清是哪一层挂了
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档