本文目录导读:

在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倍,一般用于审计日志
实际生产环境建议组合使用,根据日志类型分级处理,兼顾性能和可靠性。日志无小事,宁可慢不可丢。