很多开发者认为“写个日志而已,能有多慢?”,但在 PHP-FPM 高并发模型下,同步写文件往往是 QPS 上不去的罪魁祸首。本文从系统底层详细剖析其影响。
PHP 的 fopen 或 file_put_contents 在追加写入模式(FILE_APPEND)下,为了保证多进程写入不串行(日志内容交织在一起),必须进行加锁。
当你的代码执行 Yii::info('msg') 并最终触发写盘时,底层发生了什么?
# 伪代码 strace 追踪
openat(..., "app.log", O_WRONLY|O_CREAT|O_APPEND, ...) = 5 # 打开文件
flock(5, LOCK_EX) # 【关键】请求排他锁
lseek(5, 0, SEEK_END) # 定位到末尾
write(5, "Timestamp [info] msg\n", ...) # 写入数据
flock(5, LOCK_UN) # 释放锁
close(5) # 关闭文件
假设一个 PHP 请求只需 10ms 处理业务,但在最后写日志时需要持有锁 0.1ms (SSD)。
flock(LOCK_EX) 上。状态从 Running 变为 Waiting。PHP-FPM 是典型的同步阻塞模型。
write() 调用在数据没从 Page Cache 刷入(或至少拷贝到内核态)之前不会返回。write() 拷贝内存的时间开销也会显现。每次 I/O 操作(即使是写缓存)都涉及用户态到内核态的切换。频繁的小日志写入(比如循环里打日志)会带来大量的 Context Switches,消耗 CPU 时间片,导致 CPU 即使不满载,处理能力也下降。
假设环境:AWS EC2, GP2 SSD (IOPS 3000), PHP-FPM 100 Workers.
| 场景 | 日志策略 | 单次写入耗时 | 锁竞争概率 | QPS 上限估算 |
|---|---|---|---|---|
| 无日志 | 不写 | 0ms | 0% | 2000+ (受 CPU 限制) |
| 异步队列 | 推送 Redis | 0.2ms | < 1% | 1800+ |
| 高效文件 | SSD + Buffer 1000 | 0.5ms (均摊) | 低 | 1600+ |
| 低效文件 | SSD + 实时写入 | 2ms | 中 | 800-1200 |
| 灾难现场 | HDD/云盘 + 实时 | 10ms+ | 极高 (所有进程卡死) | < 100 |
如果一次请求不仅写 app.log,还要同时写 payment.log, trace.log,性能损耗不是简单的线性叠加,而是倍增。
假设 1000 QPS,每个请求写 3 个文件:
write(A) -> write(B) -> write(C)如果必须分文件,请务必使用 异步队列 方案。在 PHP 侧只推送到唯一的队列(顺序 IO/内存操作),由消费端去慢慢分发到不同的文件。
Yii2 的 exportInterval (默认 1000) 实际上是在做 Application Level Buffering。
文件代码日志对性能的影响是 非线性 的。
这就是为什么生产环境严禁开启 DEBUG 级别日志,且在高并发核心链路推荐使用 Kafka/Redis 异步日志 彻底剥离 I/O 压力的根本原因。
本文由 best 创作,采用 知识共享署名 3.0 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。