Retour à l'accueil

Titanium : backend messagerie sur Erlang

L'article dissèque l'évolution du backend Titanium pour une messagerie personnalisée : abandon de XMPP/FastAPI au profit d'Erlang/OTP. Décrit la synchronisation pts, le sharding, des benchmarks jusqu'à 5k RPS. Pour développeurs seniors.

Comment construire un backend messagerie scalable sur Erlang
Advertisement 728x90

Titanium : Cœur backend scalable pour messageries personnalisées — Du prototype à la production

Les développeurs qui construisent des messageries personnalisées commencent souvent par XMPP/ejabberd ou Matrix/Synapse. Ces plateformes offrent un transport prêt à l'emploi, la fédération et le chiffrement de bout en bout. Mais l'adaptation à une expérience utilisateur moderne se heurte à des obstacles.

XMPP avec ejabberd sur Erlang est compact et résistant à la charge, mais :

  • Les stanzas XML ajoutent une surcharge de parsing.
  • Les salles MUC ne scalent pas linéairement : les pics CPU explosent avec le nombre de participants.
  • Les extensions partielles XEP entraînent des conditions de course dans la logique métier.

Matrix est puissant, mais son architecture en couches rend les modifications complexes : Synapse sur Python peine avec le GIL pour les tâches temps réel.

Google AdInline article slot

Les configurations hybrides — comme ejabberd pour le transport + FastAPI pour l'API/OTP/réactions — provoquent un état scindé, des conversions XML<->JSON et des files d'attente supplémentaires. La source unique de vérité se brouille entre les composants.

Leçon clé : Il est plus simple de construire un backend from scratch pour un contrôle total sur l'état et les performances.

Titanium v1 : FastAPI sous charge

La première version de Titanium sur FastAPI avec WebSocket gérait les fonctionnalités de base : chats, UIN, médias, pts-sync. Le prototype tenait 500 RPS dans un scénario de 1000 utilisateurs avec 120 msg/s, 40 médias/min et 7 % de déconnexions.

Google AdInline article slot

Les RPS étaient mesurés sur des chemins complets : envoi -> livraison -> accusé -> sync. Cela couvrait :

  • Validation des droits d'accès.
  • Mises à jour pts (points de message pour delta-sync).
  • Pousses WebSocket temps réel.
  • Retentatives sur réseaux instables.
  • Journaux de mises à jour pour clients hors ligne.

Des problèmes sont apparus à grande échelle :

  • Le GIL limitait la concurrence WebSocket.
  • PostgreSQL se bloquait sur haute contention dans les tables pts.
  • Le stockage média nécessitait des liens temporaires, pas permanents.

Architecture Titanium : Où vit la vérité

Titanium définit des règles strictes de gestion d'état :

Google AdInline article slot
  • Serveur comme source de vérité : Ne jamais faire confiance aux clients ; tous les changements via API.
  • Mécanisme pts : Delta-sync via points de message, style Telegram.
  • Journal de mises à jour : Log immuable pour relecture d'état.
  • État distribué : Sharding par UIN, sans sessions sticky.
  • Médias : Signatures temporaires style S3, TTL 24h.

Composants clés :

  • Gateway : WebSocket + HTTP/2, limitation de débit, JWT.
  • Core : Logique métier, pts, ACL (par UIN/groupes).
  • Stockage : PostgreSQL (états) + Redis (sessions/files) + S3 (médias).
  • Workers : Celery/RQ pour tâches asynchrones (traitement média, bots).

Exemple pts-sync (pseudocode) :

% Le client demande un delta
handle_get_updates(ReqPts) ->
  Updates = db:fetch_updates_after(ReqPts, UserId),
  NewPts = lists:max([U#update.pts || U <- Updates]),
  {reply, #{updates => Updates, pts => NewPts}}.

Passage à Erlang/OTP : Scalabilité et résilience

Python ne gérait pas 10k WebSockets concurrents. Erlang/OTP a résolu ça :

  • Modèle actor : Chaque UIN obtient un arbre de superviseurs avec gen_server.
  • Rechargement à chaud de la VM sans downtime.
  • Mnesia/ETS natifs pour stockage basse latence.
  • Cowboy pour scaler WebSocket à 1M+ connexions par nœud.

Benchmarks :

| Scénario | FastAPI (Python) | Titanium (Erlang) |

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

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

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

| Médias 100/min | 2s latence | 200ms |

Exemple session gen_server :

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

Chats de groupe et cas limites

Les groupes sont une bête noire. Abandon des MUC pour :

  • Salles shardées : Leader par hash(UIN_list), followers répliquent les deltas.
  • Présence : ETS pour en ligne/hors ligne, pubsub au reconnexion.
  • Réseaux dégradés : Envoi optimiste + ACK serveur, comblement des trous depuis les logs.

Cas gérés :

  • Envois dupliqués (idempotence par msg_id).
  • Réordonnancement de paquets (timestamp + seq).
  • Onglets sleep/wakeup (fallback long-poll).
  • Webhooks bots avec retries (dead letter queue).

Leçons clés

  • Titanium n'est pas un clone MTProto — c'est une pile temps réel légère : pts, logs, sharding.
  • Erlang/OTP offre 10x RPS à 1/4 CPU vs Python.
  • La vérité serveur prévient les incohérences client.
  • Médias/bots déchargés asynchrones ; core focalisé sur la livraison.
  • Scale : 3 nœuds gèrent 50k users, 1k msg/s sans fédération.

— Editorial Team

Advertisement 728x90

Lire ensuite