WordPress 站点的 PHP 版本升级,出问题的往往不是代码本身,而是你以为已经换掉、实际上还在跑旧版本的那个进程。
一个晚上把镜像切到新 PHP,页面 500。php -v 显示新版本,FPM 的 pool 里还是旧的——这在容器化的 WordPress 里很常见,因为 Web 走 php-fpm,CLI 走 php,队列 worker 是 supervisor 拉起来的另一个进程,三者的镜像或 ini 完全可以不一样。
下面按「动手前确认什么 → 怎么扫代码 → 非生产怎么验 → 回滚和灰度怎么做 → 出事看哪里」的顺序写。
CLI 和 FPM 是两套配置,容器里还可能是两个镜像。
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
// 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:
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」是升级后最常见的翻车来源之一。切换前先在旧运行时上导出清单,进仓库:
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新运行时上再导一次,然后比对:
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.txtWordPress 跑起来至少要看这几个:mysqli(或 pdo_mysql)、mbstring、curl、gd 或 imagick、zip、xml、fileinfo、intl、exif、opcache。用 WP-CLI 直接问更快:
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 扫出来,别等到运行时才发现。
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-plugintestVersion 传的是目标版本区间,上面这行从当前 CLI 的版本推出来,避免手抄错。报告里优先处理 ERROR 级别,WARNING 里重点看「deprecated since」这类——它们在当前版本还能跑,下一个小版本就可能变成移除。
再补一轮粗暴但有效的文本搜索,覆盖被移除或改行为的函数:
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} 写法,新版本会抛废弃提示,日志一多就把真的错误淹了。主题和插件里 functions.php、inc/ 下的老代码是重灾区,先扫。
drop-in 文件要单独看:wp-content/object-cache.php、advanced-cache.php、db.php 在 WordPress 核心加载之前就执行,它们出问题会直接白屏,而且报错位置看着跟 WordPress 无关。
插件逐个启用,不要一次性全开——全开出问题时你没法定位是哪一个。
wp plugin list --status=active --fields=name,status --format=table在 staging 上先全部停用,核心跑通后按依赖顺序一个个启回来。
Web 通过 /?rt=1 验完,不代表另外几条路也换了。
php -v # 你 ssh 进去时的 CLI
wp --info | grep -i php # WP-CLI 实际用的解释器
which -a php # 有没有多个 PHP 并存
supervisorctl status # 队列 worker 是哪个进程在跑supervisor 配置里把解释器路径写死的情况非常多:
[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 打一行进日志。
定时任务有两条路:
crontab -l
which -a phpphp /var/www/html/wp-cron.php 走 CLI 的版本;curl http://127.0.0.1/wp-cron.php 走 FPM 的版本。这两条要分别确认。容器环境里如果有独立的 cron sidecar,它也是自己的一份 PHP。
本地用旧版本跑 composer install 再把 vendor/ 传上去,是把旧平台的依赖解析结果带进新运行时——某些包会按 platform.php 的配置装不同分支。
# 按目标运行时锁平台
composer config platform.php "$(php -r 'echo PHP_VERSION;')"
composer update --lockCI 里构建产物就用目标版本的镜像跑:
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction构建出来的镜像 tag 跟 git sha 对齐,latest 只做别名。这样回滚就是换 tag,不用重建。
升级之前先做一次数据库转储,别指望回滚代码能顺带回滚数据:
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"三层分开看:
最省事的做法是把 PHP 版本升级和插件升级拆成两个维护窗口。如果业务上必须一起做,记下窗口开始时间,回滚时把窗口内的新写入单独导出来留档:
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 的环境里,不重载的话新代码根本不生效,你会以为升级失败。
php-fpm -t # 先验配置语法
systemctl reload php-fpm # 或者 docker restart <container>站里如果没人能接这次升级,AumCreate 做 WordPress 站的部署和定制开发,运行时版本、扩展清单、插件依赖这几项是在交付前一起过一遍的内容,范围和报价写在 aumcreate.com/services。有人手自己上的话,上面这些脚本留在仓库里更省事。
按权重切:
upstream wp_php {
server 127.0.0.1:9000 weight=9; # 旧池
server 127.0.0.1:9001 weight=1; # 新池
}更稳的是按 cookie 分流,让内部人先全量走新池:
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 → 全量推进,每一步至少观察一个完整的定时任务周期。对账、日结、清理任务往往是半夜跑的,白天看不出问题。
观察要看的东西:
tail -F /var/log/php-fpm/*.log | grep -E 'PHP (Fatal|Parse) error|Uncaught'wp eval 'var_dump(opcache_get_status()["opcache_statistics"]);'。新池刚启动命中率天然低,看趋势不看绝对值。curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/php-fpm -t 验配置,再看 docker logs / systemctl status。这种情况 PHP 代码根本没执行,去翻代码是白费时间。error_log、Nginx 的 error_log,以及 WordPress 自己的 wp-content/debug.log。打开 WordPress 调试前先关掉页面输出,免得报错被访客看到:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);如果全站白屏且日志干净,两个方向:单个文件语法错误,或者 opcache 里还是旧的编译结果(重启 FPM 而不是 reload)。
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 删除。