同步写 error_log 会卡住 PHP 请求,因其采用阻塞式文件 I/O,高并发下磁盘成瓶颈;审计日志须异步化,禁用 error_log 直接记录,改用消息队列、syslog 或带缓冲的 StreamHandler,并精简内容、规范 logrotate。

error_log 写太多会拖慢请求,不是日志“该不该记”,而是“谁来记、怎么记、记什么”。
为什么同步写 error_log 会卡住 PHP 请求
PHP 默认用 error_log() 或配置 error_log = /path/to/file 时,是同步、阻塞式文件 I/O。每次调用都要打开文件、seek 到末尾、写入、flush、关闭——在高并发下,磁盘 I/O 成为瓶颈,实测可使接口耗时翻倍。
-
error_log()比file_put_contents(..., FILE_APPEND)略快,但仍是同步行为 - 若日志路径挂载在机械硬盘或 NFS 上,延迟更明显
- 多进程(如 php-fpm worker)同时写同一文件,还可能触发内核级锁竞争
别指望靠“加 SSD”解决——根本问题是同步路径没解耦。
把审计日志从 error_log 迁移到异步通道
审计类日志(如登录失败、权限变更、SQL 注入尝试)和错误日志性质不同:它不用于即时排障,而用于事后追溯与合规检查。必须和业务请求生命周期解耦。
立即学习“PHP免费学习笔记(深入)”;
- 禁用
error_log直接写审计事件:删掉所有类似error_log("user_id=123 login failed")的调用 - 改用消息队列缓冲:用
amqp_publish()(RabbitMQ)、kafka_produce()(rdkafka 扩展)或syslog(LOG_WARNING, ...),后两者由系统守护进程异步落盘 - 若必须用文件,至少换用
StreamHandler+RotatingFileHandler(Monolog),并设置buffer_size => 100启用内存缓冲
注意:syslog() 不等于“丢给系统不管”——它走 Unix domain socket,由 rsyslogd 或 syslog-ng 异步处理,压测显示比文件写快 3 倍以上。
审计内容本身要轻量,别记全量上下文
很多团队把整个 $_POST、$e->getTraceAsString() 往审计日志里 dump,这会导致:
- 序列化开销飙升(尤其含 resource 或递归引用时)
- 日志体积暴涨,后续传输/存储/解析全受影响
- 敏感字段(如密码、token)意外泄露
只保留四个核心字段:timestamp、user_id(或 ip)、action(如 "auth.login.fail")、outcome("blocked" / "rate_limited")。其余一律摘要化:
- 请求体用
md5(serialize($_POST))替代原文 - 异常堆栈只取前 3 层:
array_slice($e->getTrace(), 0, 3) - 数据库语句只记
preg_replace('/\s+/', ' ', substr($sql, 0, 128))截断+空格压缩
logrotate 配置必须带 delaycompress 和 copytruncate
审计日志量大,轮转时若直接 rename 文件,正在写的 PHP 进程会继续往旧 inode 写,导致日志丢失或分散到两个文件。
正确配置节选(/etc/logrotate.d/php-audit):
/var/log/php/audit.log {
daily
missingok
rotate 30
compress
delaycompress
copytruncate
create 644 www-data adm
sharedscripts
postrotate
systemctl kill --signal=USR1 rsyslog.service 2>/dev/null || true
endscript
}copytruncate 是关键:先拷贝再清空原文件,保证 PHP 进程始终有文件可写;delaycompress 防止刚轮转完就压缩,留出排查窗口。
审计日志的“量”从来不是问题,问题在于是否把它当成业务请求的一部分来处理——只要它还和 fastcgi_finish_request() 之前的操作绑在一起,性能损耗就无法回避。



















