production 环境如何隐藏错误又记录日志?
解读
国内绝大多数互联网公司的线上 PHP 服务跑在 Linux + Nginx/Apache + PHP-FPM 的容器或物理机集群里,面试时考官想确认两点:
- 你能否让终端用户“看不到”任何敏感错误信息(防泄露、防扫描);
- 你能否把错误完整、结构化地留下来(排障、审计、告警)。
回答必须兼顾“配置层”与“代码层”,并给出可落地的运维细节,否则会被追问“如果配置改错了怎么办”“日志太大怎么切”。
知识点
- php.ini 核心指令:display_errors、display_startup_errors、log_errors、error_log、error_reporting、ignore_repeated_errors、report_memleaks
- 与 Web 服务器耦合的输出:Nginx 的 fastcgi_hide_header/fastcgi_param PHP_VALUE;Apache 的 php_flag/php_admin_flag
- 用户自定义处理器:set_error_handler、set_exception_handler、register_shutdown_function,以及 PSR-3 LoggerInterface 实现(Monolog)
- 日志分级与字段:RFC5424(Emergency/Alert/Critical/Error/Warning/Notice/Info/Debug)+ 统一 trace_id(UUID 或 Snowflake)
- 日志落地方案:本地 syslog/rsyslog、Filebeat -> Kafka -> ELK、阿里云 SLS、腾讯云 CLS;按天/按大小切割(logrotate 或 rotatelogs)
- 安全合规:隐藏版本号(expose_php = Off)、关闭回显(X-Powered-By)、统一返回 500 空白页、防路径泄露
- 灰度与回滚:PHP-FPM 的 pool 配置支持 php_admin_flag 不可被 .htaccess 覆盖,上线前用 diff + git webhooks 自动校验
- 性能:生产环境开启 log_errors 但关闭 ignore_repeated_errors 可减少 IO;使用 syslog 的 imjournal 比写文件少一次磁盘 flush
答案
线上隐藏错误又记录日志,我分四步实施,代码与运维联动:
-
配置层(php.ini 或 PHP-FPM pool)
display_errors = Off
display_startup_errors = Off
expose_php = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php/php-error.log
ignore_repeated_errors = On
report_memleaks = On
说明:PHP-FPM 模式下用 php_admin_flag[display_errors] = Off 可防止开发误改;路径给 rsyslog 统一管理,避免写死磁盘。 -
Web 服务器层
Nginx:
fastcgi_param PHP_VALUE "display_errors=Off";
统一错误页:error_page 500 502 503 504 /50x.html; 保证用户只看到定制静态页。 -
代码层兜底
在 Composer 的 autoload 之后立刻注入 Monolog:
log->pushHandler(new StreamHandler('php://stderr', Logger::ERROR)); // 走 php-fpm 的 stderr,被 systemd 收集
set_error_handler(function (errstr, errline) use (log) { log->error('PHP error', ['errno'=>errstr,'file'=>errline,'trace_id'=>_SERVER['HTTP_X_TRACE_ID']??uniqid()]); return true; // 不再抛给 PHP 默认处理 }); set_exception_handler(function (e) use (log) { log->critical(e->getTraceAsString(),'trace_id'=>_SERVER['HTTP_X_TRACE_ID']??uniqid()]); http_response_code(500); echo ''; // 空白输出 }); register_shutdown_function(function () use (log) {
err && in_array(err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { log->emergency('Fatal error', $err);
}
}); -
运维层
logrotate 每日切档:
/var/log/php/*.log {
daily
rotate 15
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
/usr/bin/killall -USR1 rsyslogd
endscript
}
同时 Filebeat 监听该目录,写入 Kafka,Logstash 解析后入 Elasticsearch,Kibana 按 trace_id 检索;告警用 ElastAlert,错误级别 ≥Error 就飞书/钉钉。
结果:用户侧永远返回空白 500,内部所有错误带文件名、行号、trace_id 落盘并实时入 ELK,满足审计与排障,且性能损耗 <1%。
拓展思考
- 多池隔离:同一台宿主机跑 A/B 两个业务,用 PHP-FPM 两个 pool,各自 error_log 指向 /var/log/php/A.log、/var/log/php/B.log,避免日志串扰。
- 日志采样:高并发接口(如 2w QPS)全量写盘易打爆磁盘,可在 Monolog 增加 SamplingHandler,按 1/1000 比例采样,同时把致命错误全量保留。
- 链路透传:把 trace_id 从 Nginx 层 $request_id 注入响应头,前端捕获后反馈给用户,客服收到投诉可直接在 Kibana 搜索该 ID,秒级定位。
- 配置热更新:使用 php-fpm 的 –test 先校验,再 SIGUSR2 平滑重载;配合 Ansible + GitLab CI,在 Merge Request 阶段跑
php -l+phpstan+iniscan,确保上线配置与代码均无低级错误。