Construindo para sistemas grandes e tarefas em segundo plano de longa duração.
Crédito: Ilias Chebbi no UnsplashHá meses, assumi o papel que exigia construir infraestrutura para streaming de mídia (áudio). Mas além de servir áudio como fragmentos transmissíveis, havia tarefas de processamento de mídia de longa duração e um extenso pipeline RAG que atendia à transcrição, transcodificação, incorporação e atualizações sequenciais de mídia. Construir um MVP com uma mentalidade de produção fez-nos reiterar até alcançarmos um sistema perfeito. Nossa abordagem tem sido uma onde integramos recursos e a pilha subjacente de prioridades.
Ao longo da construção, cada iteração surgiu como resposta a uma necessidade imediata e frequentemente "abrangente". A preocupação inicial era enfileirar tarefas, o que prontamente bastou com Redis; simplesmente disparamos e esquecemos. Bull MQ na estrutura NEST JS deu-nos um controlo ainda melhor sobre novas tentativas, acumulações e a fila de cartas mortas. Localmente e com algumas cargas úteis em produção, acertamos no fluxo de mídia. Logo fomos sobrecarregados pelo peso da Observabilidade:
Logs → Registo de tarefas (pedidos, respostas, erros).
Métricas → Quanto / com que frequência estas tarefas são executadas, falham, completam, etc.
Rastreios → O caminho que uma tarefa percorreu através dos serviços (funções/métodos chamados dentro do caminho do fluxo).
Pode resolver alguns destes problemas projetando APIs e construindo um painel personalizado para conectá-los, mas o problema de escalabilidade será suficiente. E, de facto, projetamos as APIs.
O desafio de gerir fluxos de trabalho de backend complexos e de longa duração, onde as falhas devem ser recuperáveis e o estado deve ser durável, o Inngest tornou-se a nossa salvação arquitetónica. Reformulou fundamentalmente a nossa abordagem: cada tarefa em segundo plano de longa duração torna-se uma função em segundo plano, desencadeada por um evento específico.
Por exemplo, um evento Transcription.request desencadeará uma função TranscribeAudio. Esta função pode conter execuções de etapas para: fetch_audio_metadata, deepgram_transcribe, parse_save_trasncription e notify_user.
A primitiva de durabilidade central são as execuções de etapas. Uma função em segundo plano é internamente dividida nestas execuções de etapas, cada uma contendo um bloco mínimo e atómico de lógica.
Abstrato da função Inngest:
import { inngest } from 'inngest-client';
export const createMyFunction = (dependencies) => {
return inngest.createFunction(
{
id: 'my-function',
name: 'My Example Function',
retries: 3, // retry the entire run on failure
concurrency: { limit: 5 },
onFailure: async ({ event, error, step }) => {
// handle errors here
await step.run('handle-error', async () => {
console.error('Error processing event:', error);
});
},
},
{ event: 'my/event.triggered' },
async ({ event, step }) => {
const { payload } = event.data;
// Step 1: Define first step
const step1Result = await step.run('step-1', async () => {
// logic for step 1
return `Processed ${payload}`;
});
// Step 2: Define second step
const step2Result = await step.run('step-2', async () => {
// logic for step 2
return step1Result + ' -> step 2';
});
// Step N: Continue as needed
await step.run('final-step', async () => {
// finalization logic
console.log('Finished processing:', step2Result);
});
return { success: true };
},
);
};
O modelo orientado a eventos do Inngest fornece uma visão granular de cada execução do fluxo de trabalho:
A ressalva de confiar no processamento puro de eventos é que, embora o Inngest enfileire eficientemente as execuções de funções, os próprios eventos não são enfileirados internamente no sentido tradicional de um corretor de mensagens. Esta ausência de uma fila de eventos explícita pode ser problemática em cenários de alto tráfego devido a potenciais condições de corrida ou eventos descartados se o endpoint de ingestão estiver sobrecarregado.
Para abordar isto e impor uma durabilidade estrita de eventos, implementamos um sistema de enfileiramento dedicado como buffer.
O AWS Simple Queue System (SQS) foi o sistema escolhido (embora qualquer sistema de enfileiramento robusto seja viável), dada a nossa infraestrutura existente na AWS. Arquitetamos um sistema de duas filas: uma Fila Principal e uma Fila de Cartas Mortas (DLQ).
Estabelecemos um Ambiente de Trabalhador Elastic Beanstalk (EB) especificamente configurado para consumir mensagens diretamente da Fila Principal. Se uma mensagem na Fila Principal falhar ao ser processada pelo Trabalhador EB um número definido de vezes, a Fila Principal move automaticamente a mensagem falhada para a DLQ dedicada. Isto garante que nenhum evento seja perdido permanentemente se falhar ao ser acionado ou ser recolhido pelo Inngest. Este ambiente de trabalhador difere de um ambiente de servidor web EB padrão, pois sua única responsabilidade é o consumo e processamento de mensagens (neste caso, encaminhar a mensagem consumida para o endpoint da API Inngest).
Uma parte subestimada e bastante pertinente da construção de infraestrutura em escala empresarial é que ela consome recursos, e eles são de longa duração. A arquitetura de microserviços fornece escalabilidade por serviço. Armazenamento, RAM e timeouts de recursos entrarão em jogo. Nossa especificação para o tipo de instância AWS, por exemplo, moveu-se rapidamente de t3.micro para t3.small, e agora está fixada em t3.medium. Para tarefas em segundo plano de longa duração e intensivas em CPU, o escalonamento horizontal com instâncias pequenas falha porque o gargalo é o tempo que leva para processar uma única tarefa, não o volume de novas tarefas entrando na fila.
Tarefas ou funções como transcodificação, incorporação são tipicamente limitadas por CPU e limitadas por Memória. Limitadas por CPU porque requerem uso sustentado e intenso de CPU, e Limitadas por Memória porque frequentemente requerem RAM substancial para carregar grandes modelos ou lidar com arquivos grandes ou cargas úteis de forma eficiente.
Em última análise, esta arquitetura aumentada, colocando a durabilidade do SQS e a execução controlada de um ambiente de Trabalhador EB diretamente a montante da API Inngest, forneceu resiliência essencial. Alcançamos propriedade estrita de eventos, eliminamos condições de corrida durante picos de tráfego e ganhamos um mecanismo de carta morta não volátil. Aproveitamos o Inngest para suas capacidades de orquestração de fluxo de trabalho e depuração, enquanto confiamos em primitivas AWS para máximo rendimento de mensagens e durabilidade. O sistema resultante não é apenas escalável, mas altamente auditável, traduzindo com sucesso tarefas de backend complexas e de longa duração em micro-etapas seguras, observáveis e tolerantes a falhas.
Building Spotify for Sermons. foi originalmente publicado em Coinmonks no Medium, onde as pessoas estão continuando a conversa destacando e respondendo a esta história.


