Costruire per Sistemi di Grandi Dimensioni e Lavori in Background di Lunga Durata.
Credito: Ilias Chebbi su UnsplashMesi fa, ho assunto il ruolo che richiedeva la costruzione di infrastrutture per lo streaming di media (audio). Ma oltre a servire l'audio come blocchi trasmissibili, c'erano lavori di elaborazione dei media di lunga durata e un'ampia pipeline RAG che si occupava di trascrizione, transcodifica, embedding e aggiornamenti sequenziali dei media. Costruire un MVP con una mentalità orientata alla produzione ci ha fatto reiterare fino a raggiungere un sistema senza interruzioni. Il nostro approccio è stato quello in cui abbiamo integrato funzionalità e lo stack sottostante di priorità.
Nel corso della costruzione, ogni iterazione è arrivata come risposta a un'esigenza immediata e spesso "onnicomprensiva". La preoccupazione iniziale era la messa in coda dei lavori, che si è rivelata sufficiente con Redis; semplicemente lanciavamo e dimenticavamo. Bull MQ nel framework NEST JS ci ha dato un controllo ancora migliore sui tentativi, i backlog e la coda delle lettere morte. Localmente e con alcuni payload in produzione, abbiamo ottenuto il flusso dei media corretto. Presto siamo stati gravati dal peso dell'Osservabilità:
Log → Registro dei lavori (richieste, risposte, errori).
Metriche → Quanto / quanto spesso questi lavori vengono eseguiti, falliscono, completano, ecc.
Tracce → Il percorso che un lavoro ha seguito attraverso i servizi (funzioni/metodi chiamati all'interno del percorso di flusso).
Puoi risolvere alcuni di questi progettando API e costruendo una dashboard personalizzata in cui inserirli, ma il problema della scalabilità sarà sufficiente. E infatti, abbiamo progettato le API.
La sfida di gestire flussi di lavoro backend complessi e di lunga durata, dove i fallimenti devono essere recuperabili e lo stato deve essere durevole, Inngest è diventata la nostra salvezza architettonica. Ha fondamentalmente riformulato il nostro approccio: ogni lavoro in background di lunga durata diventa una funzione in background, attivata da un evento specifico.
Ad esempio, un evento Transcription.request attiverà una funzione TranscribeAudio. Questa funzione potrebbe contenere step-run per: fetch_audio_metadata, deepgram_transcribe, parse_save_trasncription e notify_user.
Il primitivo di durabilità principale sono gli step-run. Una funzione in background è internamente suddivisa in questi step-run, ciascuno contenente un blocco minimo e atomico di logica.
Astratto della funzione 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 };
},
);
};
Il modello event-driven di Inngest fornisce un'intuizione granulare in ogni esecuzione del flusso di lavoro:
L'avvertenza nell'affidarsi all'elaborazione di eventi puri è che mentre Inngest mette in coda efficientemente le esecuzioni di funzioni, gli eventi stessi non sono messi in coda internamente nel senso tradizionale di un broker di messaggistica. Questa assenza di una coda di eventi esplicita può essere problematica in scenari ad alto traffico a causa di potenziali condizioni di gara o eventi persi se l'endpoint di ingestione è sovraccarico.
Per affrontare questo problema e imporre una rigorosa durabilità degli eventi, abbiamo implementato un sistema di accodamento dedicato come buffer.
AWS Simple Queue System (SQS) è stato il sistema scelto (anche se qualsiasi sistema di accodamento robusto è fattibile), data la nostra infrastruttura esistente su AWS. Abbiamo architettato un sistema a due code: una Coda Principale e una Coda delle Lettere Morte (DLQ).
Abbiamo stabilito un Ambiente Worker Elastic Beanstalk (EB) specificamente configurato per consumare messaggi direttamente dalla Coda Principale. Se un messaggio nella Coda Principale non viene elaborato dal Worker EB un numero prestabilito di volte, la Coda Principale sposta automaticamente il messaggio fallito nella DLQ dedicata. Questo assicura che nessun evento venga perso permanentemente se non riesce ad attivare o ad essere prelevato da Inngest. Questo ambiente worker differisce da un ambiente server web EB standard, poiché la sua unica responsabilità è il consumo e l'elaborazione dei messaggi (in questo caso, l'inoltro del messaggio consumato all'endpoint API di Inngest).
Una parte sottovalutata e piuttosto pertinente della costruzione di infrastrutture su scala aziendale è che consuma risorse, e sono di lunga durata. L'architettura dei microservizi fornisce scalabilità per servizio. Archiviazione, RAM e timeout delle risorse entreranno in gioco. La nostra specifica per il tipo di istanza AWS, ad esempio, è passata rapidamente da t3.micro a t3.small, e ora è fissata a t3.medium. Per lavori in background di lunga durata e ad alta intensità di CPU, la scalabilità orizzontale con istanze minuscole fallisce perché il collo di bottiglia è il tempo necessario per elaborare un singolo lavoro, non il volume di nuovi lavori che entrano nella coda.
Lavori o funzioni come transcodifica, embedding sono tipicamente vincolati dalla CPU e vincolati dalla memoria. Vincolati dalla CPU perché richiedono un uso sostenuto e intenso della CPU, e vincolati dalla memoria perché spesso richiedono RAM sostanziale per caricare modelli di grandi dimensioni o gestire file o payload di grandi dimensioni in modo efficiente.
In definitiva, questa architettura aumentata, che posiziona la durabilità di SQS e l'esecuzione controllata di un ambiente Worker EB direttamente a monte dell'API Inngest, ha fornito una resilienza essenziale. Abbiamo ottenuto una rigorosa proprietà degli eventi, eliminato le condizioni di gara durante i picchi di traffico e acquisito un meccanismo di lettere morte non volatile. Abbiamo sfruttato Inngest per le sue capacità di orchestrazione del flusso di lavoro e di debug, mentre ci siamo affidati ai primitivi AWS per il massimo throughput dei messaggi e durabilità. Il sistema risultante non è solo scalabile ma altamente verificabile, traducendo con successo lavori backend complessi e di lunga durata in micro-passi sicuri, osservabili e tolleranti ai guasti.
Building Spotify for Sermons. è stato originariamente pubblicato in Coinmonks su Medium, dove le persone stanno continuando la conversazione evidenziando e rispondendo a questa storia.


