ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP7.4本地正常线上报错怎么排查

PHP7.4本地正常线上报错怎么排查 前言本地跑得好好的一上线就报错几乎是每个 PHP 工程师都会撞上的场景。典型症状有三种接口直接返回 500 白屏页面能出来但功能悄悄失效比如上传的图片永远 404或者最折磨人的——线上什么都不显示display_errors关着日志里也只有一行PHP Fatal error。把同样的代码、同样的数据库拿到本地一切正常。这类问题的根因几乎从来不在业务代码里而在本地环境和线上环境的差异上。差异无非几个来源PHP 小版本与编译参数、加载的扩展、文件系统的大小写敏感性、目录权限与open_basedir、INI 配置、以及代码发布方式OPcache 是否重载。它们共同的特点是在本地是恰好成立的在线上是恰好不成立的所以本地测不出来。本文给出一套可复制的排查流程先用一个脚本把两个环境的关键差异全部打出来做对比再按错误类型逐个击破。示例代码基于 PHP 7.4 编写7.4 兼容语法未使用 8.0 的联合类型、命名参数等特性可直接保存成.php运行。一、先分类你遇到的到底是哪一类错不同症状对应完全不同的排查路径先对号入座能省一大半时间。症状高度怀疑对象最快的验证手段500 白屏 / 空白页display_errorsOff且未看日志致命错误发生在输出之前看 PHP-FPM 慢日志与error_log临时ini_set(display_errors, 1)局部功能报错、报错信息指向某个函数扩展缺失mbstring、intl、gd、fileinfo、redisphp -m对比报错文本含 Call to undefined function明明存在的类/文件报 No such file文件系统大小写敏感Windows 本地 vs Linux 线上require的路径大小写与磁盘实际文件名逐字符比对上传/写缓存失败目录属主、open_basedir、SELinux打印is_writable()与ini_get(open_basedir)改动已发布但行为还是旧的OPcachevalidate_timestamps0未重载 FPMopcache_reset()或 reloadphp-fpm时间/金额差一点date.timezone、default_charset不同对比php -i中的date.timezone症状一白屏不是没报错是错被藏起来了很多人的第一反应是线上没报错啊其实只是display_errors被关掉了。此时错误仍然产生只是写进了日志。第一步永远是定位日志而不是猜代码。# 找到 PHP 的错误日志实际落在哪里 php -i | grep -E error_log|display_errors|log_errors # 看 PHP-FPM 的错误输出路径以实际配置为准 tail -n 100 /var/log/php-fpm/error.log # 看 nginx 的 5xx 记录与 upstream 报错 tail -n 100 /var/log/nginx/error.log # 若是 systemd 管理的 FPM错误可能被 journald 收走 journalctl -u php-fpm -n 100 --no-pager如果日志也是空的说明错误发生在还没来得及写日志的阶段比如open_basedir拦截、扩展加载失败导致 FPM 子进程起不来这时应该去看 FPM 的启动日志和php-fpm -t的配置检查结果。症状二报错文本本身就是答案报错信息里的关键词能直接定位到差异源不要跳过它去翻代码Call to undefined function mb_strlen()→ 线上没装mbstring或该函数被写进了disable_functions。Class Redis not found→redis扩展未加载。failed to open stream: Permission denied→ 目录权限或 SELinux。open_basedir restriction in effect→ 代码访问了open_basedir之外的路径比如sys_get_temp_dir()指向的/tmp被限制。The server requested authentication method unknown to the clientPDO/MySQL → 线上 MySQL 8 的认证插件与客户端库不匹配。二、实战一个把你的环境拍扁成文本的脚本与其一条条猜不如把两个环境的关键信息都导出成同样格式的文本然后diff一把。下面这个脚本只用 PHP 7.4 就有的 API本地和线上各跑一次即可。?php // env-diff.php —— 本地与线上各执行一次然后 diff 两份输出 // 兼容 PHP 7.4。用法php env-diff.php env-local.txt $rows []; // 1) 运行时基本信息 $rows[PHP_VERSION] PHP_VERSION; $rows[PHP_SAPI] PHP_SAPI; $rows[PHP_OS_FAMILY] PHP_OS_FAMILY; // PHP 7.2 可用 $rows[PHP_INT_SIZE] PHP_INT_SIZE . bytes; $rows[date.timezone] (string) ini_get(date.timezone); // 2) 最容易踩坑的 INI 项 $iniKeys [ display_errors, error_reporting, log_errors, error_log, memory_limit, max_execution_time, post_max_size, upload_max_filesize, open_basedir, disable_functions, opcache.enable, opcache.validate_timestamps, default_charset, short_open_tag, include_path, ]; foreach ($iniKeys as $k) { $rows[ini. . $k] var_export(ini_get($k), true); } // 3) 已加载扩展排序后拼成一行diff 时一眼看出谁多谁少 $exts get_loaded_extensions(); sort($exts); $rows[ext.count] count($exts); $rows[ext.list] implode(,, $exts); // 4) 文件系统大小写敏感性探针 $probe __DIR__ . /CaseProbe.php; file_put_contents($probe, ?php return ok;\n); // 写成 CaseProbe.php却用小写去问存在 不区分大小写 $rows[fs.case_sensitive] is_file(__DIR__ . /caseprobe.php) ? no : yes; unlink($probe); // 5) 换行符与临时目录 $rows[fs.eol] (PHP_EOL \n) ? LF : CRLF; $rows[fs.tmp] sys_get_temp_dir(); $rows[fs.tmp_writable] is_writable(sys_get_temp_dir()) ? yes : no; // 6) 统一宽度输出方便肉眼比对 $width 0; foreach ($rows as $k $v) { $width max($width, strlen($k)); } foreach ($rows as $k $v) { printf(%-{$width}s : %s\n, $k, $v); }# 本地 php env-diff.php env-local.txt # 线上用 ssh 跑注意远端 php 路径可能不是 /usr/bin/php ssh deployprod cd /www/app php env-diff.php env-prod.txt # 只关心差异行 diff -u env-local.txt env-prod.txt输出的差异会直接指向问题。比如-PHP_VERSION : 7.4.33 PHP_VERSION : 7.4.3 ... -PHP_INT_SIZE : 8 bytes PHP_INT_SIZE : 4 bytes ... -fs.case_sensitive : no fs.case_sensitive : yes第二组差异PHP_INT_SIZE从 8 变成 4说明线上是 32 位 build这会让intval()在大 ID 上静默溢出、crc32()/ip2long()结果与本地不同——通常是编译参数被人改过本身就该修正。除了这个脚本还有两条命令值得固化到运维流程里# 校验依赖声明的平台要求composer.json 里的 require.ext-* / php 约束 composer check-platform-reqs --no-dev # 确认线上真的没装某个扩展而不是被 disable_functions 关掉 php -r var_dump(function_exists(mb_strlen), extension_loaded(mbstring));composer check-platform-reqs是排查扩展缺失类问题最快的一招它会把composer.json里声明的平台依赖逐条核对直接告诉你线上缺了哪个扩展或 PHP 版本不达标。三、把差异落回代码五类高频真凶拿到差异数据之后剩下的就是改代码或改环境。按出现频率排1. 路径大小写Windows 本地 / Linux 线上这是本地正常线上报错的头号原因占了我见过的一半以上。Windows 的 NTFS 默认不区分大小写require App/Models/User.php和实际文件app/models/user.php在本地都能加载Linux 的 ext4 严格区分线上立刻Failed opening required。// ❌ 硬编码路径大小写与实际文件名不一致时只有 Linux 会挂 require_once App/Models/User.php; // ✅ 用 __DIR__ 与磁盘完全一致的大小写并让 Composer 自动加载 require_once __DIR__ . /app/Models/User.php;更稳的做法是全部交给 PSR-4 自动加载autoload并在 CI 里加一条大小写一致性检查从源头掐掉这个问题。2.include_path与当前工作目录PHP-FPM 下getcwd()通常是脚本所在目录取决于fpm的chdir设置而 CLI 下是执行命令时所在的目录两者常常不同。// ❌ 依赖 cwd 或 include_path 的相对引入 require config/database.php; // ✅ 以文件自身位置为基准绝对路径 require __DIR__ . /config/database.php;3. 目录权限、open_basedir与 SELinux本地的storage/是你自己的用户线上跑 FPM 的是www-data。写失败时file_put_contents()返回false而不是抛异常很多人不看返回值于是上传成功但文件不见了。$target /www/app/storage/uploads/; // ❌ 不检查返回值失败被静默吞掉 file_put_contents($target . $name, $data); // ✅ 失败要立刻抛出把真实原因带出来 if (!is_dir($target) || !is_writable($target)) { throw new RuntimeException(sprintf( 目录不可写: %s (dir%s, writable%s, open_basedir%s), $target, var_export(is_dir($target), true), var_export(is_writable($target), true), ini_get(open_basedir) )); } if (file_put_contents($target . $name, $data) false) { throw new RuntimeException(写入失败: . $target . $name); }如果is_writable()返回true但写入仍然失败把注意力转向 SELinuxgetenforce返回Enforcing时chcon -R -t httpd_sys_rw_content_t storage/往往是解药。4. 配置与代码不同步OPcache 与.env线上常把opcache.validate_timestamps设为0PHP 只在 FPM 启动时读一次文件。发布完代码不 reload跑的还是旧字节码改了没生效就是这么来的。# 发布后的标准动作 php-fpm -t systemctl reload php-fpm # 或者在部署脚本里调用需要 FPM 内有相应权限/端点 php -r if (function_exists(opcache_reset)) { opcache_reset(); echo reset ok\n; }.env同理线上.env缺失某个键时getenv()返回false而本地因为有你手工加的键跑得通。上线前应当用composer check-platform-reqs同级的思路给必需的环境变量加一道启动校验。5. 数据库与字符集差异本地 MySQL 5.7、线上 MySQL 8.0是最常见的组合。差异点集中在认证插件、默认字符集5.7 是latin18.0 是utf8mb4以及 SQL 模式ONLY_FULL_GROUP_BY在 8.0 默认开启。SQL 模式这条特别隐蔽本地能跑的SELECT a, b FROM t GROUP BY a线上直接报1055。// ✅ 显式设置连接字符集不依赖服务端默认值 $pdo new PDO( mysql:host127.0.0.1;dbnameshop;charsetutf8mb4, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES false, ] );常见坑点1. 只开display_errors却忘了它是运行时设置白屏时已经太晚// ❌ 致命错误发生在 ini_set 执行之前比如语法错误、扩展缺失根本走不到这行 ini_set(display_errors, 1);; ✅ 排查期直接在 php.ini / pool 配置里打开并对生产环境只开日志 display_errors Off log_errors On error_log /var/log/php-fpm/app-error.log2. 用$_SERVER[DOCUMENT_ROOT]拼绝对路径// ❌ nginx PHP-FPM 下 DOCUMENT_ROOT 可能为空或与预期不符 require $_SERVER[DOCUMENT_ROOT] . /../vendor/autoload.php;// ✅ 用文件位置定位与 Web 服务器配置解耦 require dirname(__DIR__) . /vendor/autoload.php;3. 排查完忘了关调试开关把堆栈和数据库口令一起输出到线上// ❌ 提交进仓库的排错开关 define(DEBUG, true); if (DEBUG) { var_dump($pdo, $config); }// ✅ 从环境变量读默认关闭且只记录不输出 $debug getenv(APP_DEBUG) 1; if ($debug) { error_log(print_r($config, true)); }4. 拿php -v的版本当结论忽略了 SAPI 差异# ❌ CLI 是 7.4FPM 用的可能是另一个二进制 php -v# ✅ 分别确认 CLI 与 FPM 实际加载的版本和 ini php -v php-fpm -v php -i | grep -E Loaded Configuration File|Scan this dir # 走 HTTP 再看一次确认 FPM 真正用的版本 curl -s https://example.com/probe.php # probe.php 内输出 phpversion() 与 php_ini_loaded_file()5. 认为本地也装了同样的扩展就不查扩展// ❌ 依赖扩展提供的行为却不校验缺扩展时行为静默降级 $len mb_strlen($name);// ✅ 启动时集中断言必需扩展缺失立刻暴露 foreach ([mbstring, json, pdo_mysql, fileinfo] as $ext) { if (!extension_loaded($ext)) { throw new RuntimeException(缺少必需扩展: {$ext}); } }6. 换行符与 BOM 导致的 headers already sent// ❌ 文件以 UTF-8 BOM 开头或 ? 之后留了空行 ?php header(Location: /login);// ✅ 纯 PHP 文件不写结束标签保存为 UTF-8 无 BOM ?php header(Location: /login); exit;7. 时区不同导致日期差一天被当成业务 bug// ❌ 依赖运行环境的默认时区 echo date(Y-m-d); // 本地 UTC8 是今天线上 UTC 还是昨天// ✅ 代码里显式固定时区或用 UTC 存、展示时再转换 date_default_timezone_set(Asia/Shanghai); echo (new DateTimeImmutable(now, new DateTimeZone(UTC)))-format(Y-m-d);总结排查本地正常、线上报错的流程可以固化成一个固定套路步骤动作产出1看日志不看代码拿到真实的报错文本与堆栈2跑env-diff.php本地/线上各一次一份可直接diff的环境快照3composer check-platform-reqs确认缺失的平台依赖与扩展4按报错关键词归类扩展/大小写/权限/配置/数据库锁定差异源5改代码或改环境并把校验固化进部署流程问题不再复发核心结论只有一句本地正常从来不是证据环境一致才是。把 PHP 版本、扩展列表、INI 关键项、文件系统大小写敏感性这四样东西在本地与线上对齐绝大多数线上才报错的问题会在发布前就暴露出来剩下的部分则应该靠启动时的显式校验缺扩展即抛异常、目录不可写即抛异常而不是靠运气来兜底。
返回列表