production 环境如何隐藏错误又记录日志?

解读

国内绝大多数互联网公司的线上 PHP 服务跑在 Linux + Nginx/Apache + PHP-FPM 的容器或物理机集群里,面试时考官想确认两点:

  1. 你能否让终端用户“看不到”任何敏感错误信息(防泄露、防扫描);
  2. 你能否把错误完整、结构化地留下来(排障、审计、告警)。
    回答必须兼顾“配置层”与“代码层”,并给出可落地的运维细节,否则会被追问“如果配置改错了怎么办”“日志太大怎么切”。

知识点

  1. php.ini 核心指令:display_errors、display_startup_errors、log_errors、error_log、error_reporting、ignore_repeated_errors、report_memleaks
  2. 与 Web 服务器耦合的输出:Nginx 的 fastcgi_hide_header/fastcgi_param PHP_VALUE;Apache 的 php_flag/php_admin_flag
  3. 用户自定义处理器:set_error_handler、set_exception_handler、register_shutdown_function,以及 PSR-3 LoggerInterface 实现(Monolog)
  4. 日志分级与字段:RFC5424(Emergency/Alert/Critical/Error/Warning/Notice/Info/Debug)+ 统一 trace_id(UUID 或 Snowflake)
  5. 日志落地方案:本地 syslog/rsyslog、Filebeat -> Kafka -> ELK、阿里云 SLS、腾讯云 CLS;按天/按大小切割(logrotate 或 rotatelogs)
  6. 安全合规:隐藏版本号(expose_php = Off)、关闭回显(X-Powered-By)、统一返回 500 空白页、防路径泄露
  7. 灰度与回滚:PHP-FPM 的 pool 配置支持 php_admin_flag 不可被 .htaccess 覆盖,上线前用 diff + git webhooks 自动校验
  8. 性能:生产环境开启 log_errors 但关闭 ignore_repeated_errors 可减少 IO;使用 syslog 的 imjournal 比写文件少一次磁盘 flush

答案

线上隐藏错误又记录日志,我分四步实施,代码与运维联动:

  1. 配置层(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 统一管理,避免写死磁盘。

  2. Web 服务器层
    Nginx:
    fastcgi_param PHP_VALUE "display_errors=Off";
    统一错误页:error_page 500 502 503 504 /50x.html; 保证用户只看到定制静态页。

  3. 代码层兜底
    在 Composer 的 autoload 之后立刻注入 Monolog:
    log=newLogger(api);log = new Logger('api'); log->pushHandler(new StreamHandler('php://stderr', Logger::ERROR)); // 走 php-fpm 的 stderr,被 systemd 收集
    set_error_handler(function (errno,errno, errstr, errfile,errfile, errline) use (log) { log->error('PHP error', ['errno'=>errno,msg=>errno,'msg'=>errstr,'file'=>errfile,line=>errfile,'line'=>errline,'trace_id'=>_SERVER['HTTP_X_TRACE_ID']??uniqid()]); return true; // 不再抛给 PHP 默认处理 }); set_exception_handler(function (e) use (log) { log->critical(e>getMessage(),[exception=>e->getMessage(), ['exception'=>e->getTraceAsString(),'trace_id'=>_SERVER['HTTP_X_TRACE_ID']??uniqid()]); http_response_code(500); echo ''; // 空白输出 }); register_shutdown_function(function () use (log) {
    err=errorgetlast();if(err = error_get_last(); if (err && in_array(err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { log->emergency('Fatal error', $err);
    }
    });

  4. 运维层
    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%。

拓展思考

  1. 多池隔离:同一台宿主机跑 A/B 两个业务,用 PHP-FPM 两个 pool,各自 error_log 指向 /var/log/php/A.log、/var/log/php/B.log,避免日志串扰。
  2. 日志采样:高并发接口(如 2w QPS)全量写盘易打爆磁盘,可在 Monolog 增加 SamplingHandler,按 1/1000 比例采样,同时把致命错误全量保留。
  3. 链路透传:把 trace_id 从 Nginx 层 $request_id 注入响应头,前端捕获后反馈给用户,客服收到投诉可直接在 Kibana 搜索该 ID,秒级定位。
  4. 配置热更新:使用 php-fpm 的 –test 先校验,再 SIGUSR2 平滑重载;配合 Ansible + GitLab CI,在 Merge Request 阶段跑 php -l + phpstan + iniscan,确保上线配置与代码均无低级错误。