🚀 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:

  1. Gerador de Logs: Cria arquivos de log falsos com tamanho exato (ex: 10GB, 500MB).
  2. 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_LAZY ou unbuffered_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://output linha a linha usando fputcsv dentro 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?

  1. Stream de Leitura: O fopen + fgets lê o arquivo direto do disco em chunks, sem carregar tudo.
  2. Generators: O yield permite iterar sobre esses chunks sem criar arrays gigantes.
  3. 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):

  1. Abra 4 terminais.
  2. 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