Processamento Assíncrono de Imagens em Escala com Google Cloud

Nos últimos meses, venho trabalhando em um desafio técnico fascinante: construir um sistema de processamento de imagens que consegue lidar com até 10.000 arquivos por upload de forma eficiente e escalável.

Hoje compartilho algumas das soluções que implementei usando Google Cloud Platform e Next.js.

O Desafio

Imagine um sistema que precisa:

O problema: processar tudo sequencialmente levaria horas e travaria completamente a interface.

Google Cloud Platform: Papel Crucial

O GCP foi fundamental para resolver esse desafio, especialmente o Google Cloud Storage (GCS):

Por que GCS é essencial:

Arquitetura com GCS:

Upload → GCS Bucket Original → Processamento → GCS Bucket Processed

Cada imagem gera 3 versões em buckets separados:

A Solução: Processamento Assíncrono

1. Pool de Concorrência Controlada

const MAX_CONCURRENCY = 3;

async function asyncPool<T, R>(
  poolLimit: number,
  array: T[],
  iteratorFn: (item: T) => Promise<R>
): Promise<R[]> {
  const ret: Promise<R>[] = [];
  const executing: Promise<any>[] = [];

  for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    ret.push(p);

    if (executing.length >= poolLimit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(ret);
}

Por que limite de 3? Testes mostraram que mais que isso sobrecarrega o servidor e causa timeouts.

2. Upload Paralelo para GCS

Para cada arquivo, fazemos 3 uploads simultâneos para diferentes buckets:

const uploads: Promise<UploadResult>[] = [];

// Upload original para bucket principal
const originalUpload = uploadToGCS(file, projectId, 'original');
uploads.push(originalUpload);

// Upload processado para bucket otimizado
const processedUpload = applyWatermarkToImage(buffer, watermarkPath, 0.7)
  .then(processedBuffer => uploadToGCS(
    processedBuffer,
    projectId,
    'processed',
    file.name.replace(/\.[^/.]+$/, ".webp"),
    'image/webp'
  ));
uploads.push(processedUpload);

const [originalResult, processedResult] = await Promise.all(uploads);

3. Sistema de Sessões de Upload

Implementamos um sistema robusto de sessões para rastrear o progresso:

// Criar sessão de upload
let uploadSession = await db.uploadSession.create({
  data: { 
    projectId, 
    userId: session.user.id, 
    totalFiles: validFiles.length, 
    status: 'IN_PROGRESS' 
  }
});

// Atualizar progresso em tempo real
await db.uploadSession.update({
  where: { id: uploadSession.id },
  data: { 
    processedFiles: processedCount,
    failedFiles: failedCount,
    status: newStatus 
  }
});

4. Google Pub/Sub para Processamento Assíncrono

Após o upload inicial no GCS, enviamos mensagens para uma fila:

const pubSubMessage: ProcessingMessage = {
  projectId,
  originalImagePath: originalResult.originalUrl,
  fileName: originalResult.fileName,
  userId: sessionUserId,
  timestamp: new Date().toISOString(),
  uploadSessionId
};

await publishBatchProcessing([pubSubMessage]);

5. Otimização com Sharp.js

export async function applyWatermarkToImage(
  originalBuffer: Buffer,
  watermarkPath?: string,
  opacity: number = 1
): Promise<Buffer> {
  const { width, height } = await sharp(originalBuffer).metadata();
  
  const resizedImage = sharp(originalBuffer)
    .rotate() // Auto-rotate baseado em EXIF
    .resize({ width: 400 })
    .webp({ quality: 50 });
    
  // Aplicar marca d'água cobrindo toda a imagem
  const finalImageBuffer = await sharp(resizedBuffer)
    .composite([{
      input: watermarkWithOpacity,
      blend: "over"
    }])
    .webp({ quality: 50 })
    .toBuffer();
    
  return finalImageBuffer;
}

Monitoramento e Métricas em Tempo Real

Sistema de Progresso Detalhado

// Verificar progresso a cada 2 segundos
useEffect(() => {
  if (uploadSessionId) {
    const interval = setInterval(async () => {
      const response = await fetch(`/api/upload/status/${uploadSessionId}`)
      const data = await response.json()
      setUploadProgress(data.stats)
      
      if (data.session.status === 'COMPLETED') {
        clearInterval(interval)
        loadData() // Recarregar dados
      }
    }, 2000)
    
    return () => clearInterval(interval)
  }
}, [uploadSessionId])

Estatísticas Detalhadas

const stats = {
  total: uploadSession.totalFiles,
  processed: uploadSession.processedFiles,
  failed: uploadSession.failedFiles,
  pending: uploadSession.totalFiles - uploadSession.processedFiles - uploadSession.failedFiles,
  progress: uploadSession.totalFiles > 0 ? 
    Math.round((uploadSession.processedFiles / uploadSession.totalFiles) * 100) : 0
}

Tratamento de Erros e Recuperação

Sistema de Retry Inteligente

try {
  await processFile(file, projectId, userId, uploadSessionId);
} catch (error) {
  console.error(`Erro no upload do arquivo ${file.name}:`, error);
  
  // Atualizar contador de falhas
  await db.uploadSession.update({
    where: { id: uploadSessionId },
    data: { failedFiles: { increment: 1 } }
  });
  
  return null; // Continuar com próximo arquivo
}

Validação de Arquivos

const validFiles = files.filter(file => {
  const isValidType = ['image/jpeg', 'image/png'].includes(file.type);
  const isValidSize = file.size <= 5 * 1024 * 1024; // 5MB max
  return isValidType && isValidSize;
});

Processamento em Lotes Otimizado

Sistema de Batches Inteligente

const CONCURRENCY_LIMIT = 15; // Máximo 15 lotes simultâneos
const BATCH_SIZE = 50; // 50 arquivos por lote

const processBatch = async (batchFiles: UploadedFile[], batchIndex: number) => {
  const batchSizeMB = batchFiles.reduce((sum, file) => sum + file.size, 0) / (1024 * 1024);
  
  try {
    const formData = new FormData();
    batchFiles.forEach(file => formData.append('files', file.file));
    
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData
    });
    
    if (response.ok) {
      successCount += batchFiles.length;
      toast.success(`Lote ${batchIndex + 1} processado: ${batchFiles.length} arquivos`);
    }
  } catch (err) {
    errorCount += batchFiles.length;
    toast.error(`Erro no lote ${batchIndex + 1}: Erro de conexão`);
  }
};

Resultados Alcançados

Performance Detalhada:

Métricas Específicas:

Stack Técnica Completa

Desafios Enfrentados e Soluções

1. Timeout de Upload

Problema: Uploads grandes causavam timeout no servidor Solução: Sistema de batches com limite de concorrência

2. Consumo de Memória

Problema: Processar muitas imagens simultaneamente esgotava RAM Solução: Pool de concorrência limitado + streaming de arquivos

3. Falhas de Rede

Problema: Conexões instáveis causavam perda de arquivos Solução: Sistema de retry + tracking de sessões

4. Monitoramento de Progresso

Problema: Usuários não sabiam o status do processamento Solução: API de status em tempo real + WebSocket para updates

Lições Aprendidas

  1. GCS é game-changer - escalabilidade infinita sem dor de cabeça
  2. Controle de concorrência é crucial - sem limites, você sobrecarrega o servidor
  3. Processamento assíncrono permite UX fluida mesmo com grandes volumes
  4. Otimização de imagens reduz custos de storage e melhora performance
  5. Sistema de filas garante que nada se perca mesmo com falhas
  6. Buckets separados facilitam organização e controle de acesso
  7. Monitoramento em tempo real é essencial para UX profissional
  8. Validação rigorosa previne problemas antes de chegarem ao processamento
  9. Sistema de sessões permite recuperação de uploads interrompidos
  10. Batches inteligentes equilibram performance e estabilidade

Próximos Passos


Você já trabalhou com processamento de imagens em escala? Que desafios enfrentou?

Compartilhe suas experiências nos comentários!


Este artigo foi publicado originalmente no meu blog pessoal. Para mais conteúdo sobre desenvolvimento e arquitetura de software, visite danielmeloramos.github.io