Streams: Quando e por que usar para processamento eficiente de dados

Michel Oliveira
6 min read
Streams: Quando e por que usar para processamento eficiente de dados

Introdução

Vivemos em uma era de excesso de informação. Logs que crescem a cada segundo, bancos de dados de terabytes, vídeos transmitidos em tempo real. Nesse cenário, surge a pergunta:

Como processar volumes massivos de dados sem travar a aplicação ou estourar a memória?

A resposta está em uma técnica antiga, mas cada vez mais essencial: Streams.

  • No ecossistema .NET e C#, streams aparecem em várias formas, como Stream, StreamReader, IAsyncEnumerable<T>, FileStream.
  • Em outras linguagens, streams também aparecem, por exemplo em Java (InputStream / OutputStream) e Node.js (ReadableStream / WritableStream).
  • Fluxos de dados contínuos em HTTP, gRPC, filas e storage em nuvem também possuem suas APIs de stream.

Paradigma Tradicional vs Streaming

A diferença entre processar tudo de uma vez e processar em partes pode ser representada dessa forma:

flowchart LR subgraph Streaming["Streaming"] A2[Arquivo de 10GB] --> E[Lê em blocos menores] E --> F[Processa parcial] F --> E E --> G[Resultados progressivos] end subgraph EmMemoria["Em Memória"] A[Arquivo de 10GB] --> B[Carrega tudo na memória] B --> C[Processa completamente] C --> D[Resultado final] end

Na primeira abordagem, só obtemos resultados após carregar tudo. Na segunda, já começamos a produzir valor enquanto processamos.

Arquitetura: Visão conceitual

Um sistema que processa dados como fluxo pode ser entendido como um pipeline:

flowchart TD Input[Dados brutos] --> Stage1[Ler pedaço] Stage1 --> Stage2[Transformar/Processar] Stage2 --> Stage3[Emitir resultado parcial] Stage3 --> Loop[Próximo pedaço]

Essa abordagem traz ganhos claros:

  • Menor uso de memória.
  • Tempo de resposta mais rápido.
  • Capacidade de lidar com dados variáveis ou infinitos.

O conceito de buffer

Um buffer é uma área de memória temporária usada para armazenar partes de dados enquanto eles são processados. Em streams, usamos buffers para ler blocos de dados em vez de carregar tudo de uma vez. Por exemplo, ao ler um arquivo de 10GB, podemos usar um buffer de 4MB:

  • O buffer reduz o consumo de memória.
  • Permite começar a processar dados antes de ter lido o arquivo inteiro.
  • Pode ser ajustado para equilibrar latência e uso de memória.

Exemplo Teórico: Lendo um arquivo de 10GB em .NET, C#

Abordagem sem streaming


var data = File.ReadAllBytes("dados-10gb.bin");
Process(data);
  • Requer 10GB de memória disponível.
  • Se não houver, a aplicação pode cair com OutOfMemoryException.

Abordagem com streaming e buffer


using var stream = new FileStream("dados-10gb.bin", FileMode.Open);
var buffer = new byte[4 * 1024 * 1024]; // buffer de 4MB
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
    Process(buffer.AsSpan(0, read));
}
  • Usa apenas 4MB de memória por vez.
  • Escala para arquivos muito maiores sem esforço.

Visualizando a diferença

sequenceDiagram participant App participant RAM participant Disco App->>Disco: Solicita arquivo (10GB) Disco->>RAM: Carrega tudo RAM->>App: Retorna dados completos App->>App: Processa (alto custo) break Streaming App->>Disco: Solicita próximo bloco (4MB) Disco->>RAM: Retorna bloco parcial RAM->>App: Dados processados e liberados App->>Disco: Solicita próximo bloco end

Streaming em HTTP, gRPC e outros dados contínuos

Streams não se limitam a arquivos. Eles podem ser aplicados a:

  • HTTP uploads e downloads.
  • gRPC streaming de dados entre microsserviços.
  • Mensageria e filas (Kafka, RabbitMQ, Azure Event Hub).
  • Blobs em Azure Storage ou S3.
  • Processamento de logs contínuos e eventos em tempo real.

A ideia central é processar enquanto os dados chegam, independente da fonte, reduzindo o uso de memória e aumentando a escalabilidade.

Impactos na Performance e Garbage Collection

Processar grandes volumes de dados em memória gera várias alocações de objetos, o que aciona o Garbage Collector (GC) com mais frequência, aumentando a latência e diminuindo a performance geral.

  • Sem streaming, cada arquivo ou coleção inteira precisa ser alocada, o que pressiona a heap e aumenta pausas do GC.
  • Com streaming, alocamos apenas buffers pequenos, liberando memória continuamente.
  • O processamento incremental minimiza picos de uso de memória e reduz a fragmentação da heap.

Onde ainda vejo memória sendo estourada

Mesmo times maduros ainda cometem o erro de processar arquivos massivos direto na memória. Exemplos comuns:

  • Arquivos CSV de 10–20GB sendo carregados com File.ReadAllText.
  • ETLs corporativos que baixam dados do data lake e tentam abrir tudo de uma vez.
  • Integrações batch usando List<T> para acumular milhões de registros.

Motivos:

  1. Pressão de prazo: soluções rápidas “funcionam na máquina”.
  2. Desconhecimento de streams.
  3. Testes com datasets pequenos, que não refletem produção.
  4. Falsa sensação de simplicidade: ler tudo parece mais direto.

Sintomas típicos:

  • Servidores com dezenas de gigabytes de RAM esgotadas.
  • Jobs que demoram horas só para carregar dados.
  • Sistemas instáveis em produção mas ok no dev.

O uso de streaming, nesses casos, não é só boa prática - é literalmente a diferença entre rodar ou não rodar.

Trade-offs: Stream vs Não Stream

Sem streaming:

  • Simplicidade de implementação.
  • Fácil acesso randômico aos dados.
  • Garantia de ordenação completa.
  • Risco de alto consumo de memória.
  • Escalabilidade limitada.
  • Latência maior para começar a processar grandes arquivos.

Com streaming:

  • Complexidade maior de implementação.
  • Processamento incremental, menor latência.
  • Uso controlado de memória, maior escalabilidade.
  • Necessidade de lógica adaptada a chunks de dados.
  • Não garante ordenação de dados se processados em paralelo ou em chunks.
  • Possível necessidade de buffering ou sorting adicional se a ordenação for crítica.
  • Dependência da disponibilidade contínua do fluxo de dados.
  • Requer tratamento cuidadoso de erros durante o fluxo contínuo.
  • Dificuldade de reprocessamento de dados já consumidos sem armazenar intermediários.

Quando usar Streams

SituaçãoUse StreamsEvite Streams / Mude a Estratégia
Tamanho dos dadosGrandes ou pouco previsíveis (> 1 MB)Pequenos, previsíveis (< 1KB)
Fluxo de dadosContínuo ou indeterminadoDados finitos e facilmente carregáveis
Latência / ProcessamentoPossível processar antes do fim da leituraQuando múltiplas iterações são necessárias
Consumo de memóriaPrecisa ser previsívelQuando o custo incremental supera o benefício
OrdenaçãoNão crítica ou gerenciável com buffersQuando a ordenação estrita é essencial

Aplicações no mundo real

  • Leitura de arquivos de log gigantes (auditorias, ETLs).
  • Streaming de mídia (vídeo, áudio).
  • Integrações entre microsserviços usando gRPC streaming.
  • Mensageria e filas: Kafka, RabbitMQ, Azure Event Hub.
  • Leitura de blobs em Azure Storage ou Amazon S3.
  • Processamento de eventos em tempo real.

Conclusão

Streams não são apenas um detalhe de implementação. Elas são uma filosofia arquitetural: processar enquanto se recebe, produzir valor parcial antes do fim e projetar sistemas que crescem junto com os dados.

Na prática, ainda vejo equipes maduras caírem no erro de carregar tudo em memória. Times que adotam streaming constroem sistemas mais resilientes, previsíveis e prontos para lidar com o crescimento dos dados.

Conecte-se para transformar sua tecnologia!

Saiba mais e entre em contato: