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.
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ń.
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:
- 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
Brak komentarzy.