Skip to content
Queues ohne Kafka: SQS, EventBridge und die eine Stelle, an der wir wirklich einen Stream brauchten
← ← Zurück zu Gedanken Cloud

Queues ohne Kafka: SQS, EventBridge und die eine Stelle, an der wir wirklich einen Stream brauchten

Jede Plattform erreicht den Moment, in dem eine Anfrage zu viel tut. Der Checkout-Handler verschickt eine E-Mail, aktualisiert den Suchindex, benachrichtigt das Lager, zeichnet ein Analytics-Ereignis auf und, ach ja, belastet auch noch die Karte. Das dauert vier Sekunden und scheitert, wenn eines der fünf Dinge langsam ist. Die Lösung ist eine Queue, und der erste Vorschlag lautet meist Kafka, weil Kafka das ist, was die großen Unternehmen benutzen, und die Konferenzvorträge von Kafka handeln.

Wir betreiben kein Kafka. Für eine Plattform, die wir für einen US-Kunden betreiben, nutzen wir SQS für Arbeit, EventBridge für Ereignisse und einen einzigen Kinesis-Stream für den einen Workload, der Reihenfolge und Replay brauchte. Hier ist, wie wir es aufgeteilt haben, was jedes kostet, und der Test dafür, ob Sie überhaupt einen Stream brauchen.

Drei verschiedene Probleme

„Queue“ trägt in den meisten Architekturgesprächen eine Menge Last. Darunter verstecken sich drei Formen.

Arbeit, die einmal von jemandem erledigt werden muss. Diese E-Mail senden. Dieses Bild skalieren. Diese Bestellung mit dem Lager synchronisieren. Dem Erzeuger ist egal, wer es tut oder wann, nur dass es passiert, und einmal passiert. Das will eine Queue: Ein Element wird an einen Consumer geliefert, bei Erledigung bestätigt, sonst wiederholt, nach zu vielen Fehlern in die Dead-Letter-Queue geschoben.

Etwas ist passiert, und jeder Interessierte sollte es wissen. Eine Bestellung wurde aufgegeben. Ein Kunde hat seine E-Mail geändert. Der Erzeuger weiß nicht, wer zuhört, und sollte es nicht wissen müssen. Das will einen Event-Bus: einmal veröffentlichen, viele Abonnenten, jeder mit eigener Queue dahinter, hinzugefügt und entfernt, ohne den Erzeuger anzufassen.

Eine geordnete, wiederabspielbare Historie. Jede Änderung an diesem Konto, in Reihenfolge, die ein Consumer von jedem Punkt aus lesen und nach einem Bug erneut lesen kann. Das will einen Stream, und es ist die einzige der drei Formen, in der Kafka einzigartig gut ist. Es ist auch der seltenste Bedarf.

Die meisten Plattformen haben viel von der ersten, etwas von der zweiten und eine oder null von der dritten. Kafka kann alle drei, zum Preis, Kafka zu betreiben, oder für ein verwaltetes Kafka zu zahlen, das bei ein paar hundert Dollar im Monat beginnt, bevor Sie eine Nachricht gesendet haben.

Queue · Arbeittu das einmal, irgendjemandein Consumer nimmt esAck · Retry · Dead-LetterSQS · 11 QueuesE-Mails, Bildskalierung, Lager-Sync~2 $ / Monat Event-Bus · Faktendas ist passierteinmal veröffentlichen, viele Abonnentenjeder bekommt seine eigene QueueEventBridge · 1 Bus · 9 RegelnOrderPlaced, CustomerUpdated, RefundIssued~1 $ / Monat Stream · Historiegeordnet pro Schlüssel, wiederabspielbarvon jedem Punkt lesbarnach einem Bug erneut lesenKinesis · 1 Stream · 1 Sharddie Hauptbuch-Projektion, und nur die~15 $ / Monat Kafka kann alle drei. Verwaltetes Kafka beginnt bei ein paar hundert im Monat; selbst gehostet beginnt bei einem Operator. Die meisten Plattformen brauchen viel vom ersten, etwas vom zweiten und eins oder null vom dritten.

Queues: SQS

Elf SQS-Queues, eine pro Art von Arbeit, jede mit einer Dead-Letter-Queue und einem Lambda oder einem App-Runner-Worker, der sie konsumiert. Der Checkout-Handler, der vier Sekunden brauchte, schreibt jetzt die Bestellung, belastet die Karte (das Einzige, was synchron sein muss), veröffentlicht ein Ereignis und kehrt in 400 ms zurück. Alles andere ist ein Queue-Consumer.

Die Einstellungen, die zählen, in CDK:

const dlq = new sqs.Queue(this, 'EmailDlq', { retentionPeriod: Duration.days(14) });
const emailQueue = new sqs.Queue(this, 'EmailQueue', {
  visibilityTimeout: Duration.seconds(90),      // > 6 × das Timeout des Consumers
  deadLetterQueue: { queue: dlq, maxReceiveCount: 5 },
  encryption: sqs.QueueEncryption.SQS_MANAGED,
});
new lambda.EventSourceMapping(this, 'EmailConsumer', {
  target: emailFn, eventSourceArn: emailQueue.queueArn,
  batchSize: 10, reportBatchItemFailures: true,     // teilweiser Batch-Erfolg
  maxConcurrency: 20,                                // schützt den E-Mail-Anbieter
});
new cloudwatch.Alarm(this, 'EmailDlqAlarm', {
  metric: dlq.metricApproximateNumberOfMessagesVisible(), threshold: 1, evaluationPeriods: 1,
});

Drei Dinge, die in der ersten Version falsch waren und jetzt richtig sind. Das Visibility-Timeout war gleich dem Funktions-Timeout, also wurde eine langsame Nachricht erneut zugestellt, während sie noch verarbeitet wurde, und E-Mails gingen doppelt raus; jetzt ist es das Sechsfache des Funktions-Timeouts, wie die Doku sagt, die niemand liest. reportBatchItemFailures war aus, also ließ eine schlechte Nachricht in einem Zehnerbatch alle zehn scheitern, und neun gute E-Mails wurden je fünfmal wiederholt, bevor der Batch in der Dead-Letter-Queue landete. Und die Dead-Letter-Queue hatte keinen Alarm, also lagen Nachrichten eine Woche darin, bevor jemand hinsah; jetzt alarmiert sie bei eins.

FIFO oder Standard? Standard, überall außer beim Lager-Sync, bei dem „stornieren“ nach „anlegen“ für dieselbe Bestellung ankommen muss. FIFO mit der Bestell-ID als Message Group liefert das, zu etwa demselben Preis, mit einer Durchsatzgrenze pro Gruppe, von der wir weit entfernt sind. Setzen Sie FIFO nicht als Standard; es fügt einen Deduplizierungs- und Ordnungsvertrag hinzu, den die meiste Arbeit nicht will, und lässt eine hängende Nachricht alles hinter ihr in ihrer Gruppe blockieren.

Ereignisse: EventBridge

Ein eigener Bus. Erzeuger legen Ereignisse mit detail-type und Quelle ab; Regeln matchen und leiten an Ziele weiter, die fast immer eine SQS-Queue im Besitz des konsumierenden Dienstes sind, mit einem Lambda dahinter. Der Erzeuger von OrderPlaced hat keine Ahnung, dass fünf Dienste es abonnieren, und wenn nächstes Quartal der sechste auftaucht, ist das eine neue Regel und eine neue Queue, ohne Änderung am Checkout.

const bus = new events.EventBus(this, 'PlatformBus');
new events.Rule(this, 'OrderPlacedToSearch', {
  eventBus: bus,
  eventPattern: { source: ['platform.orders'], detailType: ['OrderPlaced'] },
  targets: [new targets.SqsQueue(searchIndexQueue)],
});
new events.Rule(this, 'AllEventsToArchive', {
  eventBus: bus, eventPattern: { source: [{ prefix: 'platform.' }] },
  targets: [new targets.CloudWatchLogGroup(eventArchive)],   // 30 Tage, für „was ist passiert?“
});

Die Archivregel ist der billige Trick, der Ihnen das meiste von dem gibt, was Leute von einem Stream wollen: eine durchsuchbare, zeitlich geordnete Aufzeichnung jedes Ereignisses, im selben Log-Speicher wie alles andere, ohne Stream. Sie kann nicht in einen Consumer zurückspielen, aber sie kann „haben wir OrderPlaced für Bestellung 4412 ausgelöst?“ in einer Abfrage beantworten, und das ist die Frage, die tatsächlich gestellt wird.

EventBridges Vertrag ist mindestens-einmal, ungeordnet, mit einem Payload-Limit von 256 KB. Jeder Consumer ist idempotent, geschlüsselt auf die Ereignis-ID, und jedes Ereignis über ein paar KB trägt einen Zeiger auf S3, nicht die Payload. Diese zwei Regeln decken jedes Problem ab, das wir damit hatten.

Der Stream: Kinesis, einmal

Das Hauptbuch. Jede finanzielle Bewegung auf der Plattform, in Reihenfolge pro Konto, projiziert in Salden und Berichte durch einen Consumer, der von Grund auf neu aufgebaut werden können muss, falls ein Projektionsfehler gefunden wird. Das ist das stream-förmige Problem: Reihenfolge pro Schlüssel und Replay von jedem Punkt.

Ein Kinesis-Stream, ein Shard, 7 Tage Aufbewahrung, ein Lambda-Consumer mit Checkpoint. Als im vierten Monat ein Projektionsfehler gefunden wurde, bestand die Lösung darin, den Consumer zu korrigieren, den Checkpoint auf den Beginn der Aufbewahrung zurückzusetzen und ihn drei Tage Ereignisse in eine frische Tabelle erneut lesen zu lassen. SQS kann das nicht; eine konsumierte Nachricht ist weg. EventBridge kann das nicht; das Archiv ist ein Log, kein Cursor.

Checkout · 400 msBestellung schreibenKarte belasten (sync)1 Ereignis veröffentlichen OrderPlaced EventBridge9 Regeln SQS · E-Mail → LambdaSQS · Suchindex → LambdaSQS FIFO · Lager, nach Bestell-IDSQS · Analytics → LambdaCloudWatch Logs · Archiv, 30 T Hauptbuch-Write Kinesis · 1 Shard Saldenprojektion · wiederabspielbar jeder Consumeridempotent auf Ereignis-IDeigene DLQ, eigener Alarm die einzige Stelle,die Reihenfolge + Replay braucht

Der Test für „brauchen Sie einen Stream?“

Fragen Sie: Wenn ein Consumer letzten Dienstag einen Bug hatte, müssen Sie ihm die Nachrichten vom Dienstag in Reihenfolge erneut zuführen? Wenn die ehrliche Antwort „wir würden einen Batch-Job gegen die Datenbank neu laufen lassen“ lautet, brauchen Sie eine Queue und eine Datenbank, keinen Stream. Wenn die Antwort „ja, und die Datenbank hat die Historie nicht“ lautet, brauchen Sie einen Stream, für diesen Consumer. Ein Stream für einen Consumer ist kein Grund, die ganze Plattform auf Kafka zu ziehen.

Was es kostet

Monatlich Betrieb
SQS, 11 Queues + 11 DLQs, ~4 Mio. Nachrichten ~2 $ Null. Alarme auf den DLQs
EventBridge, 1 Bus, 9 Regeln, ~1 Mio. Ereignisse ~1 $ Null. Archivregel für „was ist passiert“
Kinesis, 1 Shard, 7 Tage Aufbewahrung ~15 $ Checkpoint-Überwachung; Shard-Anzahl, falls sie je relevant wird
Gesamt ~18 $
Verwaltetes Kafka, kleinster sinnvoller Cluster 300–600 $ Topics, Partitionen, Consumer Groups, Aufbewahrung, eine Broker-Version zum Nachhalten

Achtzehn Dollar und kein Broker. Die Plattform verarbeitet ein paar Millionen Nachrichten im Monat; SQS würde ein paar Milliarden in derselben Form verarbeiten, mit linear skalierender Rechnung und nichts, das neu zu architekturieren wäre.

Die Kurzfassung

Teilen Sie „Queue“ in drei Probleme. Arbeit geht an SQS mit Dead-Letter-Queue und Alarm. Fakten gehen an EventBridge mit einer Queue pro Abonnent und einer Archivregel. Historie, wenn Sie wirklich einen Consumer haben, der in Reihenfolge wiederabspielen muss, geht in einen Kinesis-Stream, für diesen Consumer. Kafka ist die richtige Antwort, wenn Sie viel vom dritten Problem haben, und es ist in Ordnung, dieses Problem nicht zu haben.

Wenn Ihr Checkout vier Sekunden dauert, weil er fünf Dinge tut, nehmen wir in einer Woche vier davon vom Anfragepfad, für etwa zwei Dollar im Monat.