不安全,多进程下易错乱;应加flock排他锁或用error_log()、file_put_contents(..., file_append | lock_ex)确保原子性,并配合外部logrotate轮转。

PHP用fopen追加写入日志文件是否安全?
直接用fopen($file, 'a')写日志在低并发场景下可行,但多进程/多请求同时写入时极易出现内容错乱(比如两行日志挤在同一行或截断)。PHP本身不保证'a'模式的原子性,底层依赖系统调用,而Linux的open(O_APPEND)虽能保证单次write()原子,但PHP的fwrite()可能分多次系统调用——尤其当开启输出缓冲或日志内容含换行符时。
实操建议:
- 必须加上
flock($fp, LOCK_EX)排他锁,写完再flock($fp, LOCK_UN) - 避免长时间持有锁:先拼好整条日志(含时间、级别、消息),再一次性
fwrite() - 打开文件后立即检查
is_writable(),失败时不要静默忽略 - 日志路径别用相对路径,统一用
__DIR__ . '/logs/app.log'这类绝对路径
为什么error_log()比手动写文件更省心?
error_log()是PHP内置的日志函数,底层由Zend引擎调度,对并发写入做了优化(例如在CLI/SAPI层有轻量锁或缓冲策略),且天然支持写入系统日志(syslog)、邮件(mail)或自定义目标。它不依赖用户代码加锁,也不容易因忘记fclose()导致句柄泄漏。
常见用法:
立即学习“PHP免费学习笔记(深入)”;
- 写入指定文件:
error_log("[INFO] User login\n", 3, "/var/log/myapp.log") - 写入Web服务器错误日志(如Apache的
error_log):error_log("DB timeout", 0) - 触发
syslog:openlog("myapp", LOG_PID, LOG_USER); error_log("Config loaded", 4); closelog();
注意:error_log(..., 3, $file)方式仍需确保$file目录可写,且PHP进程有权限创建该文件(首次写入时)。
如何用file_put_contents()实现原子日志写入?
file_put_contents()配合FILE_APPEND | LOCK_EX标志,是比fopen+flock更简洁的选择。它内部自动处理打开、锁定、写入、关闭全流程,且PHP 7.1+已确保该组合的原子性。
关键点:
- 必须显式传
LOCK_EX,否则并发下仍可能错乱 - 日志内容末尾要自己加换行符,
file_put_contents()不会自动补 - 返回值为写入字节数,建议判断是否等于
strlen($log),不等则说明写入异常(如磁盘满) - 示例:
file_put_contents('/logs/app.log', date('Y-m-d H:i:s') . " [DEBUG] {$msg}\n", FILE_APPEND | LOCK_EX);
日志轮转和大小控制不能靠“手写逻辑”
用filesize()判断再rename()归档,在高并发下极不可靠:两个请求同时读到文件大小超限,都执行rename(),结果一个覆盖另一个,或直接报错Permission denied。
更稳妥的做法:
- 用
logrotate(Linux)或rotatelogs(Apache)做外部轮转,PHP只负责写入单一文件 - 若必须PHP内控,改用时间维度轮转(如按天生成
app-2024-06-15.log),避免大小判断竞争 - 慎用
touch()或stat()查修改时间——NFS或容器环境可能有缓存延迟
真正难的不是写一行日志,而是让十万次写入不丢、不错、不卡、不爆盘。锁、路径、权限、轮转,漏掉任何一个,上线后都得半夜爬起来修。











