Architekturdiagramm einer Chat-App: WebSockets, Fan-out und Nachrichtenspeicher

Das Architekturdiagramm einer Chat-App: WebSocket-Gateways, Nachrichtenreihenfolge, idempotentes Senden, Fan-out an Online- und Offline-Nutzer, Presence und Speicher.

· Gründer & Entwickler

Veröffentlicht am

Architekturdiagramm einer Chat-App: WebSockets, Fan-out und Nachrichtenspeicher

Eine Chat-App ist die System-Design-Frage, die am meisten Zustand versteckt. Jeder Nutzer hält eine Live-Verbindung zu genau einem Server, jede Nachricht muss genau einmal und in der richtigen Reihenfolge ankommen, und dieselbe Nachricht muss vielleicht eine Person oder zehntausend erreichen. Ein Diagramm mit „Client, Server, Datenbank“ beantwortet nichts davon.

Dieser Beitrag geht ein Chat-Backend Entscheidung für Entscheidung durch: wie Clients sich verbinden, wie eine Nachricht ihren Platz in einer Unterhaltung bekommt, wie sie alle erreicht, die sie sehen sollen, und wie sie gespeichert wird. Am Ende steht ein Architekturdiagramm, das du als Ausgangspunkt nutzen kannst.

Anforderungen

  • Einzel- und Gruppenunterhaltungen, mit Text und Medien.
  • Nachrichten kommen bei jedem Mitglied einer Unterhaltung in derselben Reihenfolge an.
  • Keine Duplikate, wenn ein Client einen Sendevorgang wiederholt.
  • Online-Nutzer bekommen Nachrichten innerhalb einer Sekunde; Offline-Nutzer bekommen eine Push-Benachrichtigung und holen nach, wenn sie sich wieder verbinden.
  • Zustell- und Lesebestätigungen sowie eine Online-Anzeige.

Verbindungen: warum WebSockets und warum Gateways

Nach neuen Nachrichten zu pollen verschwendet Anfragen und erzeugt Verzögerung. Eine WebSocket-Verbindung bleibt offen, sodass der Server eine Nachricht in dem Moment pushen kann, in dem sie existiert. Jedes Gerät hält eine Verbindung zu einem Chat-Gateway.

Gateways sind der zustandsbehaftete Teil des Systems. Ein Gateway weiß, welche Nutzer mit ihm verbunden sind, und sonst nichts. Halte Gateways schlank: die Verbindung einmal authentifizieren, Nachrichten annehmen und an einen zustandslosen Service weiterreichen, ausgehende Nachrichten in den richtigen Socket schieben. Wenn du eine neue Gateway-Version deployst, brechen Verbindungen ab und Clients verbinden sich neu. Clients müssen sich also mit Backoff und Jitter neu verbinden und ab der letzten gesehenen Nachricht fortsetzen.

Setz einen Layer-4-Load-Balancer davor, oder einen, der WebSocket-Upgrades versteht, und rechne damit, dass langlebige Verbindungen die Last ungleich verteilen. Neue Verbindungen sollten das am wenigsten ausgelastete Gateway bevorzugen.

Eine Nachricht senden: Idempotenz und Reihenfolge

Der Client erzeugt vor dem Senden eine client_msg_id (eine UUID). Bricht die Verbindung ab, bevor die Bestätigung ankommt, sendet der Client erneut mit derselben ID, und der Nachrichtenservice verwirft das Duplikat. Ohne sie erzeugen wacklige Mobilfunknetze doppelte Nachrichten.

Die Reihenfolge gilt pro Unterhaltung, nicht global. Der Nachrichtenservice vergibt jeder Nachricht die nächste Sequenznummer ihrer Unterhaltung und speichert sie. Clients sortieren nach dieser Nummer, nie nach ihrer eigenen Uhr. Zwei praktische Wege, sie zu vergeben:

  • Ein Zähler pro Unterhaltung, erhöht im selben Schreibvorgang, der die Nachricht speichert.
  • Eine zeitlich sortierbare ID, etwa eine vom Server erzeugte ID im Snowflake-Stil, wenn du akzeptieren kannst, dass „Reihenfolge“ „Reihenfolge, wie der Server sie gesehen hat“ bedeutet.

Die Sequenznummer macht auch das Nachholen günstig. Ein Client, der sich neu verbindet, sagt „Ich habe Unterhaltung 42 bis 1871“ und bekommt alles danach.

Fan-out: online, offline und große Gruppen

Nachdem die Nachricht gespeichert ist, veröffentlicht der Service ein Event. Fan-out-Worker übernehmen von dort:

  1. Die Mitglieder der Unterhaltung nachschlagen.
  2. In einem Presence- und Session-Register wie Redis nachschlagen, welche Mitglieder online sind und welches Gateway ihre Verbindung hält.
  3. Die Nachricht an diese Gateways schicken, die sie in die Sockets pushen.
  4. Mitgliedern, die offline sind, Push-Benachrichtigungen über APNs und FCM senden.

Die Nachricht einmal zu schreiben und beim Lesen aufzufächern hält den Speicher linear in der Zahl der Nachrichten, nicht der Mitglieder. Große Gruppen brauchen Sorgfalt: Ein Kanal mit 100.000 Mitgliedern sollte nicht 100.000 Push-Benachrichtigungen pro Nachricht auslösen. Benachrichtige nur Mitglieder, die es aktiviert haben, oder bündle und fasse zusammen.

Presence

Presence ist ein Heartbeat mit TTL. Das Gateway erneuert etwa alle 30 Sekunden „Nutzer 7 ist mit Gateway 3 verbunden“; läuft der Key ab, ist der Nutzer offline. Sende nicht jede Presence-Änderung in Echtzeit an jeden Kontakt. Gemessen daran, wie wenig Nutzer Verzögerungen dabei bemerken, ist es eines der teuersten Features im Chat. Hol die Presence, wenn eine Unterhaltung geöffnet wird, und aktualisiere sie regelmäßig. Ablaufende Keys funktionieren wie Cache-Einträge; der Beitrag zu Caching-Strategien behandelt TTLs und was passiert, wenn sie alle gleichzeitig ablaufen.

Bestätigungen und Ende-zu-Ende-Verschlüsselung

Zustell- und Lesebestätigungen sind ebenfalls Nachrichten. Empfängt ein Gerät Nachricht 1871, sendet es ein kleines Event „zugestellt bis 1871“; öffnet der Nutzer die Unterhaltung, sendet es „gelesen bis 1871“. Speichere die höchste Nummer pro Mitglied und Unterhaltung statt einer Bestätigung pro Nachricht, dann bleiben die Kosten in aktiven Gruppen flach.

Ende-zu-Ende-Verschlüsselung ändert, was der Server sehen kann. Mit einem Protokoll wie dem von Signal speichert und leitet der Server Chiffretext weiter, also wandern Suche, Moderation und Link-Vorschauen auf die Clients. Entscheide früh, denn es ändert sowohl den Speicher als auch den Fan-out.

Speicher

Nachrichten werden einmal geschrieben, meist kurz danach gelesen und fast immer pro Unterhaltung gelesen. Das spricht für einen Speicher, der nach Unterhaltung partitioniert und nach Sequenznummer sortiert ist. Discord partitioniert Nachrichten zum Beispiel nach Kanal plus festem Zeit-Bucket, damit ein aktiver Kanal nicht zu einer riesigen Partition wird; der Beitrag darüber, wie Discord Billionen Nachrichten speichert, beschreibt den Umzug dieses Modells von Cassandra zu ScyllaDB.

Halte Medien aus dem Nachrichtenspeicher heraus. Clients laden Bilder und Videos mit einer vorab signierten URL direkt in einen Objektspeicher, und die Nachricht trägt eine Referenz. Der Nachrichtenservice fasst die Bytes nie an.

Das Architekturdiagramm

Nutzer verbinden sich per WebSocket über einen Load Balancer mit Chat-Gateways. Die Gateways geben Nachrichten an einen Nachrichtenservice, der sie speichert und Events veröffentlicht. Fan-out-Worker prüfen Presence in Redis, liefern über Gateways aus und pushen an Offline-Mitglieder.

11 Komponenten · 11 Verbindungen

In Archboard öffnen
Diagramminhalt
  • Users: mobile and web
  • Load balancer
  • Chat gateway
  • Message service
  • Message store
  • Message events
  • Fan-out workers
  • Presence and sessions
  • APNs and FCM
  • Media bucket
  • wss
  • send
  • append, next seq
  • event
  • who is online, which gateway
  • deliver
  • offline
  • heartbeat
  • pre-signed upload
Ein Chat-Backend mit zustandsbehafteten WebSocket-Gateways und einem zustandslosen Nachrichtenservice. Nachrichten bekommen eine Sequenznummer pro Unterhaltung, werden einmal gespeichert und erreichen Online-Mitglieder über ihr Gateway, Offline-Mitglieder per Push.

So liest du es:

  • Links: Nutzer verbinden sich per WebSocket (wss) über den Load Balancer mit einem Chat-Gateway. Medien-Uploads gehen direkt in den Bucket.
  • Mitte: Das Gateway gibt jeden Sendevorgang an den Nachrichtenservice, der Duplikate anhand von client_msg_id verwirft, die nächste Sequenznummer vergibt und die Nachricht an den Speicher anhängt.
  • Rechts: Der Service veröffentlicht ein Event. Fan-out-Worker prüfen Presence und Sessions, liefern über das richtige Gateway aus und schicken Offline-Mitgliedern eine Push-Benachrichtigung.
  • Der Heartbeat vom Gateway zur Presence ist gepunktet, weil er per Timer läuft und nicht pro Nachricht.
  • Der Markdown-Block zeigt den Nachrichtenumschlag, damit sich alle auf die Felder einigen.

Was du beschriften solltest

  • Welche Komponenten Zustand halten. Gateways halten Verbindungen; der Nachrichtenservice hält nichts. Das entscheidet, wie du jede Komponente deployst und skalierst.
  • Die Reihenfolgeregel. „Sequenz pro Unterhaltung, vom Server vergeben“ gehört ins Diagramm.
  • Der Deduplizierungsschlüssel. Nenne client_msg_id am Sendepfad.
  • Online- und Offline-Pfad. Zeichne beide; die meisten Bugs stecken im zweiten.

Der Beitrag zur ereignisgesteuerten Architektur behandelt die Event- und Consumer-Seite ausführlicher, und das Beispiel für ein Sequenzdiagramm ist ein guter Weg, den Austausch aus Senden und Bestätigen Schritt für Schritt zu dokumentieren.

In Archboard nachbauen

  1. Melde dich mit Google oder GitHub an und leg ein Diagramm an.
  2. Füge aus dem C4-Modell-Pack eine Person für die Nutzer hinzu und aus dem Cloud-Pack einen Cloud-Lastverteiler.
  3. Füge aus dem C4-Modell-Pack drei Container-Nodes hinzu: Chat-Gateway, Nachrichtenservice und Fan-out-Worker. Vermerke bei jedem, ob er zustandsbehaftet ist.
  4. Füge aus dem C4-Modell-Pack eine Datenbank für den Nachrichtenspeicher hinzu, aus dem Daten-Pack Redis und Kafka für Presence und Events, aus dem Cloud-Pack einen Objektspeicher für Medien und ein Externes System für APNs und FCM.
  5. Verbinde sie, zeichne Heartbeat und Medien-Upload als gestrichelte oder gepunktete Kanten und füge einen Markdown-Block aus dem Basis-Pack mit dem Nachrichtenumschlag hinzu.

Das Beispiel nutzt 11 Diagramm-Nodes und bleibt damit im Free-Plan.

FAQ

Brauche ich WebSockets für eine Chat-App? Für Echtzeitzustellung ja, oder einen ähnlichen Push-Kanal wie Server-Sent Events plus HTTP zum Senden. Long Polling funktioniert für kleine Apps, kostet aber mehr Anfragen und erzeugt Verzögerung.

Wie halte ich Nachrichten in der richtigen Reihenfolge? Lass den Server beim Speichern eine Sequenznummer pro Unterhaltung vergeben und die Clients danach sortieren. Client-Uhren weichen voneinander ab und taugen nicht zum Sortieren.

Wie vermeide ich doppelte Nachrichten? Der Client erzeugt vor dem Senden eine Nachrichten-ID und verwendet sie bei Wiederholungen erneut. Der Server speichert die ID und ignoriert eine zweite Nachricht mit derselben.

Welche Datenbank sollte Chat-Nachrichten speichern? Eine, die schnell pro Unterhaltung liest und nach Sequenz sortiert, etwa ein Wide-Column-Store, partitioniert nach Unterhaltung und Zeit-Bucket. Für kleinere Apps funktioniert Postgres mit einem Index auf Unterhaltung und Sequenz gut.

Wie werden Nachrichten an Nutzer zugestellt, die offline sind? Sie werden wie jede andere Nachricht gespeichert. Der Fan-out-Schritt sendet eine Push-Benachrichtigung, und wenn sich die App wieder verbindet, fragt sie nach allem nach ihrer letzten Sequenznummer.

Kostenlos starten — ein Seat, keine Kreditkarte.

Ähnliche Beiträge

← Zurück zum Blog

In einer Minute ein Canvas öffnen

Melden Sie sich mit Google oder GitHub an. Kostenlos für einen Sitzplatz. Upgraden Sie, wenn das Team dazukommt.