PHP 异步日志写入

wen PHP项目 3

本文目录导读:

PHP 异步日志写入

  1. 使用消息队列(推荐)
  2. 基于 RabbitMQ(更可靠)
  3. 使用 Swoole 协程(高性能)
  4. 简化版:使用 fastcgi_finish_request()
  5. 使用第三方服务
  6. 最佳实践建议
  7. 总结对比

在 PHP 中实现异步日志写入,主要有以下几种方案,我按推荐程度和使用场景来说明:

使用消息队列(推荐)

Redis 队列方案

<?php
class AsyncLogger {
    private $redis;
    private $queueKey;
    public function __construct($redisConfig, $queueKey = 'php_logs') {
        $this->redis = new Redis();
        $this->redis->connect($redisConfig['host'], $redisConfig['port']);
        $this->queueKey = $queueKey;
    }
    // 写入日志(异步,立即返回)
    public function log($level, $message, array $context = []) {
        $logData = [
            'timestamp' => date('Y-m-d H:i:s'),
            'level' => $level,
            'message' => $message,
            'context' => json_encode($context),
            'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
            'uri' => $_SERVER['REQUEST_URI'] ?? ''
        ];
        // 推送到 Redis 队列,LPUSH 后立即返回
        $this->redis->lPush($this->queueKey, json_encode($logData));
        return true;
    }
}
// 使用示例
$logger = new AsyncLogger(['host' => '127.0.0.1', 'port' => 6379]);
$logger->log('error', '数据库连接失败', ['db' => 'user', 'code' => 500]);

消费者脚本(单独进程运行)

<?php
// consumer.php - 通过 cron 或 supervisor 运行
class LogConsumer {
    private $redis;
    private $queueKey;
    private $logFile;
    private $batchSize = 100;
    public function __construct($redisConfig, $logFile) {
        $this->redis = new Redis();
        $this->redis->connect($redisConfig['host'], $redisConfig['port']);
        $this->queueKey = 'php_logs';
        $this->logFile = $logFile;
    }
    public function consume() {
        while (true) {
            $logs = [];
            // 批量取出日志
            for ($i = 0; $i < $this->batchSize; $i++) {
                $log = $this->redis->rPop($this->queueKey);
                if ($log === false) break;
                $logs[] = json_decode($log, true);
            }
            if (empty($logs)) {
                sleep(1); // 队列为空时等待
                continue;
            }
            $this->writeToFile($logs);
        }
    }
    private function writeToFile(array $logs) {
        $content = '';
        foreach ($logs as $log) {
            $content .= sprintf(
                "[%s] %s: %s %s\n",
                $log['timestamp'],
                strtoupper($log['level']),
                $log['message'],
                $log['context'] ? ' | Context: ' . $log['context'] : ''
            );
        }
        // 使用 flock 防止并发写入冲突
        $fp = fopen($this->logFile, 'a');
        if (flock($fp, LOCK_EX)) {
            fwrite($fp, $content);
            flock($fp, LOCK_UN);
        }
        fclose($fp);
    }
}
// 运行消费者
$consumer = new LogConsumer(
    ['host' => '127.0.0.1', 'port' => 6379],
    '/var/log/app/application.log'
);
$consumer->consume();

基于 RabbitMQ(更可靠)

<?php
class RabbitMQLogger {
    private $connection;
    private $channel;
    public function __construct($config) {
        $this->connection = new AMQPConnection($config);
        $this->channel = new AMQPChannel($this->connection);
        // 创建交换机
        $exchange = new AMQPExchange($this->channel);
        $exchange->setName('logs_exchange');
        $exchange->setType(AMQP_EX_TYPE_TOPIC);
        $exchange->declareExchange();
        // 创建队列
        $queue = new AMQPQueue($this->channel);
        $queue->setName('logs_queue');
        $queue->declareQueue();
        $queue->bind('logs_exchange', 'log.*');
    }
    public function log($level, $message) {
        $data = [
            'timestamp' => time(),
            'level' => $level,
            'message' => $message
        ];
        $exchange = new AMQPExchange($this->channel);
        $exchange->publish(
            json_encode($data),
            'log.' . $level,
            AMQP_NOPARAM,
            ['delivery_mode' => 2] // 持久化消息
        );
        return true;
    }
    public function __destruct() {
        $this->channel->close();
        $this->connection->close();
    }
}

使用 Swoole 协程(高性能)

<?php
// 需要安装 Swoole 扩展
class SwooleAsyncLogger {
    private $logDir;
    public function __construct($logDir) {
        $this->logDir = $logDir;
        Swoole\Runtime::enableCoroutine();
    }
    public function log($level, $message, array $context = []) {
        $logData = [
            'timestamp' => date('Y-m-d H:i:s'),
            'level' => $level,
            'message' => $message,
            'context' => $context
        ];
        // 使用协程异步写入
        go(function() use ($logData) {
            $file = $this->logDir . '/' . date('Y-m-d') . '.log';
            $content = json_encode($logData) . PHP_EOL;
            // 使用异步文件写入
            $fp = fopen($file, 'a');
            if (flock($fp, LOCK_EX)) {
                fwrite($fp, $content);
                flock($fp, LOCK_UN);
            }
            fclose($fp);
        });
        return true;
    }
}

简化版:使用 fastcgi_finish_request()

如果只是想在前端快速响应,但仍在同一请求周期内写入:

<?php
class NonBlockingLogger {
    public function log($message) {
        // 使用 fastcgi_finish_request() 后仍在执行
        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request(); // 先发送响应给客户端
        }
        // 这里在响应发送后才执行
        usleep(100000); // 模拟耗时操作
        error_log($message);
        return true;
    }
}

使用第三方服务

日志文件守护进程方案

<?php
class FileAsyncLogger {
    private $logFile;
    public function __construct($logFile) {
        $this->logFile = $logFile;
    }
    public function log($level, $message) {
        // 使用非阻塞文件写入
        $fp = fopen($this->logFile, 'a');
        stream_set_blocking($fp, false); // 设置为非阻塞
        $data = sprintf(
            "[%s] [%s] %s\n",
            date('Y-m-d H:i:s'),
            strtoupper($level),
            $message
        );
        fwrite($fp, $data);
        fclose($fp);
        return true;
    }
}

最佳实践建议

日志级别配置

<?php
class LoggerConfig {
    const DEBUG = 100;
    const INFO = 200;
    const WARNING = 300;
    const ERROR = 400;
    const CRITICAL = 500;
    private $minLevel = self::INFO;
    public function shouldLog($level) {
        return $level >= $this->minLevel;
    }
}

内存缓冲区(批量写入)

<?php
class BufferedLogger {
    private $buffer = [];
    private $maxBufferSize = 50;
    private $flushThreshold = 10; // 秒
    public function log($level, $message) {
        $this->buffer[] = [
            'timestamp' => microtime(true),
            'level' => $level,
            'message' => $message
        ];
        // 达到缓冲区大小立即刷新
        if (count($this->buffer) >= $this->maxBufferSize) {
            $this->flush();
        }
        // 注册关闭时的刷新
        register_shutdown_function([$this, 'flush']);
        return true;
    }
    private function flush() {
        if (empty($this->buffer)) return;
        // 批量写入 Redis 或文件
        $logs = $this->buffer;
        $this->buffer = [];
        // 实际的写入操作...
        error_log(json_encode($logs));
    }
}

总结对比

方案 可靠性 性能 复杂度 适用场景
Redis 队列 生产环境首选
RabbitMQ 很高 中高 大规模分布式系统
Swoole 协程 很高 高性能应用
fastcgi_finish_request 简单场景
文件非阻塞 极低 开发环境

推荐方案:生产环境中,使用 Redis 队列 + 消费者进程 是最成熟可靠的方案,对于简单的应用,可以先使用 error_log + 文件系统监控,然后逐步演进。

抱歉,评论功能暂时关闭!