面對 800MB、500 萬行日志文件,file()、file_get_contents() 全量讀入記憶體會導致 OOM。本文實測四種流式/隨機讀取方案,從 tail 系統命令到 fseek 反向塊讀,附帶執行耗時數據與適用場景建議。
access.log,800 MB,~500 萬行。memory_limit = -1(方案 1 需求)。<?php
ini_set('memory_limit', '-1');
$file = 'access.log';
$data = file($file); // 一次性載入全部行到陣列
$line = $data[count($data) - 1];
echo $line;
// 耗時:116.96 秒,記憶體峰值 > 800 MB,系統近乎凍結
?>
<?php
$file = 'access.log';
$file = escapeshellarg($file); // 防命令注入
$line = `tail -n 10 $file`; // 反引號執行命令並捕獲輸出
echo $line;
// 耗時:0.0034 秒
// 優點:極快、零 PHP 記憶體、支援超大文件
// 缺點:依賴 Linux 環境、需 escapeshellarg、Windows 不通用
?>
<?php
$fp = fopen($file, 'r');
$lineCount = 10;
$pos = -2; // 從 EOF 前一字節開始(跳過末尾換行)
$t = ' ';
$data = '';
while ($lineCount > 0) {
while ($t !== "\n") {
fseek($fp, $pos, SEEK_END);
$t = fgetc($fp);
$pos--;
}
$t = ' ';
$data .= fgets($fp); // 讀取整行
$lineCount--;
}
fclose($fp);
echo $data;
// 耗時:0.0095 秒
// 缺點:大量系統調用、極長行時性能劣化
?>
<?php
$fp = fopen($file, 'r');
$num = 10;
$chunk = 4096; // 4KB 塊
$fs = sprintf('%u', filesize($file)); // 避免 32 位溢位
$max = ($fs == PHP_INT_MAX) ? PHP_INT_MAX : (int)$fs;
$readData = '';
for ($len = 0; $len < $max; $len += $chunk) {
$seekSize = min($chunk, $max - $len);
fseek($fp, -($len + $seekSize), SEEK_END);
$readData = fread($fp, $seekSize) . $readData; // 前置拼接保持順序
if (substr_count($readData, "\n") >= $num + 1) {
// 正則提取最後 N 行
preg_match("!(.*?\n){$num}$!", $readData, $match);
$data = $match[0];
break;
}
}
fclose($fp);
echo $data;
// 耗時:0.0009 秒
// 優點:系統調用少、記憶體恆定、純 PHP 跨平台
?>
<?php
function tail($fp, int $n, int $base = 5): array {
$pos = $n + 1;
$lines = [];
while (count($lines) <= $n) {
try {
fseek($fp, -$pos, SEEK_END);
} catch (Exception $e) { // 到達文件開頭
fseek($fp, 0);
break;
}
$pos *= $base; // 指數增大讀取窗口
while (!feof($fp)) {
array_unshift($lines, fgets($fp));
}
}
return array_slice($lines, 0, $n);
}
var_dump(tail(fopen('access.log', 'r'), 10));
// 耗時:0.0003 秒
// 原理:每次跳躍距離指數增長 (5, 25, 125, 625...),極快定位到包含 N 行的區塊
?>
| 方案 | 耗時 | 記憶體 | 跨平台 | 適用場景 |
|---|---|---|---|---|
| file() | 116.96s | >800MB | ✅ | 小文件 (<10MB)、需全量處理 |
| tail 命令 | 0.0034s | ~0 | ❌ (Linux/Unix) | 日志分析、Linux 部署、極大文件 |
| fseek 逐字節 | 0.0095s | ~0 | ✅ | 教學演示、極短行文件 |
| fseek 分塊 (4KB) | 0.0009s | ~4KB×N | ✅ | **通用生產首選**、跨平台、無外部依賴 |
| 指數退避 | 0.0003s | ~0 | ✅ | 極大文件、追求極致性能、行長度不可預測 |
<?php
class BigFileReader {
public static function tail(string $path, int $lines = 10, int $chunk = 8192): array {
if (!is_readable($path)) throw new \InvalidArgumentException("File not readable: $path");
$fp = fopen($path, 'rb');
if (!$fp) throw new \RuntimeException("Cannot open file: $path");
try {
$size = filesize($path);
if ($size === false || $size === 0) return [];
$data = '';
$read = 0;
while ($read < $size && substr_count($data, "\n") < $lines + 1) {
$seekSize = min($chunk, $size - $read);
fseek($fp, -($read + $seekSize), SEEK_END);
$data = fread($fp, $seekSize) . $data;
$read += $seekSize;
}
$matches = [];
preg_match("!(.*?\n){$lines}$!", $data, $matches);
return $matches ? explode("\n", trim($matches[0])) : explode("\n", trim($data));
} finally {
fclose($fp);
}
}
}
?>