PHP 高并发日志不丢

wen PHP项目 3

本文目录导读:

PHP 高并发日志不丢

  1. 核心问题分析
  2. 最佳实践方案
  3. 生产级配置建议
  4. 性能与可靠性权衡
  5. 终极方案(组合拳)
  6. 监控与保障

在PHP高并发场景下确保日志不丢失,需要从架构设计、写入策略、缓冲机制三个层面综合解决,以下是完整的解决方案:

核心问题分析

高并发下日志丢失的主要原因:

  • 多进程/线程同时写入同一文件(竞态条件)
  • 频繁的I/O阻塞导致进程被kill时数据丢失
  • 系统崩溃/断电导致缓冲未刷盘
  • 日志文件过大或权限问题导致写入失败

最佳实践方案

方案1:使用高性能日志库(推荐)

// 使用 Monolog + RotatingFileHandler + buffer 配置
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\BufferHandler;
use Monolog\Formatter\JsonFormatter;
$logger = new Logger('app');
// 启用缓冲,每100条或1024KB写入一次
$handler = new RotatingFileHandler(
    '/var/log/app/app.log',   // 日志路径
    30,                         // 保留30天
    Logger::DEBUG
);
// JSON格式化,便于ELK收集
$handler->setFormatter(new JsonFormatter());
// 缓冲处理器 - 关键:减少磁盘I/O
$bufferHandler = new BufferHandler(
    $handler,
    100,        // 缓冲100条
    Logger::DEBUG
);
$logger->pushHandler($bufferHandler);

方案2:异步日志写入(推荐生产环境)

// 使用 ReactPHP 或 Swoole 实现异步日志
class AsyncLogger {
    private static $instance;
    private $queue;
    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    private function __construct() {
        $this->queue = new \SplQueue();
        // 注册进程退出时强制刷盘
        register_shutdown_function([$this, 'flushOnShutdown']);
    }
    public function log($message, $level = 'INFO') {
        $this->queue->enqueue([
            'time' => time(),
            'level' => $level,
            'message' => $message,
            'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)
        ]);
        // 达到阈值时批量写入
        if ($this->queue->count() >= 50) {
            $this->flush();
        }
    }
    public function flush() {
        if ($this->queue->isEmpty()) return;
        $logs = [];
        while (!$this->queue->isEmpty()) {
            $logs[] = $this->queue->dequeue();
        }
        // 使用JSON数组一次性写入
        $file = '/var/log/app/async_' . date('Ymd') . '.log';
        $content = json_encode($logs) . PHP_EOL;
        file_put_contents($file, $content, FILE_APPEND | LOCK_EX);
    }
    public function flushOnShutdown() {
        $this->flush(); // 确保退出前写入所有日志
    }
}

方案3:Redis缓冲 + 定时落盘

class RedisLogBuffer {
    private $redis;
    private $key = 'log:buffer';
    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
        // 使用pipeline优化
    }
    public function log($message) {
        $logEntry = json_encode([
            'time' => microtime(true),
            'message' => $message,
            'pid' => getmypid()
        ]);
        // 使用List数据结构,天然支持高并发
        $this->redis->rPush($this->key, $logEntry);
        // 定期触发落盘(每10秒)
        if ($this->redis->llen($this->key) > 500) {
            $this->flushToDisk();
        }
    }
    private function flushToDisk() {
        // 原子性取出所有日志
        $logs = $this->redis->lRange($this->key, 0, -1);
        $this->redis->del($this->key);
        $file = '/var/log/app/redis_' . date('Ymd') . '.log';
        file_put_contents($file, implode(PHP_EOL, $logs) . PHP_EOL, FILE_APPEND);
    }
}

方案4:文件锁 + chunked 写入

class SafeFileLogger {
    private $filePath;
    public function __construct($filePath) {
        $this->filePath = $filePath;
        $this->ensureDirectory();
    }
    public function write(array $logs) {
        $chunked = array_chunk($logs, 100); // 每次写100条
        foreach ($chunked as $chunk) {
            $this->writeChunk($chunk);
        }
    }
    private function writeChunk(array $logs) {
        $fp = fopen($this->filePath, 'a');
        if (!$fp) {
            throw new RuntimeException('无法打开日志文件');
        }
        // 使用 flock 确保原子写
        if (flock($fp, LOCK_EX)) {
            foreach ($logs as $log) {
                fwrite($fp, json_encode($log) . PHP_EOL);
            }
            fflush($fp);         // 刷到内核缓冲
            fsync($fp);          // 强制刷到磁盘(可选,性能消耗大)
            flock($fp, LOCK_UN);
        }
        fclose($fp);
    }
}

生产级配置建议

日志目录规划

/var/log/
├── app/                    # 应用日志
│   ├── access/             # 访问日志
│   ├── error/              # 错误日志  
│   ├── business/           # 业务日志
│   └── system/             # 系统日志

Swoole Worker 配合日志

// Swoole 常驻内存中的最佳实践
class LogWorker {
    private $channel;  // Swoole 内置队列
    public function __construct(int $capacity = 1024) {
        $this->channel = new Swoole\Channel($capacity * 1024 * 1024); // 1MB buffer
    }
    public function push(string $log) {
        $this->channel->push($log);
    }
    public function run() {
        // 异步消费者
        Swoole\Coroutine::create(function() {
            while (true) {
                $log = $this->channel->pop();
                if ($log !== false) {
                    $this->writeToFile($log);
                }
            }
        });
    }
}

日志轮转策略

// 使用 logrotate 配置
// /etc/logrotate.d/php-app
{
    daily                    # 每日轮转
    rotate 30                # 保留30天
    compress                 # 压缩旧日志
    delaycompress            # 延迟1天压缩
    notifempty               # 空文件不轮转
    copytruncate             # 先复制再清空(不影响写入)
    missingok                # 缺失不报错
    create 0640 www-data www-data  # 新日志权限
    postrotate
        # 触发PHP进程重载(如果需要)
        # kill -USR1 $(cat /var/run/php-fpm.pid)
    endscript
}

性能与可靠性权衡

方案 可靠性 性能 适用场景
文件锁直写 日志量小但关键
缓冲+批量 大多数应用
Redis缓冲 超高并发
异步队列 业务高峰不可丢失

终极方案(组合拳)

class UltimateLogger {
    private $logger;
    private $emergencyFiles = []; // 应急文件
    public function __construct() {
        // 主日志:监控告警
        $mainHandler = new StreamHandler('/var/log/app/main.log', Logger::INFO);
        // 错误日志:即时写入
        $errorHandler = new StreamHandler('/var/log/app/error.log', Logger::ERROR);
        $errorHandler->setFormatter(new JsonFormatter());
        // 审计日志:必须可靠
        $auditHandler = new StreamHandler('/var/log/app/audit.log', Logger::NOTICE);
        $auditHandler->setBufferSize(1); // 每条立即写入
        $this->logger = new Logger('app');
        $this->logger->pushHandler($mainHandler);
        $this->logger->pushHandler($errorHandler);
        $this->logger->pushHandler($auditHandler);
    }
    public function log($level, $message, $context = []) {
        try {
            $this->logger->$level($message, $context);
        } catch (\Throwable $e) {
            // 主日志失败时写入应急文件
            file_put_contents(
                '/var/log/app/emergency_' . date('Ymd') . '.log',
                json_encode([
                    'time' => microtime(true),
                    'level' => $level,
                    'message' => $message,
                    'context' => $context,
                    'error' => $e->getMessage()
                ]) . PHP_EOL,
                FILE_APPEND | LOCK_EX
            );
        }
    }
}

监控与保障

日志完整性校验

// 记录日志总数和md5
$logStats = [
    'count' => 12345,
    'size' => 567890,
    'md5' => hash_file('md5', $logFile)
];

定期自检

# crontab 每小时检查
*/60 * * * * /usr/local/bin/check_log_integrity.sh

告警阈值

# Prometheus 告警规则
- alert: 日志丢失率过高
  expr: rate(log_write_failures_total[1m]) > 10
  for: 5m

最简单的可靠方案:Monolog + BufferHandler + 进程结束自动flush

最高性能方案:Redis/队列 + 异步消费者批量落盘

不丢失的极端要求:每条日志都 fflush + fsync,但性能会下降10倍,一般用于审计日志

实际生产环境建议组合使用,根据日志类型分级处理,兼顾性能和可靠性。日志无小事,宁可慢不可丢

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