Processando Logs de N Gigas com PHP
Tutorial completo de como gerar e processar arquivos de log gigantes (N GBs) usando Streams e Generators para máxima performance
🚀 Processando Logs de N Gigas com PHP
Neste tutorial prático, vamos resolver um problema clássico de engenharia de software: como processar arquivos maiores que a memória RAM disponível?
Vamos construir duas ferramentas CLI (Command Line Interface) de alta performance:
- Gerador de Logs: Cria arquivos de log falsos com tamanho exato (ex: 10GB, 500MB).
- Processador de Logs: Analisa esses arquivos linha a linha com consumo de memória constante (~2MB).
💡 Pré-requisitos: PHP 7.4+ (Recomendado 8.1+).
🏗️ Parte 1: O Gerador de Logs (High Performance Write)
Para testar nossa performance, precisamos de dados. Vamos criar um script que gera logs no formato padrão Apache/Nginx.
O Desafio da Escrita
Escrever 10GB de dados linha por linha é lento. Para acelerar, usaremos Bufferização. Em vez de escrever no disco a cada linha, acumulamos dados na memória e escrevemos blocos de 8KB ou mais.
generator.php
<?php
// generator.php
// Uso: php generator.php <tamanho_gb> [arquivo_saida]
// Ex: php generator.php 1.5 access.log (Gera 1.5GB)
ini_set('memory_limit', '50M');
$sizeInGb = (float) ($argv[1] ?? 0.1);
$outputFile = $argv[2] ?? 'large_access.log';
$targetBytes = $sizeInGb * 1024 * 1024 * 1024;
echo "🚀 Iniciando geração de log de {$sizeInGb}GB...\n";
echo "📂 Arquivo: {$outputFile}\n";
$handle = fopen($outputFile, 'w');
if (!$handle) die("Erro ao abrir arquivo para escrita.\n");
// Buffer de escrita para performance (64KB)
stream_set_write_buffer($handle, 65536);
$bytesWritten = 0;
$start = microtime(true);
$lines = 0;
// Arrays pré-definidos para evitar alocação constante
$ips = ['192.168.1.1', '10.0.0.1', '172.16.0.1', '127.0.0.1', '8.8.8.8'];
$methods = ['GET', 'POST', 'PUT', 'DELETE'];
$uris = ['/api/users', '/home', '/login', '/assets/style.css', '/dashboard'];
$statuses = [200, 201, 400, 401, 403, 404, 500, 502];
$agents = ['Mozilla/5.0', 'Curl/7.68', 'Googlebot/2.1', 'MobileApp/1.0'];
// OTIMIZAÇÃO: Chunked Writing
// Em vez de escrever linha por linha (muitas syscalls),
// acumulamos em uma string e escrevemos blocos maiores.
$chunkSize = 1024 * 1024; // 1MB buffer na aplicação
$buffer = '';
while ($bytesWritten < $targetBytes) {
// Gera uma linha de log randômica
$ip = $ips[array_rand($ips)];
$date = date('d/M/Y:H:i:s O');
$method = $methods[array_rand($methods)];
$uri = $uris[array_rand($uris)];
$status = $statuses[array_rand($statuses)];
$bytes = rand(100, 5000);
$agent = $agents[array_rand($agents)];
$line = sprintf(
"%s - - [%s] \"%s %s HTTP/1.1\" %d %d \"-\" \"%s\"\n",
$ip, $date, $method, $uri, $status, $bytes, $agent
);
$buffer .= $line;
$lines++;
// Se buffer encheu, escreve no disco
if (strlen($buffer) >= $chunkSize) {
$len = fwrite($handle, $buffer);
if ($len === false) die("Erro de escrita no disco.\n");
$bytesWritten += $len;
$buffer = ''; // Limpa buffer
// Feedback visual
$progress = ($bytesWritten / $targetBytes) * 100;
echo sprintf("\r⏳ Progresso: %.2f%% (%s linhas)", $progress, number_format($lines));
}
}
// Escreve o restante do buffer
if (!empty($buffer)) {
fwrite($handle, $buffer);
$bytesWritten += strlen($buffer);
}
fclose($handle);
$duration = microtime(true) - $start;
$mbPerSec = ($bytesWritten / 1024 / 1024) / $duration;
echo "\n\n✅ Concluído!\n";
echo sprintf("⏱️ Tempo: %.2fs\n", $duration);
echo sprintf("⚡ Velocidade: %.2f MB/s\n", $mbPerSec);
echo sprintf("📄 Linhas totais: %s\n", number_format($lines));
Como rodar
# Gera um arquivo de 100MB
php generator.php 0.1
# Gera um arquivo de 1GB
php generator.php 1
🕵️ Parte 2: O Processador (High Performance Read)
Agora vamos processar esse arquivo gigante. O segredo aqui é NÃO usar file() ou file_get_contents(), pois eles carregam tudo na RAM.
Usaremos um Generator (yield) que lê o arquivo linha a linha, mantendo apenas 1 linha na memória por vez.
analyzer.php
<?php
// analyzer.php
// Uso: php analyzer.php <arquivo_log>
ini_set('memory_limit', '50M'); // Limite baixo proposital para provar eficiência
$inputFile = $argv[1] ?? 'large_access.log';
if (!file_exists($inputFile)) die("Arquivo não encontrado: $inputFile\n");
/**
* Generator que lê o arquivo linha a linha de forma eficiente
*/
function readLogLines(string $path): Generator {
$handle = fopen($path, 'r');
if (!$handle) throw new Exception("Não foi possível abrir o arquivo");
while (($line = fgets($handle)) !== false) {
yield $line;
}
fclose($handle);
}
echo "🔍 Iniciando análise de: $inputFile\n";
echo "📊 Memória inicial: " . round(memory_get_usage() / 1024 / 1024, 2) . "MB\n\n";
$start = microtime(true);
$totalLines = 0;
$totalBytes = 0;
// Métricas para coletar
$statusCounts = [];
$methodCounts = [];
$topUris = [];
$errors500 = 0;
// Loop principal - A mágica acontece aqui!
// O foreach puxa uma linha por vez do Generator
foreach (readLogLines($inputFile) as $line) {
$totalLines++;
$totalBytes += strlen($line);
// Parse simples (mais rápido que Regex complexo)
// Formato: IP - - [Date] "METHOD URI HTTP/1.1" STATUS BYTES ...
// Extrai Status Code (ex: 200, 404)
// Procura o primeiro espaço após as aspas do request
$parts = explode('"', $line);
if (isset($parts[2])) {
$meta = explode(' ', trim($parts[2])); // "200 1234"
$status = $meta[0] ?? '000';
// Incrementa contadores (usando referência para micro-otimização)
if (!isset($statusCounts[$status])) $statusCounts[$status] = 0;
$statusCounts[$status]++;
if ($status >= 500) $errors500++;
}
// Extrai Método e URI
if (isset($parts[1])) {
$req = explode(' ', $parts[1]); // "GET /api/users HTTP/1.1"
$method = $req[0] ?? 'UNKNOWN';
$uri = $req[1] ?? '/';
if (!isset($methodCounts[$method])) $methodCounts[$method] = 0;
$methodCounts[$method]++;
// Top URIs (amostragem simples para economizar memória em arquivos gigantes)
// Em produção real, usaríamos um banco ou Redis para contagem exata de cardinalidade alta
if ($totalLines % 10 === 0) { // Amostra 10% para não explodir array
if (!isset($topUris[$uri])) $topUris[$uri] = 0;
$topUris[$uri]++;
}
}
// Feedback visual a cada 500k linhas
if ($totalLines % 500000 === 0) {
echo sprintf("\rProcessing: %s lines | Mem: %.2fMB",
number_format($totalLines),
memory_get_usage() / 1024 / 1024
);
}
}
$duration = microtime(true) - $start;
arsort($topUris);
$topUris = array_slice($topUris, 0, 5);
echo "\n\n" . str_repeat("=", 40) . "\n";
echo "📄 RELATÓRIO DE ANÁLISE\n";
echo str_repeat("=", 40) . "\n";
echo sprintf("⏱️ Tempo Total: %.2fs\n", $duration);
echo sprintf("⚡ Velocidade: %.2f MB/s\n", ($totalBytes / 1024 / 1024) / $duration);
echo sprintf("💾 Pico de Memória: %.2f MB\n", memory_get_peak_usage(true) / 1024 / 1024);
echo "📝 Linhas Processadas: " . number_format($totalLines) . "\n";
echo "\n📊 Status Codes:\n";
ksort($statusCounts);
foreach ($statusCounts as $code => $count) {
$perc = ($count / $totalLines) * 100;
echo sprintf(" [%s]: %s (%.1f%%)\n", $code, number_format($count), $perc);
}
echo "\nmethods HTTP Methods:\n";
foreach ($methodCounts as $method => $count) {
echo " $method: " . number_format($count) . "\n";
}
echo "\n🔥 Top 5 URIs (Amostragem):\n";
foreach ($topUris as $uri => $count) {
echo " $uri\n";
}
echo "\n🚨 Total Erros 5xx: " . number_format($errors500) . "\n";
🧪 Caso Real: Processando 7GB de Logs
Para provar a eficiência, rodamos este script em um cenário real com um arquivo de 7.7GB contendo 77 milhões de linhas.
O Teste
# Gerando 7GB de dados (simulado)
php generator.php 7
Os Resultados
========================================
� RELATÓRIO DE ANÁLISE
========================================
⏱️ Tempo Total: 79.34s (~1min 19s)
⚡ Velocidade: 90.35 MB/s
💾 Pico de Memória: 2.00 MB
📝 Linhas Processadas: 77,556,232
Análise Visual
Veja a diferença brutal de consumo de memória entre a abordagem clássica (file()) e a nossa abordagem com Generators:
graph TD
subgraph "Abordagem Tradicional (Crash)"
A1[Início] -->|Carrega 7GB| B1[RAM: 7GB ❌]
B1 --> C1[Crash: Memory Limit Exceeded]
end
subgraph "Abordagem com Generators (Sucesso)"
A2[Início] -->|Lê Chunk 8KB| B2[RAM: 2MB]
B2 -->|Processa| C2[Descarta da RAM]
C2 -->|Lê Próximo| B2
C2 -->|Fim do Arquivo| D2[Sucesso ✅]
end
style B1 fill:#ff0000,color:#fff
style B2 fill:#00ff00,color:#000
style D2 fill:#00ff00,color:#000
🌍 Onde usar isso no Mundo Real?
Esta técnica não serve apenas para logs. Ela é a base para sistemas de ETL (Extract, Transform, Load) e processamento de Big Data em PHP.
1. Migração de Banco de Dados
Imagine migrar uma tabela de users com 50 milhões de registros do MySQL para o PostgreSQL.
- Errado:
fetchAll()(Carrega tudo, estoura RAM). - Certo:
PDO::FETCH_LAZYouunbuffered_query+ Generators. Você lê um registro, converte e insere no destino, mantendo a memória em 1KB.
2. Relatórios Financeiros (CSV/Excel)
Gerar um CSV com todas as vendas do ano (1 milhão de linhas).
- Errado: Montar o array
$csv[]e salvar no final. - Certo: Escrever no
php://outputlinha a linha usandofputcsvdentro de um loop. O usuário começa a baixar o arquivo imediatamente (stream), sem esperar o servidor processar tudo.
3. Importação de Produtos (E-commerce)
Ler um XML de 2GB de um fornecedor para atualizar estoque.
- Errado:
simplexml_load_file(Carrega a árvore toda). - Certo:
XMLReader(Stream parser) + Generators. Lê nó por nó<product>, atualiza o banco e libera memória.
🧪 Caso Real 2: Teste de Stress com 12GB (Geração Paralela)
Para levar ao limite, geramos 12GB de logs usando paralelismo de processos (3 instâncias do gerador) e analisamos o resultado final.
O Comando (Correto)
# Gera 12GB em paralelo (2GB + 3GB + 7GB)
# Cada processo escreve em seu próprio arquivo para evitar corrupção
php generator.php 2 part1.log &
php generator.php 3 part2.log &
php generator.php 7 part3.log &
# Aguarda terminar e combina tudo
cat part1.log part2.log part3.log > large_access.log
rm part1.log part2.log part3.log
O Resultado da Análise
Processamos o arquivo combinado de 132 milhões de linhas:
========================================
📄 RELATÓRIO DE ANÁLISE
========================================
⏱️ Tempo Total: 115.34s (~1min 55s)
⚡ Velocidade: 106.54 MB/s
💾 Pico de Memória: 2.00 MB
📝 Linhas Processadas: 132,954,491
Impressionante: Processamos 12GB de dados em menos de 2 minutos usando apenas 2MB de RAM!
⚠️ Cuidado: A Armadilha da Concorrência
No exemplo acima, usamos arquivos separados (part1.log, part2.log). Isso é crucial!
O Erro Comum (Race Condition): Se você tentar escrever no mesmo arquivo com múltiplos processos simultâneos:
# ❌ PERIGO: Todos escrevendo em 'large_access.log' ao mesmo tempo
php generator.php 2 large_access.log &
php generator.php 3 large_access.log &
Os processos vão "atropelar" a escrita um do outro (interleaved writes). O resultado será um arquivo corrompido com linhas misturadas, gerando erros de parse como:
Status: [10.0.0.1](IP aparecendo onde deveria ser o status)Method: DELETE7.0.0.1(Método misturado com IP)
Sempre use arquivos separados ou locking (flock) para escritas concorrentes.
🧠 Por que isso é rápido?
- Stream de Leitura: O
fopen+fgetslê o arquivo direto do disco em chunks, sem carregar tudo. - Generators: O
yieldpermite iterar sobre esses chunks sem criar arrays gigantes. - Complexidade O(1): O consumo de memória não aumenta com o tamanho do arquivo. Processar 100MB ou 100GB gasta a mesma memória RAM.
⚡ E sobre Paralelismo? (Fibers/Async)
Você pode se perguntar: "Posso usar Fibers para gerar o log mais rápido?"
A resposta curta é: NÃO.
Por que?
Fibers e Async PHP são ótimos para I/O Bound (esperar rede, banco de dados), mas a geração de logs é CPU Bound (formatar strings, calcular randômicos) e Disk Bound (escrever no disco).
Como o PHP é single-threaded, usar Fibers aqui apenas adicionaria overhead de troca de contexto sem ganho real, pois a CPU estaria 100% ocupada o tempo todo.
Como paralelizar de verdade?
Se você precisa gerar 100GB de logs muito rápido, a melhor estratégia é Paralelismo de Processos (OS Level):
- Abra 4 terminais.
- Rode 4 instâncias do script simultaneamente:
# Terminal 1
php generator.php 25 part1.log &
# Terminal 2
php generator.php 25 part2.log &
# ...
O Sistema Operacional vai distribuir cada processo PHP em um núcleo diferente da CPU (Core 1, Core 2, etc), multiplicando a velocidade de geração.
🔗 Veja Também
- Generators para Memória - Explicação detalhada da teoria.
- Fibers em PHP - Para processamento assíncrono.