PHP error_log高并发请求下日志行丢失问题排查
解决PHP多请求下error_log日志丢失的问题
这问题我之前帮同事排查过一模一样的情况,核心原因就是多进程写入的竞态冲突——PHP的error_log()函数默认是没有加锁机制的,当你用FPM运行API时,多个worker进程会同时处理客户端请求,要是它们同时往同一个日志文件写内容,操作系统的文件指针就会互相干扰,导致部分日志行被覆盖或者直接丢失。而业务功能正常是因为每个请求的处理都是在独立进程里完成的,和日志写入的冲突没关联。
下面给你几个实用的解决方案:
1. 封装带文件锁的自定义日志函数
手动用flock()给文件加排他锁,确保同一时间只有一个进程能写入日志。这样能从根源上避免冲突,代码示例如下:
function safe_error_log($message, $log_path = '/var/log/my_api.log') { // 以追加模式打开文件 $file_handle = fopen($log_path, 'a'); if (!$file_handle) { return false; } try { // 获取排他锁(LOCK_EX),阻塞直到拿到锁 flock($file_handle, LOCK_EX); // 格式化日志,建议加上时间、进程ID方便排查 $log_content = sprintf( "[%s] [PID:%d] %s%s", date('Y-m-d H:i:s'), getmypid(), $message, PHP_EOL ); fwrite($file_handle, $log_content); } finally { // 释放锁并关闭文件,确保即使报错也能释放资源 flock($file_handle, LOCK_UN); fclose($file_handle); } return true; }
使用的时候直接调用safe_error_log('请求处理完成')就行,比原生error_log()多了进程安全保障。
2. 使用成熟的日志库(推荐)
如果不想自己造轮子,直接用Monolog这类专业日志库,它内置了多进程安全的文件处理器,还支持日志分级、格式化、多渠道输出等功能。举个快速上手的例子:
// 先通过Composer安装Monolog:composer require monolog/monolog require 'vendor/autoload.php'; use Monolog\Logger; use Monolog\Handler\StreamHandler; // 创建日志实例 $logger = new Logger('api_service'); // 添加文件处理器,最后一个true参数就是开启文件锁 $logger->pushHandler( new StreamHandler( '/var/log/my_api.log', Logger::INFO, // 日志级别 true, // 冒泡设置 0664, // 文件权限 true // 开启文件锁,关键! ) ); // 写入日志,还能附带上下文信息 $logger->info('用户登录成功', ['user_id' => 456, 'ip' => $_SERVER['REMOTE_ADDR']]);
Monolog会帮你处理锁的获取与释放,比自己写的更健壮,后续扩展日志功能也更方便。
3. 改用系统日志或集中式日志服务
如果你的服务器支持syslog,可以直接把日志写到系统日志里,syslog本身是进程安全的:
error_log('API请求异常', LOG_SYSLOG, 'my_api');
要是你的API并发量很高,还可以考虑用ELK Stack、Graylog这类集中式日志系统,通过UDP/TCP把日志发送到后端服务,每个进程独立发送,完全避免本地文件写入的冲突,还能方便地做日志检索、分析。
额外小提示
- 别用
file_put_contents($file, $content, FILE_APPEND)替代error_log(),除非加上LOCK_EX参数:file_put_contents($file, $content, FILE_APPEND | LOCK_EX),否则一样会丢日志。 - 加锁会带来极轻微的性能开销,但对于常规API请求来说完全可以忽略;如果是超高并发场景,建议用异步日志或者集中式日志方案。
内容的提问来源于stack exchange,提问作者mps
相关产品推荐
相关产品推荐

