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
- GNU Coreutils: Pipeline and Command Substitution — Documentação oficial sobre pipelines Unix e substituição de comandos
- Advanced Bash-Scripting Guide: I/O Redirection — Guia avançado sobre redirecionamento e descritores de arquivo
- GNU Awk User's Guide: Memory Management — Documentação oficial sobre gerenciamento de memória em AWK
- Bash Hackers Wiki: Process Substitution — Explicação detalhada sobre substituição de processos e implicações de memória
- Linux man page: stdbuf — Manual completo do comando stdbuf para controle de buffering
- Efficient Large File Processing in Bash — Tutorial prático sobre processamento eficiente de arquivos grandes em Bash
- GNU Sort: Memory Management Options — Documentação oficial sobre opções de memória no comando sort