如果你有多个 PHP 进程写同一个日志文件,第一反应多半是:加锁。我曾经维护过一套用 supervisor 拉起来的多进程 PHP 消费队列脚本,8 个 worker 同时往一个日志文件里追加记录,线上的表现很典型——日志出现半截行、行与行互相拼贴、严重时还会丢记录。排查了两轮才反应过来,真正的问题不是“并发写该不该加锁”,而是我打开文件的方式根本不具备日志文件追加(Append-Only)语义。多进程 PHP 环境里,想要安全地往同一个文件写日志,最干净的做法不是自己维护锁,而是把“追加”这个动作交给操作系统去保证原子性。这篇文章就围绕这套方案展开,适合正在处理多进程日志、审计日志、或想搞懂 O_APPEND 底层原理的 PHPer 参考。
1. 日志损坏现场:先搞清楚多进程并发写为什么会坏
1.1 三个最常见的“坏日志”症状
我在排查线上问题时,把日志“写坏”的症状归成了三类,几乎每个踩坑的人都会遇到其中一种。
第一类是截断清空。某些代码为了“每次写最新日志”,习惯用 fopen($file, 'w') 来打开文件。如果是多个进程各自执行 fopen('w'),那么每次打开文件都会把文件长度清成 0,整个文件会变成几个进程的“拼图残骸”,日志行数远小于预期,甚至只剩最后一个进程写入的内容。更隐蔽的是有的代码用 file_put_contents($file, $log) 不加 FILE_APPEND,效果一样,每次都会先截断再写,多进程下日志基本等于没有。
第二类是半截行和内容拼贴。不少代码写日志喜欢分几次写,先写时间戳,再写 PID,再写消息内容和换行。比如:
php复制fwrite($fp, '[' . date('c') . '] ');
fwrite($fp, '[pid:' . getmypid() . '] ');
fwrite($fp, $message . "\n");
这 3 次 fwrite 在单进程里没问题,但在多进程里,进程 A 写完时间戳后未必能马上写完消息,进程 B 可能在这中间插入完整日志。最终文件里出现 [时间] 进程B的完整日志 [pid:123] 进程A的消息 这种行。
第三类是追尾式覆盖。有些开发者知道不能截断,于是用 `fopen($
