Não bloqueie o loop de eventos: dimensionando a renderização pesada de vídeo com Node.js, Redis e BullMQ

Quando comecei a construir Estúdio de Animação Foog (parte do nosso ecossistema SaaS em Tecnologia Foog), bati em uma parede enorme: A renderização de vídeo é computacionalmente cara.

Se você tentar processar um vídeo 1080p com transições usando FFmpeg diretamente dentro de seu controlador Express principal, seu loop de eventos Node.js será bloqueado imediatamente. Seu servidor irá parar de responder a outros usuários, as APIs atingirão o tempo limite e seu aplicativo travará efetivamente sob pressão.

Veja como resolvemos esse pesadelo arquitetônico e construímos um sistema que pode ser dimensionado perfeitamente.



❌ A abordagem ingênua (o que NÃO fazer)

Como desenvolvedores, nosso primeiro instinto geralmente é apenas exec um processo filho e espere por ele.

// 🚨 Anti-Pattern: Blocking the main thread visually (even if async, it eats resources)
app.post('/api/render', async (req, res) => {
  const { images, text } = req.body;

  // Running heavy FFmpeg tasks right in the API layer
  await utils.renderVideoLocally(images, text); 

  res.json({ status: "done", videoUrl: "/videos/result.mp4" });
});
Entre no modo de tela cheia

Sair do modo de tela cheia

O problema: Se 10 usuários clicarem em “Renderizar” simultaneamente, seu servidor ativará 10 instâncias FFmpeg. Sua CPU atinge 100%, OOM (Out of Memory) mata o processo do nó e todos recebem uma 502 Bad Gateway.



✅ A abordagem do arquiteto: microsserviços orientados a eventos

Para corrigir isso, desacoplamos o Camada API do Camada de Processamento. Introduzimos um corretor de mensagens (Redis) e um sistema de filas robusto (BullMQ).

Aqui está a arquitetura de alto nível do nosso mecanismo de vídeo:

  1. Cliente envia uma solicitação HTTP para criar um vídeo.
  2. Servidor Expresso valida a carga útil, salva um registro “Pendente” em um banco de dados PostgreSQL (via Sequelize) e envia um Job para o Fila BullMQ.
  3. Servidor Expresso responde imediatamente ao Cliente: HTTP 202 Accepted + ID do trabalho.
  4. Servidor de trabalho (um processo Node completamente separado) seleciona o trabalho do Redis.
  5. Trabalhador corre fluent-ffmpegrenderiza o vídeo, carrega-o no S3/Cloud Storage e atualiza o banco de dados.
  6. Cliente pesquisa ou recebe um evento Webhook/WebSocket notificando que o vídeo está pronto.


Por que BullMQ + Redis?

Escolhemos o BullMQ porque ele é apoiado pelo Redis e fornece recursos prontos para uso que são essenciais para SaaS corporativo:

  • Controle de simultaneidade: Podemos limitar os trabalhadores a processar apenas 2 vídeos por vez por núcleo da CPU.
  • Novas tentativas e retirada: Se a renderização falhar (por exemplo, uma imagem corrompida), o BullMQ tentará automaticamente 3 vezes com espera exponencial.
  • Resiliência: Se o servidor Worker travar durante a renderização, o trabalho não será perdido. Ele volta para a fila.


📦 A implementação mínima

Aqui está uma versão simplificada de nossa arquitetura desacoplada.

1. O Controlador API (O Produtor):

import { Queue } from 'bullmq';
import { connection } from '../config/redis';

// Create the Queue
const videoQueue = new Queue('videoRenderQueue', { connection });

export const requestVideoRender = async (req, res) => {
  const { projectId, assets } = req.body;

  // Add job to the queue
  const job = await videoQueue.add('renderTask', { projectId, assets });

  // Respond immediately! Don't wait for FFmpeg.
  return res.status(202).json({ 
    message: "Rendering started", 
    jobId: job.id 
  });
};
Entre no modo de tela cheia

Sair do modo de tela cheia

2. O Processo de Trabalho (O Consumidor):

import { Worker } from 'bullmq';
import { renderEngine } from './ffmpegTask';
import { connection } from '../config/redis';

// Note: Concurrency set to 2 to avoid melting the CPU
const videoWorker = new Worker('videoRenderQueue', async (job) => {
  console.log(`Processing job ${job.id} for project ${job.data.projectId}`);

  try {
     // Heavy FFmpeg lifting happens here
    const videoUrl = await renderEngine(job.data.assets);

    // Update DB to "Completed"
    await DB.Project.update({ status: 'DONE', url: videoUrl }, { where: { id: job.data.projectId }});

  } catch (error) {
    throw error; // Let BullMQ handle retries
  }
}, { connection, concurrency: 2 });

console.log("👷 Video Worker listening for jobs...");
Entre no modo de tela cheia

Sair do modo de tela cheia



🎯 O impacto nos negócios

Ao separar a API do Worker, alcançamos:

  1. Tempo de inatividade zero: A API Express responde em menos de 50 ms, independentemente de quantos vídeos estão sendo renderizados.
  2. Escalabilidade infinita: Se nossa base de clientes dobrar, não mexeremos no servidor API. Acabamos de ativar um segundo Worker Server na AWS para consumir a fila do Redis.
  3. Melhor experiência do usuário: Os usuários veem uma bela barra de progresso em vez de uma roda giratória da morte aguardando uma solicitação HTTP de 2 minutos.

Como arquitetos de SaaS, nosso trabalho não é apenas fazer as coisas funcionarem; é para fazê-los resiliente.

Se você estiver construindo sistemas B2B complexos ou mecanismos de processamento pesado, sempre trate seu thread principal como um rei: Mantenha-o livre e deixe os trabalhadores fazerem o trabalho pesado.


Sou Rodrigo Hernández, CEO e arquiteto líder da Tecnologia Foog. Construímos sistemas B2B SaaS e empresariais de alto desempenho. Se você achou esta análise arquitetônica útil, vamos nos conectar!

Deseja saber mais sobre Programação e Desenvolvimento Clique Aqui!

nó,arquitetura,redis,backend,software,codificação,desenvolvimento,engenharia,comunidade inclusiva,

Deixe um comentário

Translate »