Memory-efficient scripting: processamento stream de grandes arquivos

1. Fundamentos do Processamento Stream em Bash

1.1. Conceito de pipeline Unix

O pipeline Unix é a espinha dorsal do processamento eficiente de dados em Bash. Quando você encadeia comandos com |, os dados fluem diretamente da saída de um processo para a entrada do próximo, sem necessidade de armazenamento intermediário em disco ou memória. Isso significa que gigabytes de dados podem ser processados com consumo de RAM praticamente constante.

# Pipeline eficiente: dados fluem sem armazenar o arquivo inteiro
zcat arquivo_gigante.log.gz | grep "ERRO" | awk '{print $1, $NF}' | head -100

Neste exemplo, zcat descomprime e envia linha por linha — nenhum processo precisa carregar o arquivo completo.

1.2. Processamento batch vs. stream contínuo

A diferença crucial está em como os dados são consumidos:

# INEFICIENTE: carrega o arquivo inteiro na memória
mapfile -t linhas < arquivo.txt
for linha in "${linhas[@]}"; do
    processar "$linha"
done

# EFICIENTE: stream linha a linha
while IFS= read -r linha; do
    processar "$linha"
done < arquivo.txt

No segundo caso, apenas uma linha reside na memória por vez.

1.3. O papel do buffer de kernel e stdbuf

O kernel mantém buffers para otimizar I/O, mas em pipelines longos o buffer padrão de 4KB ou 8KB pode acumular dados. Para controle fino:

# Desabilita buffering no comando
stdbuf -oL grep "padrao" arquivo_gigante.txt

# Define buffer de 1KB
stdbuf -o1K awk '{print $0}' arquivo.txt

2. Técnicas para Evitar Carregamento Completo de Arquivos

2.1. sed, awk e grep em modo stream

Estas ferramentas operam nativamente em modo stream:

# Filtragem sem carregar o arquivo
grep -E "2024-03-1[5-9]" access.log | sed 's/\[//g' | awk '{print $1, $4}'

# Transformação linha a linha
awk '{if(NR%2==0) print toupper($0); else print tolower($0)}' dados.txt

2.2. Processamento linha a linha com while read

# Processamento seguro com redirecionamento
while IFS= read -r line; do
    if [[ "$line" == *"CRITICAL"* ]]; then
        echo "$(date): $line" >> alertas.log
    fi
done < /var/log/syslog

2.3. Evitando armadilhas de memória

# EVITAR: substituição de processo que carrega tudo
diff <(sort arquivo1.txt) <(sort arquivo2.txt)

# PREFERIR: processamento incremental
sort arquivo1.txt > tmp1.txt
sort arquivo2.txt > tmp2.txt
diff tmp1.txt tmp2.txt
rm tmp1.txt tmp2.txt

3. Manipulação Eficiente de Arquivos Grandes com awk

3.1. Filtragem sem armazenamento completo

# Processa 10GB de logs sem estourar a RAM
awk '$4 ~ /2024-03-1[5-9]/ && $9 == 500 {erros[$1]++} 
     END {for(ip in erros) print ip, erros[ip]}' access.log

3.2. Arrays associativos limitados e flush

# Agregação com flush periódico para evitar acúmulo
awk '{
    chave = $1
    soma[chave] += $2
    if(NR % 10000 == 0) {
        for(k in soma) print k, soma[k] > "parcial.txt"
        delete soma
        system("")
    }
}' dados_gigantes.txt

3.3. Exemplo prático: agregação de logs de 10GB

# Contagem de IPs únicos em log Apache com stream
zcat access.log.2024-03-*.gz | 
awk '{ips[$1]++} 
     END {for(ip in ips) print ips[ip], ip}' | 
sort -rn | head -20

4. Split, Merge e Ordenação com Baixo Consumo

4.1. split controlado

# Divide em partes de 100 mil linhas
split -l 100000 dados_gigantes.txt parte_

# Processa cada parte em paralelo
for f in parte_*; do
    processar "$f" &
done
wait

4.2. Ordenação externa com sort -S

# Limita memória a 100MB para ordenação
sort -S100M -t',' -k2,2n dados_gigantes.csv > ordenado.csv

# Ordenação com fallback para disco
sort -S50M --batch-size=10 arquivo_grande.txt

4.3. Junção de streams

# Join sem carregar tudo
sort -S100M -k1,1 usuarios.csv | 
join -1 1 -2 1 - <(sort -S100M -k1,1 pedidos.csv) > relatorio.csv

5. Redirecionamento e Descritores de Arquivo

5.1. exec para economia de forks

# Redirecionamento permanente
exec 3>saida_stream.txt
while IFS= read -r linha; do
    echo "$linha" >&3
done < entrada.txt
exec 3>&-

5.2. Descritores customizados

# Leitura e escrita simultânea sem subshell
exec 3<dados.csv
exec 4>processado.csv
while IFS= read -r -u3 linha; do
    echo "${linha,,}" >&4
done
exec 3<&- 4>&-

5.3. Parser de CSV gigante

exec 3<dados_gigantes.csv
while IFS= read -r -u3 linha; do
    IFS=',' read -ra campos <<< "$linha"
    [[ ${campos[2]} -gt 1000 ]] && echo "${campos[0]},${campos[3]}"
done
exec 3<&-

6. Monitoramento e Controle de Memória

6.1. Verificação em tempo real

# Monitora memória do processo atual
while true; do
    mem=$(grep VmRSS /proc/$$/status | awk '{print $2}')
    echo "Memória atual: $mem KB"
    [ "$mem" -gt 500000 ] && { echo "Limite excedido!"; exit 1; }
    sleep 5
done &

# Limite rígido de memória
ulimit -v 300000  # 300MB

6.2. Throttling com pv

# Limita taxa de processamento
pv -L 5M dados_gigantes.txt | awk '{processar}'

# Monitora progresso em pipeline
zcat log_*.gz | pv -cN "Descomprimindo" | grep "ERRO" | pv -cN "Filtrando" > erros.txt

6.3. Detecção de vazamentos

# Loop com liberação explícita
while IFS= read -r linha; do
    resultado=$(processar "$linha")
    echo "$resultado"
    unset resultado  # Libera memória
done < dados.txt

7. Casos Práticos

7.1. Stream de logs Apache com compressão

# Filtragem por data com compressão em tempo real
for log in /var/log/apache2/access.log.*.gz; do
    zcat "$log" | awk '$4 ~ /15\/Mar\/2024/' | gzip -c >> filtro_15mar.gz
done

7.2. Transformação de CSVs enormes

# Extrai colunas específicas sem carregar tudo
cut -d',' -f1,3,5 dados_gigantes.csv | 
sed 's/^/ID:/' | 
paste - <(cut -d',' -f2 dados_gigantes.csv) > transformado.csv

7.3. Processamento de binários com dd

# Lê blocos de 1MB
dd if=arquivo.bin bs=1M | while IFS= read -r -n 1024 bloco; do
    echo "$bloco" | od -A x -t x1z -v | head -5
done

8. Armadilhas Comuns e Boas Práticas

8.1. Evitar for sobre listas grandes

# NUNCA faça isso
for arquivo in $(cat lista_gigante.txt); do
    processar "$arquivo"
done

# SEMPRE prefira
while IFS= read -r arquivo; do
    processar "$arquivo"
done < lista_gigante.txt

8.2. Cuidados com xargs -P

# Paralelismo controlado com limite de processos
xargs -P 4 -I {} sh -c 'processar "$1"' _ {} < lista.txt

# Isolamento de memória com subshell
xargs -P 2 -I {} bash -c 'ulimit -v 200000; processar "$1"' _ {}

8.3. Testes de estresse

# Gera 1 milhão de linhas para teste
yes "linha de teste com dados variados" | head -1000000 > teste.txt

# Testa consumo de memória
ulimit -v 100000  # 100MB
while IFS= read -r linha; do :; done < teste.txt
echo "Teste concluído sem estouro de memória"

Referências