fopen + fwrite 在 swoole 多进程下丢数据,因各 worker 进程独立持有文件句柄与偏移,posix 文件 i/o 无内置锁,write() 竞态导致覆盖或错位;'a' 模式不保证原子追加,超 pipe_buf(4kb)即可能截断重叠。

为什么 fopen + fwrite 在 Swoole 多进程下会丢数据?
因为 Swoole 的 Worker 进程是真正独立的 OS 进程,每个进程都持有自己对文件的打开句柄和写入偏移(offset),而普通文件没有内置锁机制。当多个进程同时用 fopen(..., 'a') 打开同一文件并写入,内核的 write() 系统调用可能因竞态导致覆盖或错位——不是 PHP 层面的问题,是 POSIX 文件 I/O 本身的限制。
常见现象:tail -f 看日志断断续续、行首缺失、两行内容挤在同一行、甚至整条记录消失。
- 即使用了
'a'模式,也不能保证原子追加:Linux 下write()对普通文件小于PIPE_BUF(通常 4KB)才保证原子性,超长内容仍可能被截断重叠 -
flock()在多进程间有效,但必须每次写前显式加锁、写后解锁,且所有进程必须用同一锁文件路径 - 不要依赖
file_put_contents($file, $data, FILE_APPEND)—— 它底层仍是fopen+fwrite,没自动加锁
用 stream_socket_client 转发到单进程日志服务更可靠
把写文件这件事从 Worker 进程里剥离出去,交给一个专用的 Manager 或 Task 进程处理,Worker 只负责发消息。这是 Swoole 场景下最推荐的解法,既避免锁竞争,又不影响高并发性能。
核心思路:Worker 进程通过 Unix Socket 或 TCP 向日志进程发送结构化日志(如 JSON),日志进程串行写入磁盘。
- 日志进程用
Swoole\Process启动,监听unix:///tmp/log.sock,收到消息后统一fopen(..., 'a')+fwrite - Worker 发送时用
stream_socket_client()连接,失败需降级(比如暂存内存或写临时文件) - 注意设置 socket 超时和缓冲区大小,防止日志进程卡住拖慢整个服务:
stream_set_timeout($fp, 0.1)
flock() 能用,但要注意三个硬约束
如果必须让 Worker 自己写文件,flock() 是唯一可行的原生方案,但它有明确的使用边界,踩错一个就失效。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 文件必须以非
'c'模式打开('a'或'w'都可以),且flock()必须在fopen()之后、fwrite()之前调用 - 锁是「建议性锁」(advisory lock),所有写该文件的进程都得主动调用
flock(),漏掉一个就全崩 - 进程退出或
fclose()会自动释放锁,但若进程崩溃未 fclose,锁可能残留(Linux 下一般会自动清理,但别依赖) - 示例关键片段:
$fp = fopen('/var/log/app.log', 'a'); if (flock($fp, LOCK_EX)) { fwrite($fp, date('Y-m-d H:i:s') . " $msg\n"); fflush($fp); // 强制刷盘,避免缓存延迟 flock($fp, LOCK_UN); } fclose($fp);
别碰 file_put_contents 的 LOCK_EX 参数
看起来很省事:file_put_contents($file, $data, FILE_APPEND | LOCK_EX),但实际在 Swoole 多进程下基本无效。
原因在于 file_put_contents 每次调用都会重新 fopen → flock → write → fclose,而 flock 的锁生命周期只在当前文件指针存在期间有效。进程 A 刚 fclose,进程 B 就可能抢到锁并开始写,中间毫无保护——这根本不是并发安全的写法。
更糟的是,频繁打开关闭文件句柄会显著增加系统调用开销,在高 QPS 场景下反而成为瓶颈。
真正要并发安全地落地日志,要么走单点日志服务,要么用 flock 配合长连接文件句柄复用。其他“一行解决”的方案,基本都在生产环境翻过车。










