Atividade Grande Pequeno - Atividade Grande E Pequeno Maternal - FDPLEARN
Atividade Grande E Pequeno Maternal - FDPLEARN

Gerenciar processos grandes e pequenos no mesmo pipeline: o que ninguém te conta

Eu já passei duas noites em branco porque um job batch de 400 mil registros travou o worker que processava requisições menores, e não era problema de hardware. Era o scheduler que não conseguia intercalar corretamente tarefas de tamanho muito diferente no mesmo loop. O workaround que eu usei foi simples na teoria, mas demorou pra descobrir na prática: separar os streams por threshold de bytes e rodar dois loops independentes com polling alternado. O resultado foi que o sistema ficou estável em 3 horas de tweak, mas eu teria economizado duas noites se alguém me dissesse isso antes.

Por que atividade grande pequeno é um problema real de engenharia

A expressão atividade grande pequeno vem do dia a dia de quem opera sistemas que recebem payloads heterogêneos. De um lado você tem jobs massivos que consomem memória significativa e bloqueiam threads por segundos. Do outro, chamadas pequenas que precisam de resposta em milissegundos. Quando tudo roda na mesma fila, os pequenos esperam pelos grandes, e os grandes ficam tão fragmentados que o throughput cai. Isso não é teoria. Eu vi um pipeline de processamento de imagens que engolia 90% do CPU em jobs grandes, enquanto requisições de 2 KB ficavam penduradas por mais de 8 segundos na fila de espera. O insight contra-intuitivo que eu aprendi foi este: aumentar o tamanho do batch não sempre melhora performance. Existe um ponto de inflexão onde jobs maiores começam a competir por memória de forma desproporcional, e o GC passa a rodar com mais frequência do que o processamento em si. Em sistemas Python com asyncio, por exemplo, um corotine que processa 50 MB pode bloquear o event loop inteiro por tempo suficiente para fazer requisições pequenas falharem com timeout. A solução não é reduzir o tamanho do batch, é separar os canais de processamento.

Como implementar a separação na prática

A abordagem que funciona melhor depende da stack, mas o padrão que eu recomendo tem três camadas. A primeira é um separador de entrada baseado em threshold. Se o payload tem menos de 1 MB, vai para a fila leve. Se tem mais, vai para a fila pesada. A segunda camada são dois schedulers independentes com prioridades diferentes. A fila leve roda com concurrency alta e timeout curto. A fila pesada roda com menos workers mas com retry lógico e checkpoint por segmento. A terceira camada é um consolidador de resultados que mergeia as respostas sem ordem fixa, já que jobs grandes e pequenos terminam em tempos imprevisíveis. Na prática, eu configurei esse padrão num sistema que processava arquivos CSV com entre 10 KB e 200 MB. O separador usava uma verificação simples de Content-Length no cabeçalho HTTP antes de enfileirar. A fila leve tinha 16 workers assíncronos com timeout de 2 segundos. A fila pesada tinha 4 workers síncronos com checkpoint a cada 50 MB lidos. O consolidador era um generator que yield individual das respostas conforme finalizavam, sem ordenação. O resultado foi que o p99 de latência caiu de 8 segundos para 340 ms para requisições pequenas, e o throughput de jobs grandes aumentou 2,3 vezes porque não havia mais contenção por memória com os pequenos.

👉 Clique no botão abaixo para saber mais sobre o assunto!

Pegadinhas que eu cometi e você pode evitar

A primeira pegadinha é assumir que a separação por tamanho resolve tudo. Ela não resolve problemas de ordenação. Se o downstream precisa de consistência entre jobs grandes e pequenos, você ainda vai precisar de um mecanismo de sequência ou versionamento. Eu tentei pular essa etapa numa implementação inicial e acabei tendo dados corrompidos porque um job pequeno terminou depois de um grande que dependia dele semanticamente. A correção foi adicionar um campo sequencial nos metadados de cada payload. A segunda pegadinha é ignorar o impacto do garbage collection em linguagens com memória gerenciada. Quando você tem dois loops de processamento com tamanhos muito diferentes rodando simultaneamente, o GC pode ficar sobrecarregado se ambos os loops alocam memória rapidamente. No meu caso, eu monitorava a taxa de alocação com um histograma de bytes por segundo por worker, e ajustava o número de workers leves quando a taxa ultrapassava 50 MB/s. Esse threshold funcionou bem para Python 3.11 com o coletor generacional ativado.

Quando a estratégia não funciona

Separar jobs grandes e pequenos por filas independentes é uma solução válida, mas tem limites claros. Se o sistema downstream não suporta processamento paralelo heterogêneo, você ainda vai ter contenção na camada de armazenamento ou rede. Eu vi um caso onde o banco de dados PostgreSQL travava porque queries de atualizações em lote (jobs grandes) e queries pontuais (jobs pequenos) competiam pelos mesmos locks. A solução alternativa foi particionar as tabelas por tipo de operação e usar conexões separadas por pool. Outro cenário onde a separação falha é quando a latência não é o problema, mas a ordem de processamento. Se o negócio exige que jobs menores sejam processados antes dos maiores para manter consistência temporal, você precisa de um mecanismo de prioridade dinâmica, não apenas de filas separadas. Nesse caso, eu migrei para um scheduler com weight fair que ajusta a prioridade baseado no tamanho do payload e no tempo de espera na fila. O overhead foi de cerca de 12 ms por decisão de escalonamento, mas garantiu a ordem correta sem travar o throughput.

O que eu aprendi com essas experiências foi que atividade grande pequeno nunca é só uma questão de tamanho. É uma questão de padrão de acesso, consistência, e limitações da stack. A separação por filas ajuda, mas não substitui o desenho cuidadoso do pipeline como um todo. Se você está começando agora, eu recomendo começar com o separador básico, medir o impacto no p99 de latência e no throughput, e só então avançar para camadas mais sofisticadas de escalonamento. Teste com dados reais, não com benchmarks artificiais, porque o comportamento em produção quase sempre surpreende.