Aufbau für große Systeme und lang laufende Hintergrundaufgaben.
Bildnachweis: Ilias Chebbi auf UnsplashVor einigen Monaten übernahm ich die Rolle, die den Aufbau einer Infrastruktur für Medien(Audio)-Streaming erforderte. Aber über das Bereitstellen von Audio als streambare Chunks hinaus gab es lang laufende Medienverarbeitungsaufgaben und eine umfangreiche RAG-Pipeline, die Transkription, Transkodierung, Einbettung und sequentielle Medienaktualisierungen umfasste. Der Aufbau eines MVP mit einer Produktionsdenkweise ließ uns so lange wiederholen, bis wir ein nahtloses System erreicht hatten. Unser Ansatz war einer, bei dem wir Funktionen und den zugrunde liegenden Stack von Prioritäten integrierten.
Im Laufe des Aufbaus kam jede Iteration als Reaktion auf unmittelbare und oft "umfassende" Bedürfnisse. Das anfängliche Anliegen war die Warteschlange von Aufgaben, was mit Redis leicht ausreichte; wir haben einfach abgefeuert und vergessen. Bull MQ im NEST JS-Framework gab uns eine noch bessere Kontrolle über Wiederholungsversuche, Rückstände und die Dead-Letter-Warteschlange. Lokal und mit einigen Nutzlasten in der Produktion haben wir den Medienfluss richtig hinbekommen. Wir wurden bald durch das Gewicht der Beobachtbarkeit belastet:
Logs → Aufzeichnung von Aufgaben (Anfragen, Antworten, Fehler).
Metriken → Wie viel / wie oft diese Aufgaben ausgeführt werden, fehlschlagen, abgeschlossen werden usw.
Traces → Der Pfad, den eine Aufgabe über Dienste hinweg genommen hat (Funktionen/Methoden, die innerhalb des Flusspfades aufgerufen wurden).
Sie können einige dieser Probleme lösen, indem Sie APIs entwerfen und ein benutzerdefiniertes Dashboard erstellen, um sie einzubinden, aber das Problem der Skalierbarkeit wird ausreichen. Und tatsächlich haben wir die APIs entworfen.
Die Herausforderung der Verwaltung komplexer, lang laufender Backend-Workflows, bei denen Fehler wiederherstellbar sein müssen und der Zustand dauerhaft sein muss, wurde Inngest zu unserer architektonischen Rettung. Es hat unseren Ansatz grundlegend neu gestaltet: jede lang laufende Hintergrundaufgabe wird zu einer Hintergrundfunktion, die durch ein bestimmtes Ereignis ausgelöst wird.
Zum Beispiel wird ein Transcription.request Ereignis eine TranscribeAudio Funktion auslösen. Diese Funktion könnte Step-Runs für Folgendes enthalten: fetch_audio_metadata, deepgram_transcribe, parse_save_trasncription und notify_user.
Das Kernhaltbarkeitsprimitive sind die Step-Runs. Eine Hintergrundfunktion wird intern in diese Step-Runs aufgeteilt, von denen jeder einen minimalen, atomaren Logikblock enthält.
Inngest-Funktionsabstrakt:
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 };
},
);
};
Das ereignisgesteuerte Modell von Inngest bietet granulare Einblicke in jede Workflow-Ausführung:
Der Vorbehalt bei der Abhängigkeit von reiner Ereignisverarbeitung ist, dass, während Inngest Funktionsausführungen effizient in die Warteschlange stellt, die Ereignisse selbst nicht intern in die Warteschlange gestellt werden im Sinne eines traditionellen Messaging-Brokers. Dieses Fehlen einer expliziten Ereigniswarteschlange kann in Szenarien mit hohem Verkehrsaufkommen problematisch sein, aufgrund potenzieller Race-Conditions oder verworfener Ereignisse, wenn der Aufnahmeendpunkt überlastet ist.
Um dies zu adressieren und strikte Ereignishaltbarkeit durchzusetzen, haben wir ein dediziertes Warteschlangensystem als Puffer implementiert.
AWS Simple Queue System (SQS) war das System der Wahl (obwohl jedes robuste Warteschlangensystem machbar ist), angesichts unserer bestehenden Infrastruktur auf AWS. Wir haben ein Zwei-Warteschlangen-System entworfen: eine Hauptwarteschlange und eine Dead Letter Queue (DLQ).
Wir haben eine Elastic Beanstalk (EB) Worker-Umgebung eingerichtet, die speziell konfiguriert ist, um Nachrichten direkt aus der Hauptwarteschlange zu konsumieren. Wenn eine Nachricht in der Hauptwarteschlange vom EB-Worker eine festgelegte Anzahl von Malen nicht verarbeitet werden kann, verschiebt die Hauptwarteschlange automatisch die fehlgeschlagene Nachricht in die dedizierte DLQ. Dies stellt sicher, dass kein Ereignis dauerhaft verloren geht, wenn es nicht ausgelöst oder von Inngest aufgenommen werden kann. Diese Worker-Umgebung unterscheidet sich von einer Standard-EB-Webserver-Umgebung, da ihre einzige Verantwortung der Nachrichtenkonsum und die Verarbeitung ist (in diesem Fall die Weiterleitung der konsumierten Nachricht an den Inngest-API-Endpunkt).
Ein unterschätzter und eher relevanter Teil des Aufbaus von Infrastruktur im Unternehmensmaßstab ist, dass sie Ressourcen verbraucht und sie lang laufend sind. Die Microservices-Architektur bietet Skalierbarkeit pro Dienst. Speicher, RAM und Timeouts von Ressourcen werden ins Spiel kommen. Unsere Spezifikation für den AWS-Instanztyp beispielsweise wechselte schnell von t3.micro zu t3.small und ist jetzt bei t3.medium festgelegt. Für lang laufende, CPU-intensive Hintergrundaufgaben scheitert die horizontale Skalierung mit winzigen Instanzen, weil der Engpass die Zeit ist, die es braucht, um eine einzelne Aufgabe zu verarbeiten, nicht das Volumen neuer Aufgaben, die in die Warteschlange eintreten.
Aufgaben oder Funktionen wie Transkodierung, Einbettung sind typischerweise CPU-gebunden und Speicher-gebunden. CPU-gebunden, weil sie anhaltende, intensive CPU-Nutzung erfordern, und Speicher-gebunden, weil sie oft erheblichen RAM benötigen, um große Modelle zu laden oder große Dateien oder Nutzlasten effizient zu handhaben.
Letztendlich bot diese erweiterte Architektur, die die Haltbarkeit von SQS und die kontrollierte Ausführung einer EB-Worker-Umgebung direkt stromaufwärts der Inngest-API platziert, wesentliche Widerstandsfähigkeit. Wir haben strikte Ereigniseigentümerschaft erreicht, Race-Conditions während Verkehrsspitzen eliminiert und einen nicht-flüchtigen Dead-Letter-Mechanismus gewonnen. Wir nutzten Inngest für seine Workflow-Orchestrierungs- und Debugging-Fähigkeiten, während wir uns auf AWS-Primitive für maximalen Nachrichtendurchsatz und Haltbarkeit verließen. Das resultierende System ist nicht nur skalierbar, sondern auch hochgradig prüfbar und übersetzt erfolgreich komplexe, lang laufende Backend-Aufgaben in sichere, beobachtbare und fehlertolerante Mikroschritte.
Building Spotify for Sermons. wurde ursprünglich in Coinmonks auf Medium veröffentlicht, wo Menschen das Gespräch fortsetzen, indem sie diese Geschichte hervorheben und darauf reagieren.

