실시간 제품을 만들면 처음엔 영상이 붙는 순간에 환호한다. 그다음 주부터는 NAT 뒤의 사용자, 회사 방화벽, 모바일 전환, 재연결 폭풍을 상대한다. PonsLink의 시그널링·TURN·SFU 구성은 그 실패를 제품 상태로 다루기 위한 결과다.
시그널링 경로
클라이언트는 nginx의 `/socket.io/` 로케이션을 통해 API 컨테이너에 WebSocket 업그레이드로 붙는다. nginx는 `Upgrade`/`Connection` 헤더를 넘기고, `proxy_read_timeout`을 3600초로 둔다. 버퍼링은 끈다.
API 쪽 모듈:
– `signaling/broker.ts` — 룸/세션 이벤트 라우팅
– `signaling/redisAdapter.ts` — 인스턴스 간 pub/sub
– `signaling/replayStore.ts` — 재연결 시 누락 이벤트 재생
– `signaling/waitingAlert.ts` — 호스트 수락 전 대기 상태 전파
Redis가 빠지면 수평 확장이 아니라 상태 단절이 된다. 어댑터를 시그널링의 일부로 취급해야 한다.
TURN 자격증명
`turn/turnCredentials.ts`가 HMAC 기반 단기 자격증명을 발급한다. 실측 env:
– `TURN_SERVER_URLS=turn:turn.ponslink.com:3478?transport=udp, …tcp, turn:…:5349?transport=tcp`
– `TURN_CREDENTIAL_TTL_SECONDS=600`
– `TURN_REALM=ponslink.com`
장기 공유 비밀번호를 클라에 심지 않는다. 600초면 세션 시작에는 충분하고, 유출 시 피해 창도 짧다. coturn은 docker 컨테이너(`docker-coturn-1`)로 돌린다.
SFU와 P2P의 경계
기본 미디어 런타임은 `MEDIA_RUNTIME_PROVIDER=pion-p2p`다. 1:1·소규모는 메시 P2P가 싸고 단순하다. 다자간·대역 비대칭이 커지면 SFU(`wss://ponslink.com/sfu/ws`, RTC 40000–40100, announced IP)로 올린다. `P2P_FORCE_TURN=false` — TURN은 폴백이지 기본 경로가 아니다. TURN 중계 비용과 지연을 기본값으로 지불하고 싶지 않았다.
룸 안 도구들도 같은 파이프를 탄다
화이트보드 이벤트, 폰캐스트 플레이리스트, 파일 전송 시그널, 자막 동기화는 각자 REST + 실시간 채널을 섞어 쓴다. 공통 규칙은 하나다.
1. **권한 확인은 API**에서 한다 (session-tokens, session-access).
2. **바이트 이동은 가능한 한 피어/오브젝트 스토리지**로 보낸다.
3. **상태 복구는 replay**로 가능하게 남긴다.
폰캐스트는 로컬 파일을 클라우드에 올리지 않고 미리보기·공유하는 면이다, 수신 오류는 사용자가 복구할 수 있는 메시지로 끝나야 한다. "영원히 로딩"은 버그가 아니라 설계 실패다.
운영 지표
TURN 면에는 `/turn/health`, `/turn/metrics`, `/turn/analytics/usage`, `/turn/security/events`가 있다. 시그널링 성공률, ICE 상태, TURN 사용 비율을 제품 지표로 보지 않으면 장애를 "네트워크 탓"으로만 넘기게 된다.
실시간 인프라는 마법이 아니다. 짧은 자격증명, 재연결 재생, 폴백 경로, 관측 가능한 상태 네 가지를 빼먹으면 데모 다음 주에 무너진다.
댓글