Powrót do strony głównej

Titanium: backend komunikatora na Erlang

Artykuł analizuje ewolucję backendu Titanium dla niestandardowego komunikatora: rezygnacja z XMPP/FastAPI na rzecz Erlang/OTP. Opisano synchronizację pts, sharding, benchmarki do 5k RPS. Dla senior deweloperów.

Jak zbudować skalowalny backend komunikatora na Erlang
Advertisement 728x90

Titanium: serwerowe jądro dla niestandardowego komunikatora — od prototypu do skalowalności

Programiści budujący własne komunikatory często zaczynają od XMPP/ejabberd lub Matrix/Synapse. Te platformy oferują gotowy transport, federację i szyfrowanie E2E. Jednak przy dostosowywaniu do nowoczesnego UX pojawiają się ograniczenia.

XMPP z ejabberd na Erlangu jest kompaktowy i odporny na obciążenia, ale:

  • Stanzas XML dodają narzut na parsowanie.
  • Sale MUC nie skalują się liniowo: wraz z wzrostem uczestników rośnie obciążenie CPU.
  • Rozszerzenia XEP są częściowo zaimplementowane, co powoduje race conditions w logice biznesowej.

Matrix jest potężny, ale warstwowa architektura komplikuje zmiany: Synapse na Pythonie cierpi z powodu GIL przy zadaniach real-time.

Google AdInline article slot

Hybrydowe podejście — ejabberd dla transportu + FastAPI dla API/OTP/reakcji — doprowadziło do split-brain w stanie, transformacjom XML<->JSON i dodatkowym kolejkom. Źródło prawdy rozmyło się między komponentami.

Wniosek: prościej zbudować backend od zera, by mieć pełną kontrolę nad stanem i wydajnością.

Titanium v1: FastAPI pod obciążeniem

Pierwsza wersja Titanium na FastAPI z WebSocketem pokryła podstawowy funkcjonalność: czaty, UIN, media, synchronizacja pts. Prototyp wytrzymywał 500 RPS w scenariuszu z 1000 użytkownikami, 120 msg/s, 40 media/min, 7% rozłączeń.

Google AdInline article slot

RPS mierzono nie po /health, a po pełnej ścieżce: wysłanie -> dostarczenie -> potwierdzenie -> synchronizacja. To obejmowało:

  • Walidację praw dostępu.
  • Aktualizację pts (message points dla delta-synchronizacji).
  • Real-time push przez WebSocket.
  • Retry na słabej sieci.
  • Dziennik aktualizacji dla klientów offline.

Problemy ujawniły się przy wzroście:

  • GIL ograniczał współbieżność w WebSocket.
  • Baza (PostgreSQL) blokowała się przy wysokim konflikcie w tabeli pts.
  • Magazyn mediów wymagał tymczasowych linków, a nie wieczystych.

Architektura Titanium: gdzie mieszka prawda

Titanium definiuje reguły zarządzania stanem:

Google AdInline article slot
  • Serwer — źródło prawdy: klientowi nie ufamy, wszystkie zmiany przez API.
  • Mechanizm pts: delta-synchronizacja po message points, jak w Telegramie.
  • Dziennik aktualizacji: niemutowalny log do replay stanów.
  • Rozproszony stan: sharding po UIN, bez sticky sessions.
  • Media: tymczasowe podpisy magazynu S3-podobnego, TTL 24h.

Kluczowe komponenty:

  • Gateway: WebSocket + HTTP/2, rate limiting, JWT.
  • Core: logika biznesowa, pts, prawa (ACL po UIN/grupach).
  • Storage: PostgreSQL (stany) + Redis (sesje/kolejki) + S3 (media).
  • Workers: Celery/RQ dla async (przetwarzanie mediów, boty).

Przykład synchronizacji pts (pseudokod):

% Klient żąda delty
handle_get_updates(ReqPts) ->
  Updates = db:fetch_updates_after(ReqPts, UserId),
  NewPts = lists:max([U#update.pts || U <- Updates]),
  {reply, #{updates => Updates, pts => NewPts}}.

Przejście na Erlang/OTP: skalowalność i odporność na awarie

Python nie poradził sobie z 10k współbieżnych WebSocketów. Przejście na Erlang/OTP rozwiązało problemy:

  • Model actor: każdy UIN — drzewo supervisor z gen_server.
  • Hot-reload VM bez przestojów.
  • Wbudowane Mnesia/ETS dla low-latency storage.
  • Cowboy dla WebSocket z 1M+ połączeniami na węzeł.

Benchmarki:

| Scenariusz | FastAPI (Python) | Titanium (Erlang) |

|------------|-------------------|--------------------|

| 1k users, 120 msg/s | 500 RPS | 5k RPS |

| 10k WS | 20% CPU | 5% CPU |

| Media 100/min | 2s latency | 200ms |

Implementacja gen_server dla sesji:

-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.

Grupowe czaty i edge cases

Grupy to osobne wyzwanie. Zamiast MUC:

  • Sharded rooms: leader po hash(UIN_list), followers replikują delty.
  • Presence: ETS dla online/offline, pubsub na reconnect.
  • Słaba sieć: optimistic send + ACK serwera, uzupełnianie luk z dziennika.

Lista obsług:

  • Duplikaty wysyłek (idempotencja po msg_id).
  • Reorder pakietów (timestamp + seq).
  • Zakładki sleep/wakeup (fallback long-poll).
  • Webhooki botów z retry (dead letter queue).

Co ważne

  • Titanium to nie klon MTProto, a minimalny zestaw dla real-time: pts, dziennik, sharding.
  • Erlang/OTP zapewnia 10x wzrost RPS przy 1/4 CPU vs Python.
  • Źródło prawdy na serwerze zapobiega niespójnościom po stronie klienta.
  • Media i boty async przez workers, core skupia się na dostarczaniu.
  • Skala: 3 węzły trzymają 50k users, 1k msg/s bez federacji.

— Editorial Team

Advertisement 728x90

Czytaj dalej