双缓冲日志不能用file_put_contents循环写,因其每次调用均触发系统调用、磁盘寻道和刷盘,吞吐仅几百条/秒且易丢日志;正确做法是主进程追加内存缓冲(splfixedarray)、子进程异步批量fwrite刷盘,通过管道通信与索引切换实现无锁解耦。

为什么双缓冲日志不能直接用 file_put_contents 循环写
因为高频写入时,每次 file_put_contents($file, $data, FILE_APPEND) 都会触发系统调用、磁盘寻道和缓冲区刷盘,实际吞吐常卡在几百条/秒,且容易因锁竞争导致丢日志。双缓冲本质是把“写”和“刷盘”解耦:一个缓冲区收日志,另一个异步刷到磁盘,交替切换。
spiral/roadrunner 或 amphp/amp 不适合纯原生场景
它们依赖扩展(如 rr SAPI 或 ext-uv),而你明确要 PHP 原生(仅 CLI + 标准扩展)。真正可落地的方案是:用 pcntl_fork() 启一个子进程专做刷盘,主进程只往内存缓冲追加;父子通过匿名管道通信,避免文件锁和轮询开销。
- 主进程用
stream_select()监听管道读端,不阻塞也不空转 - 子进程用
fwrite()批量写入,每次刷满 64KB 或超时 100ms 就落盘 - 缓冲区用
splFixedArray而非普通数组,避免写入时频繁 realloc
缓冲区切换必须用原子指针而非复制数据
常见错误是每次切换都 array_merge() 或 json_encode() 整个缓冲——这会瞬间吃光内存并引发 GC 暂停。正确做法是预分配两个 splFixedArray 实例,用一个整型变量 $activeBufferIndex 标识当前写入缓冲,切换时只交换索引:
// 初始化 $bufA = new SplFixedArray(10000); $bufB = new SplFixedArray(10000); $active = 0; // 写入时 $buf = $active === 0 ? $bufA : $bufB; $buf[$buf->count()] = $logLine; // 切换信号来时 $active = 1 - $active; // 原子翻转,无拷贝
注意:splFixedArray::count() 返回已写入长度,不是容量,别误用 getSize()。
子进程异常退出后主进程无法感知?加心跳保活
单纯靠 pcntl_waitpid($pid, $status, WNOHANG) 容易漏掉 SIGKILL 场景。应在管道写端设置 stream_set_blocking($pipeWrite, false),主进程每 500ms 尝试写一个心跳字节(如 "."),若 fwrite() 返回 false 或 errno === EPIPE,立刻 pcntl_fork() 重建子进程并重置缓冲。
- 不要用
register_shutdown_function()清理——它不保证在子进程崩溃时触发 - 子进程内务必
pcntl_signal_dispatch()处理SIGTERM,确保最后一批日志刷完再 exit - 日志路径需提前
mkdir -p并检查is_writable(),否则子进程 fwrite 失败静默丢数据
缓冲区大小、刷盘阈值、心跳间隔这些参数没有通用值,得按你的单条日志长度和峰值 QPS 实测调优——比如日志平均 200 字节,目标 5k/s,那缓冲至少设 8KB,刷盘阈值 4KB 起步。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











