apps/socket은 Redis에 강제 의존하며 메모리 폴백이 없습니다. REDIS_URL이 없거나 PING이 5초 내 응답하지 않으면 서버가 부팅 실패합니다. Redis 장애 = Socket.io 서버 전체 마비입니다.
apps/socket은 다중 인스턴스 Socket.io 서버이며 Redis를 다음 목적으로 사용합니다.
@socket.io/redis-adapter로 pub/sub3개의 ioredis 인스턴스가 동시 운영됩니다.
| 클라이언트 | 용도 |
|---|---|
main | 모든 일반 읽기/쓰기 (room, peer, settings) |
pub | Socket.io Redis adapter - 발행 |
sub | Socket.io Redis adapter - 구독 |
| 변수 | 기본값 | 설명 | 필수 |
|---|---|---|---|
REDIS_URL | redis://127.0.0.1:6379 | Redis 연결 URL | 필수 |
STAGE | local | 키 prefix 구성 (ppi:socket:{STAGE}:*) | 필수 |
REDIS_TLS | false | TLS 활성화 여부 | |
SOCKETIO_ADAPTER | redis | redis 또는 memory | |
INSTANCE_HEARTBEAT_INTERVAL_MS | 10000 | Heartbeat 갱신 주기 | |
INSTANCE_HEARTBEAT_TTL_SECONDS | 30 | Heartbeat 키 TTL | |
ROOM_META_SOURCE | redis | redis / both / memory | |
REAPER_ENABLED | true | Dead instance 정리 활성화 | |
RECORDING_PENDING_TTL_SECONDS | 3600 | 녹화 복구 메타 TTL |
{
lazyConnect: true,
maxRetriesPerRequest: 3,
enableReadyCheck: true,
enableOfflineQueue: false, // disconnect 중 명령 큐 비활성
tls: REDIS_TLS === "true" ? {} : undefined,
}
REDIS_URL 없으면 → throw (부팅 실패)main 클라이언트 connectpub/sub 클라이언트 connect (adapter 활성화 시)ppi:socket:{STAGE}: 접두사를 가집니다. 예: ppi:socket:prod:room:abc123, ppi:socket:staging:peer:xyz.
여러 Socket.io 인스턴스 간 메시지 브로드캐스트. 내부적으로 {prefix}socket.io#* 채널 사용.
장애 시: 같은 인스턴스 내부 메시지만 동작, 다른 인스턴스로 가는 메시지 손실.
키: {prefix}room:{roomId}
필드: roomId, createdAt, peerCount, guest, hosts (JSON), guestState (JSON), routerInstanceId, autoTransitionEnabled, realtimeModelVersion, vadSettings, externalSttEnabled, autoResponseEnabled, isTest, groupId
TTL: 없음. removeRoom() 호출까지 유지.
키: {prefix}room:{roomId}:instance / TTL 90초
역할. 같은 room의 모든 peer를 동일 instance로 라우팅. Lua 스크립트 PIN_ROOM_LUA로 takeover까지 원자 처리.
키: {prefix}peer:{peerId}
역 인덱스 Set (검색용).
{prefix}socket:{socketId}:peers - Socket ID로 검색{prefix}instance:{instanceId}:peers - Instance로 검색{prefix}room:{roomId}:peers - Room의 모든 peer{prefix}room:{roomId}:hosts - 호스트만{prefix}room:{roomId}:guest - 게스트 (String, 1개)Instance 생존성 추적. Reaper가 이걸 기준으로 dead instance 판정.
{prefix}instance:{instanceId}:heartbeat (String, TTL 30s){prefix}instances:heartbeat (Zset, score=timestamp)갱신 주기: 10초. Stale 판정: 45초 이상 미갱신.
키: {prefix}reaper:leader / TTL 90초
여러 instance가 동시에 reaper를 돌리지 않도록 leader election. SET NX EX로 취득, CAS Lua로 refresh.
주기: 60초. 작업.
키: {prefix}recording:pending:{roomId}
녹화 중 서버 재시작 시 메타데이터 복구. TTL 만료 시 자동 폐기.
키: {prefix}settings:ambientNoiseEnabled
전역 환경 소음 알림 토글. 유일하게 fail-open 패턴(Redis 미연결 시 true 반환).
키: {prefix}pending-disconnect:pending:{key}
게스트 disconnect 후 grace period 동안 재접속 대기. TTL은 GRACE_PERIOD_MS.
모든 키는 ppi:socket:{STAGE}: prefix를 가집니다. 표는 prefix를 생략한 형태로 표기.
| 키 패턴 | 타입 | TTL | 용도 |
|---|---|---|---|
room:{roomId} | Hash | - | Room 메타데이터 |
room:{roomId}:instance | String | 90s | Room owner instance ID |
room:{roomId}:guest | String | - | 게스트 peerId |
room:{roomId}:peers | Set | - | Room의 모든 peer |
room:{roomId}:hosts | Set | - | 호스트 peer |
rooms:active | Set | - | 활성 room ID 목록 |
peer:{peerId} | Hash | - | Peer 정보 |
socket:{socketId}:peers | Set | - | Socket ID 역 인덱스 |
instance:{instanceId}:peers | Set | - | Instance가 가진 peer |
instance:{instanceId}:rooms | Set | - | Instance가 소유한 room |
instance:{instanceId}:heartbeat | String | 30s | Instance 생존 신호 |
instances:heartbeat | Zset | - | 모든 instance heartbeat (score=timestamp) |
monitor:{roomId} | Set | - | Room 모니터링 peer |
group-monitor:{groupId} | Set | - | Group 모니터링 peer |
reaper:leader | String | 90s | Reaper leader instance ID |
pending-disconnect:pending:{key} | String | grace | 대기 중 disconnect |
recording:pending:{roomId} | String | 3600s | 녹화 메타 복구 |
settings:ambientNoiseEnabled | String | - | 환경 소음 토글 |
socket.io#... | Pub/Sub | - | Socket.io adapter 채널 |
| 메트릭 | 설명 | 알람 기준 예시 |
|---|---|---|
ppi_socket_redis_adapter_connected |
pub/sub 연결 상태 (1=연결, 0=장애) | 5분 이상 0 → 즉시 알람 |
ppi_socket_redis_op_duration_seconds |
Redis 명령 latency (operation별) | p99 > 100ms → 경고 |
ppi_socket_instance_reaper_runs_total |
Reaper 실행 횟수 | 10분 이상 증가 없음 → 경고 |
ppi_socket_instance_reaper_killed_rooms_total |
Reaper가 정리한 room 수 | 급증 시 false positive 의심 |
ppi_socket_room_pin_redirect_total |
JOIN_ROOM redirect 횟수 | 비정상 급증 → 라우팅 이슈 |
ppi_socket_room_meta_mismatch_total |
메모리 vs Redis 불일치 (both 모드) | 지속 증가 → 동기화 버그 |
/health에서 pingRedis() 호출. PONG 응답 시 healthy.
/app/main/debug/redis에서 실시간 확인 가능.
Redis 완전 장애 시 영향. 대부분 FATAL입니다.
| 기능 | 영향 | 설명 |
|---|---|---|
| 부팅 | FATAL | PING 5초 타임아웃 → 부팅 실패. 컨테이너 재시작 루프 |
| Room 메타 조회 | FATAL | getAllActiveRooms() → null. 방 목록 표시 안 됨 |
| Peer 추적 | FATAL | upsertPeer/getPeer 실패 → peer 동기화 깨짐 |
| 다중 인스턴스 라우팅 | FATAL | tryPinRoom() 실패 → redirect 무한 루프 위험 |
| Socket.io 브로드캐스트 | Degraded | 같은 instance 내부만 동작. 크로스 인스턴스 메시지 손실 |
| Reaper (정리) | Degraded | 죽은 instance 자원 정리 불가 → 고아 peer/room 축적 |
| 환경 소음 토글 | OK | 유일하게 fail-open (기본값 true 반환) |
로그에 REDIS_URL is required 또는 Redis PING timeout. 컨테이너 재시작 반복.
echo $REDIS_URL && echo $STAGEredis-cli -u "$REDIS_URL" PINGnc -zv <host> <port>openssl s_client -connect <host>:<port>다른 서비스(ppi-api, ppi-batch) 키가 섞이거나, dev 환경 키가 prod에 보임.
STAGE 변수 잘못 설정 (prod vs staging 혼동), 또는 외부 도구가 잘못된 prefix로 작성.
redis-cli -u "$REDIS_URL" KEYS "ppi:socket:*" | awk -F: '{print $1":"$2":"$3}' | sort -u
# 잘못된 prefix만 삭제 (예시)
redis-cli -u "$REDIS_URL" --scan --pattern 'ppi:socket:wrong-stage:*' | \
xargs -L 100 redis-cli -u "$REDIS_URL" DEL
heartbeat failed 로그 반복 + 정상 instance가 reap됨.
# 자신의 heartbeat 확인
redis-cli -u "$REDIS_URL" GET "ppi:socket:prod:instance:$(hostname):heartbeat"
redis-cli -u "$REDIS_URL" TTL "ppi:socket:prod:instance:$(hostname):heartbeat"
# 전체 instance heartbeat
redis-cli -u "$REDIS_URL" ZRANGEBYSCORE "ppi:socket:prod:instances:heartbeat" -inf +inf WITHSCORES
time redis-cli PINGINSTANCE_HEARTBEAT_TTL_SECONDS=60으로 상향JOIN_ROOM 시 WRONG_INSTANCE 응답 반복. 클라이언트가 무한 redirect.
# Redirect 메트릭 급증 확인
curl http://localhost:3001/metrics | grep ppi_socket_room_pin_redirect
# 해당 room owner 확인
redis-cli -u "$REDIS_URL" GET "ppi:socket:prod:room:{roomId}:instance"
# Owner instance의 heartbeat 확인
redis-cli -u "$REDIS_URL" GET "ppi:socket:prod:instance:{ownerId}:heartbeat"
# 없으면 stale → reaper가 정리할 때까지 대기 (60~120초)
# 강제로 pin 해제 (해당 room의 모든 peer가 끊김)
redis-cli -u "$REDIS_URL" DEL "ppi:socket:prod:room:{roomId}:instance"
Web admin의 /app/main/debug/redis에서 "필드 불일치" 표시.
echo $ROOM_META_SOURCE # redis / both / memory
# 특정 room 직접 비교
redis-cli -u "$REDIS_URL" HGETALL "ppi:socket:prod:room:{roomId}"
redis로 전환 (Redis가 SoT)서버 재시작 후 진행 중이던 녹화 세션의 green dot 사라짐.
redis-cli -u "$REDIS_URL" GET "ppi:socket:prod:recording:pending:{roomId}"
redis-cli -u "$REDIS_URL" TTL "ppi:socket:prod:recording:pending:{roomId}"
다운타임이 1시간 이상일 가능성 → RECORDING_PENDING_TTL_SECONDS 연장 (예: 7200).
redis-cli -u "$REDIS_URL" INFO memory | grep used_memory_human
redis-cli -u "$REDIS_URL" --bigkeys
redis-cli -u "$REDIS_URL" --memkeys
redis-cli -u "$REDIS_URL" --scan --pattern 'ppi:socket:prod:room:*' | \
xargs -L 1 redis-cli -u "$REDIS_URL" MEMORY USAGE
고아 키 정리는 Reaper의 책임이지만 긴급 시 수동 정리 가능. removeRoom() 로직과 동일한 패턴 사용 필요.
Reaper leader lease lost 로그 반복.
redis-cli -u "$REDIS_URL" GET "ppi:socket:prod:reaper:leader"
redis-cli -u "$REDIS_URL" TTL "ppi:socket:prod:reaper:leader"
redis-cli DEL "ppi:socket:prod:reaper:leader"한 instance 내 클라이언트끼리만 메시지 전달, 다른 instance로 안 감.
# Adapter 연결 상태
curl http://localhost:3001/metrics | grep ppi_socket_redis_adapter_connected
# pub/sub 채널 활성 확인
redis-cli -u "$REDIS_URL" PUBSUB CHANNELS '*socket.io*'
SOCKETIO_ADAPTER=redis 확인 (memory로 잘못 설정됐을 수 있음)redis-cli -u "$REDIS_URL" PING → 응답 없음redis-cli --latency -u "$REDIS_URL"로 지연 측정SLOWLOG GET 20으로 느린 쿼리 확인CLIENT LIST로 비정상 클라이언트 탐색--bigkeys로 거대 키 점검/app/main/debug/redis에서 불일치 식별DEL room:{id}* peer:{id}*금지: FLUSHALL 절대 사용 금지 (다른 서비스 키까지 삭제).
# prefix 한정 삭제 (안전)
redis-cli -u "$REDIS_URL" --scan --pattern 'ppi:socket:prod:*' | \
xargs -L 100 redis-cli -u "$REDIS_URL" DEL
삭제 후 모든 Socket 서버 재시작 필요 (인메모리 캐시와 Redis 정합 회복).
# 연결
redis-cli -u "$REDIS_URL" PING
# 서버 정보
redis-cli -u "$REDIS_URL" INFO server
redis-cli -u "$REDIS_URL" INFO memory
redis-cli -u "$REDIS_URL" INFO clients
# 지연 측정
redis-cli -u "$REDIS_URL" --latency
redis-cli -u "$REDIS_URL" --latency-history -i 1
# 느린 쿼리
redis-cli -u "$REDIS_URL" SLOWLOG GET 20
redis-cli -u "$REDIS_URL" SLOWLOG RESET
# 활성 room 목록
redis-cli -u "$REDIS_URL" SMEMBERS "ppi:socket:prod:rooms:active"
# 특정 room 메타
redis-cli -u "$REDIS_URL" HGETALL "ppi:socket:prod:room:{roomId}"
# Instance heartbeat 전체 (score=timestamp)
redis-cli -u "$REDIS_URL" ZRANGEBYSCORE "ppi:socket:prod:instances:heartbeat" -inf +inf WITHSCORES
# 패턴 스캔 (KEYS는 절대 사용 금지, prod에서 차단됨)
redis-cli -u "$REDIS_URL" --scan --pattern 'ppi:socket:prod:room:*'
# 실시간 명령 모니터 (prod에서 사용 주의)
redis-cli -u "$REDIS_URL" MONITOR
# 활성 클라이언트
redis-cli -u "$REDIS_URL" CLIENT LIST
# 키 공간 통계
redis-cli -u "$REDIS_URL" INFO keyspace
# 특정 room 강제 정리
redis-cli -u "$REDIS_URL" DEL \
"ppi:socket:prod:room:{roomId}" \
"ppi:socket:prod:room:{roomId}:instance" \
"ppi:socket:prod:room:{roomId}:peers" \
"ppi:socket:prod:room:{roomId}:hosts" \
"ppi:socket:prod:room:{roomId}:guest"
redis-cli -u "$REDIS_URL" SREM "ppi:socket:prod:rooms:active" "{roomId}"
# Reaper leader 강제 해제
redis-cli -u "$REDIS_URL" DEL "ppi:socket:prod:reaper:leader"
KEYS * - O(N) 차단. SCAN 사용.FLUSHALL, FLUSHDB - 다른 서비스 키까지 삭제.DEBUG SLEEP - 운영 절대 금지.CONFIG SET maxmemory - 인프라 팀 외 금지.