Titanium: 커스텀 메신저를 위한 확장형 백엔드 코어 — 프로토타입에서 프로덕션까지
커스텀 메신저를 개발하는 개발자들은 보통 XMPP/ejabberd나 Matrix/Synapse부터 시작합니다. 이 플랫폼들은 즉시 사용 가능한 전송 계층, 페더레이션, E2E 암호화를 제공하죠. 하지만 현대적인 UX에 맞게 커스터마이징하려면 벽에 부딪히게 됩니다.
Erlang 기반 ejabberd와 함께 사용하는 XMPP는 컴팩트하고 부하에 강하지만:
- XML 스탠자(stanza)로 인해 파싱 오버헤드가 발생합니다.
- MUC 룸은 참여자 증가에 따라 선형적으로 확장되지 않아 CPU가 급증합니다.
- 부분적인 XEP 확장으로 비즈니스 로직에서 레이스 컨디션이 발생합니다.
Matrix는 강력하지만 계층화된 아키텍처 때문에 변경이 까다롭습니다. Python 기반 Synapse는 실시간 작업에서 GIL(Global Interpreter Lock)로 고전하죠.
하이브리드 설정—예를 들어 전송용 ejabberd + API/OTP/반응용 FastAPI—은 상태 분산(split-brain), XML↔JSON 변환, 추가 큐를 초래합니다. 단일 진실 원천(single source of truth)이 컴포넌트 간에 모호해집니다.
핵심 요약: 상태와 성능을 완벽히 제어하려면 처음부터 백엔드를 구축하는 게 더 간단합니다.
Titanium v1: FastAPI 부하 테스트
첫 번째 Titanium 버전은 WebSocket을 지원하는 FastAPI로 구현되었으며, 채팅, UIN, 미디어, pts-동기화 등의 핵심 기능을 담당했습니다. 프로토타입은 1000명 사용자 시나리오에서 120 msg/s, 40 media/min, 7% 연결 끊김 속도에서 500 RPS를 유지했습니다.
RPS는 전체 경로에서 측정: 송신 → 전달 → ACK → 동기화. 여기 포함:
- 접근 권한 검증.
- pts 업데이트 (델타 동기화를 위한 메시지 포인트).
- 실시간 WebSocket 푸시.
- 불안정한 네트워크에서의 재시도.
- 오프라인 클라이언트용 업데이트 로그.
스케일업에서 문제 발생:
- GIL로 WebSocket 동시성 제한.
- pts 테이블 고부하 시 PostgreSQL 락업.
- 미디어 저장소에 영구 링크 대신 임시 링크 필요.
Titanium 아키텍처: 진실이 살아있는 곳
Titanium은 엄격한 상태 관리 규칙을 정의합니다:
- 서버가 진실의 원천: 클라이언트를 절대 신뢰하지 말고, 모든 변경은 API를 통해.
- Pts 메커니즘: 메시지 포인트를 통한 델타 동기화, Telegram 스타일.
- 업데이트 로그: 상태 재생을 위한 불변 로그.
- 분산 상태: UIN별 샤딩, sticky 세션 없음.
- 미디어: S3 스타일 임시 서명, 24시간 TTL.
주요 컴포넌트:
- Gateway: WebSocket + HTTP/2, 속도 제한, JWT.
- Core: 비즈니스 로직, pts, ACL (UIN/그룹별).
- Storage: PostgreSQL (상태) + Redis (세션/큐) + S3 (미디어).
- Workers: Celery/RQ로 비동기 작업 (미디어 처리, 봇).
Pts-동기화 예시 (의사코드):
% 클라이언트가 델타 요청
handle_get_updates(ReqPts) ->
Updates = db:fetch_updates_after(ReqPts, UserId),
NewPts = lists:max([U#update.pts || U <- Updates]),
{reply, #{updates => Updates, pts => NewPts}}.
Erlang/OTP로 전환: 확장성과 탄력성
Python은 10k 동시 WebSocket을 감당하지 못했습니다. Erlang/OTP가 해결:
- 액터 모델: 각 UIN에 gen_server를 포함한 슈퍼바이저 트리.
- 제로 다운타임 VM 핫-리로드.
- 저지연 저장소로 내장 Mnesia/ETS.
- 노드당 100만+ 연결 WebSocket 확장을 위한 Cowboy.
벤치마크:
| 시나리오 | FastAPI (Python) | Titanium (Erlang) |
|----------|-------------------|--------------------|
| 1k 사용자, 120 msg/s | 500 RPS | 5k RPS |
| 10k WS | 20% CPU | 5% CPU |
| 미디어 100/min | 2s 지연 | 200ms |
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.
그룹 채팅과 엣지 케이스
그룹 채팅은 난제입니다. MUC를 버리고:
- 샤딩된 룸: UIN 리스트 해시로 리더 선정, 팔로워가 델타 복제.
- Presence: 온라인/오프라인 ETS, 재연결 시 pubsub.
- 악조건 네트워크: 낙관적 송신 + 서버 ACK, 로그로 갭 채움.
처리된 케이스:
- 중복 송신 (msg_id로 멱등성).
- 패킷 순서 변경 (타임스탬프 + 시퀀스).
- 슬립/웨이크업 탭 (롱폴링 폴백).
- 재시도 봇 웹훅 (데드 레터 큐).
핵심 요약
- Titanium은 MTProto 클론이 아닙니다—pts, 로그, 샤딩 중심의 가벼운 실시간 스택입니다.
- Erlang/OTP은 Python 대비 10배 RPS, CPU 1/4로 제공.
- 서버 진실 원칙으로 클라이언트 불일치 방지.
- 미디어/봇은 비동기 오프로드; 코어는 전달에 집중.
- 스케일: 3노드로 50k 사용자, 1k msg/s (페더레이션 없이).
— Editorial Team
아직 댓글이 없습니다.