Titanium: Skalierbarer Backend-Kern für Messenger – Vom Prototyp zur Produktion
Entwickler, die eigene Messenger bauen, greifen oft auf XMPP/ejabberd oder Matrix/Synapse zurück. Diese Plattformen bieten fertige Transportmechanismen, Föderation und E2E-Verschlüsselung. Doch bei der Anpassung an moderne UX stoßen sie schnell an Grenzen.
XMPP mit ejabberd auf Erlang ist kompakt und belastbar, aber:
- XML-Stanzas verursachen Parsing-Overhead.
- MUC-Räume skalieren nicht linear: CPU-Spitzen bei wachsender Teilnehmerzahl.
- Partiell implementierte XEP-Erweiterungen führen zu Race Conditions in der Geschäftslogik.
Matrix ist mächtig, doch die geschichtete Architektur erschwert Änderungen: Synapse auf Python kämpft mit dem GIL bei Echtzeit-Aufgaben.
Hybride Setups – wie ejabberd für Transport + FastAPI für API/OTP/Reaktionen – verursachen Split-Brain-Zustände, XML<->JSON-Konvertierungen und zusätzliche Queues. Die einzige Wahrheit verschwimmt über Komponenten hinweg.
Fazit: Es ist einfacher, ein Backend von Grund auf zu bauen, um volle Kontrolle über Zustand und Performance zu haben.
Titanium v1: FastAPI unter Last
Die erste Titanium-Version auf FastAPI mit WebSocket beherrschte Kernfunktionen: Chats, UINs, Medien, pts-Sync. Der Prototyp hielt 500 RPS in einem Szenario mit 1000 Nutzern, 120 Nachr/s, 40 Medien/min und 7 % Abbrüchen stand.
RPS wurde über vollständige Pfade gemessen: Senden -> Zustellung -> Bestätigung -> Sync. Das umfasste:
- Validierung von Zugriffsrechten.
- pts-Updates (Nachrichtspunkte für Delta-Sync).
- Echtzeit-WebSocket-Pushes.
- Wiederholungen bei instabilen Netzen.
- Update-Logs für Offline-Clients.
Probleme traten bei Skalierung auf:
- GIL bremste WebSocket-Konkurrenz.
- PostgreSQL blockierte bei hoher Konkurrenz in pts-Tabellen.
- Medienspeicher brauchte temporäre Links, keine permanenten.
Titanium-Architektur: Wo die Wahrheit wohnt
Titanium definiert strenge Zustandsverwaltungsregeln:
- Server als Quelle der Wahrheit: Clients nie vertrauen; alle Änderungen via API.
- Pts-Mechanismus: Delta-Sync via Nachrichtspunkte, Telegram-Style.
- Update-Log: Unveränderliches Log für Zustandsreplay.
- Verteilter Zustand: Sharding nach UIN, keine sticky Sessions.
- Medien: Temporäre S3-ähnliche Signaturen, 24h TTL.
Schlüsselkomponenten:
- Gateway: WebSocket + HTTP/2, Rate Limiting, JWT.
- Core: Geschäftslogik, pts, ACLs (nach UIN/Gruppen).
- Storage: PostgreSQL (Zustände) + Redis (Sessions/Queues) + S3 (Medien).
- Workers: Celery/RQ für asynchrone Tasks (Medienverarbeitung, Bots).
Pts-Sync-Beispiel (Pseudocode):
% Client fordert Delta an
handle_get_updates(ReqPts) ->
Updates = db:fetch_updates_after(ReqPts, UserId),
NewPts = lists:max([U#update.pts || U <- Updates]),
{reply, #{updates => Updates, pts => NewPts}}.
Wechsel zu Erlang/OTP: Skalierbarkeit und Robustheit
Python schaffte keine 10k parallele WebSockets. Erlang/OTP löste das:
- Actor-Modell: Jeder UIN bekommt einen Supervisor-Baum mit gen_server.
- VM-Hot-Reload ohne Downtime.
- Eingebaute Mnesia/ETS für latenzarme Speicherung.
- Cowboy für WebSocket-Skalierung auf 1 Mio.+ Verbindungen pro Node.
Benchmarks:
| Szenario | FastAPI (Python) | Titanium (Erlang) |
|----------|-------------------|--------------------|
| 1k Nutzer, 120 Nachr/s | 500 RPS | 5k RPS |
| 10k WS | 20 % CPU | 5 % CPU |
| Medien 100/min | 2s Latenz | 200ms |
Gen_server-Session-Beispiel:
-module(session).
-export([start_link/1, handle_message/2]).
handle_call({send_message, Msg}, _From, State) ->
case validate_rights(Msg) of
ok ->
NewPts = State#state.pts + 1,
save_update(Msg#update{pts=NewPts}),
broadcast_to_peers(Msg),
{reply, ok, State#state{pts=NewPts}};
{error, Reason} -> {reply, {error, Reason}, State}
end.
Gruppenchats und Edge Cases
Gruppen sind ein Biest. MUC durch:
- Geshardete Räume: Leader nach hash(UIN-Liste), Follower replizieren Deltas.
- Presence: ETS für online/offline, pubsub bei Reconnect.
- Schwache Netze: Optimistisches Senden + Server-ACK, Lückenfüllen aus Logs.
Abgehandelte Fälle:
- Duplizierte Sends (Idempotenz nach msg_id).
- Paket-Reihenfolge (Timestamp + Seq).
- Schlaf-/Weck-Tabs (Long-Poll-Fallback).
- Bot-Webhooks mit Retries (Dead-Letter-Queue).
Wichtige Erkenntnisse
- Titanium ist kein MTProto-Klon – ein schlanker Echtzeit-Stack: pts, Logs, Sharding.
- Erlang/OTP liefert 10x RPS bei 1/4 CPU vs. Python.
- Server-Wahrheit verhindert Client-Inkonsistenzen.
- Medien/Bots asynchron ausgelagert; Core fokussiert auf Zustellung.
- Skalierung: 3 Nodes für 50k Nutzer, 1k Nachr/s ohne Föderation.
— Editorial Team
Noch keine Kommentare.