마지막 업데이트 2026-07-22
PPI-879 이후 SFU 소켓 서버는 다중 인스턴스 + Redis(Valkey) SoT 구조다. mediasoup Router는 인스턴스 메모리에만 존재(복구 불가)하므로, 한 room의 모든 peer를 한 인스턴스(owner)에 고정(pin)하고, 메모리는 캐시+owner 표지로만 쓰며 진실은 Redis에 둔다. 이 문서는 그 핵심인 routerManager(657L)와 room-store를 다룬다.
@handover-redis): 이 인프라는 AI 도구로 구현됐고 암묵지가 없다. 의도는 코드·커밋·테스트가 진실이다. 설계 의도 출처: docs/handover/jacob/redis-infra.md (PR #634).| 묻는 것 | 짧은 답 |
|---|---|
| SoT는 어디 | Redis(Valkey). 메모리는 캐시 + owner 표지 |
| room owner는 누구 | router가 살아있는(메모리에 room이 있는) 인스턴스. tryPinRoom으로 고정 |
| 비-owner가 상태 바꿔도 되나 | 메모리 setter는 owner만(가드 id-021). 비-owner는 Redis만 갱신(id-043) |
| cross-instance 전파 | router-sync pub/sub 채널로 owner가 메모리 refresh + 재broadcast (id-041) |
| 저장 순서 | Redis 저장 → broadcast (반대 금지, id-019) |
| 채널 prefix | STAGE/브랜치별 격리 (#640, id-009) |
| routerManager (메모리) | room-store (Redis SoT) | |
|---|---|---|
| 보관 | rooms: Map<roomId, MediasoupRoom> (router 핸들 + guestState/hosts/설정 캐시) | RedisRoomMeta 해시(room:{id}) + owner 키(room:{id}:instance) |
| 성격 | 인스턴스 로컬, 복구 불가. room이 메모리에 있으면 = 이 인스턴스가 owner | 진실(SoT). 모든 인스턴스가 공유 |
| 일관성 | 어긋나면 Redis가 옳다 | cross-instance read의 기준 |
Invariants 출처: redis-infra.md#invariants
workerManager.getWorker()(라운드로빈)로 worker.createRouter({mediaCodecs}).rooms.set 후 awaitRedis(setRoomMeta). 같은 인스턴스 후속 로직(broadcastRoomList)이 즉시 fresh를 보게 하고, cross-instance read는 어차피 Redis adapter로 처리되므로 순서 무관.autoTransitionEnabled=true, realtimeModelVersion=v1.0, guestState=null.JOIN_ROOM(게스트/호스트) 시 tryPinRoom(roomId)을 호출해 owner를 정한다(모니터는 제외). 단일 Lua 스크립트로 완전 원자 처리:
-- KEYS[1]=room:{id}:instance, ARGV: newInstanceId, ttl, keyPrefix, roomId owner = GET roomInstanceKey if owner == false → SET owner=new(EX ttl) + SADD instance:new:rooms → {1,new} if owner == newInstance → EXPIRE 갱신 + SADD(멱등) → {1,new} if owner heartbeat 없음 → SREM old:rooms, SET new, SADD → {1,new} (stale 인계) else → {0, owner} (살아있는 다른 owner → 실패)
heartbeatTtlSeconds * 3 (기본 90s).{0,owner}) connection-handler가 WRONG_INSTANCE + redirectInstance=ownerInstanceId로 응답 → 클라이언트가 owner 인스턴스로 재접속(#2 JOIN_ROOM 참고).모든 상태 setter(setAutoTransitionEnabled, setVadSettings, setNoiseReduction, updateGuestState 등)는 동일한 owner/non-owner 분기를 쓴다.
applyGuestStateUpdates로 merge → room.guestState=merged(메모리 먼저) → Redis await.redisGetRoomMeta로 현재 상태 읽어 merge → Redis write → publishRoomSync. (호스트가 비-owner 인스턴스에 붙은 카드뷰에서 토글한 경우)id-021). 비-owner는 Redis 갱신만(id-043). 저장 끝난 뒤 broadcast(id-019).비-owner가 Redis를 바꾸면 owner의 메모리 캐시는 모른다. 이를 pub/sub로 해소한다.
ROOM_SYNC_CHANNEL = redisKey("router-sync") (STAGE prefix 격리, #640).publishRoomSync는 cross-instance write 경로(비-owner setter, peerCount cross-instance 등)에서만 호출. owner 자기 write는 메모리가 이미 fresh라 불필요.id-041).peerCount++/-- + Redis Lua(INCREMENT/DECREMENT_PEER_COUNT_LUA). 비-owner: Redis Lua만 + publishRoomSync.router 종료 race / pin 폴백 회귀: id-028
인스턴스는 heartbeat를 Redis ZSET에 주기 발행한다. leader로 뽑힌 인스턴스의 reaper가 heartbeat 끊긴 인스턴스의 instance:{id}:rooms / peer 매핑을 원자적으로 정리한다. room pin의 stale takeover(PIN_ROOM_LUA의 heartbeat 체크)와 짝을 이룬다. 상세는 → 로드맵 #16(Redis 인프라/reaper).
id-021). setter 추가 시 owner 분기 필수.id-043).id-041). publishRoomSync 누락 주의.id-006). 새 상태엔 Redis fallback 경로 필수.id-019).id-009). 채널/키 추가 시 key-prefix.ts 정책 준수.id-032).| 파일 | 역할 |
|---|---|
| mediasoup/routerManager.ts | router 생성·소유권·guestState·peerCount·setter·router-sync(657L) |
| redis/room-store.ts | RedisRoomMeta·tryPinRoom(PIN_ROOM_LUA)·peerCount Lua·meta CRUD |
| redis/key-prefix.ts | 채널/키 STAGE prefix 정책(#640) |
| redis/reaper.ts, reaper-scheduler.ts | stale 인스턴스 room/peer 정리(#16) |
| sfu-socket/handlers/connection-handlers.ts | JOIN_ROOM에서 tryPinRoom·redirect |
| docs/handover/jacob/redis-infra.md | 설계 의도·invariants·failure modes 원본 |