마지막 업데이트 2026-07-22
PPI-1106(녹음 post-mix 디스크 폭주)에 이어, socket 서버가 CPU 100%·디스크 풀·OOM으로 무너질 수 있는 로직을 코드 감사로 사전 식별했다.
가장 취약한 곳은 stem ffmpeg 동시 실행 전역 상한 부재(C1),
broadcastRoomList의 O(룸×peer) 재계산 + 네임스페이스 전체 emit 증폭(C2),
hot-path logger.info 폭주(C3),
그리고 Redis peer/room 키 TTL 부재(D2)·인메모리 Router peerCount 드리프트(D3)다.
실측 prod CPU 그래프의 주기적 100% 스파이크는 C2/C3 패턴과 정합적이며, 이를 dev(t4g.small 2vCPU) 부하 테스트로 실증하고 붕괴 지점을 정량 측정한다.
ppi-socket-prod의 CPU 사용률이 16:00~21:30 구간에서 유휴(20%대)와 100% 스파이크를 반복. 짧고 날카로운 스파이크가 특정 이벤트(수업 시작 러시·모니터 대시보드 다수 접속)와 겹치는 패턴으로, 지속 부하보다 증폭성 이벤트 폭주를 시사한다.
↑ 실측 그래프(ppi-socket-prod)의 형태를 도식화. 유휴 대비 급격한 스파이크가 반복.
가드 — 없음 / 부분 / 있음
| ID | 위치 · 로직 | 가드 | 검증 |
|---|---|---|---|
| C1 | stem ffmpeg 동시 실행 무제한 recordingManager.ts:966 — 녹음 입력별 ffmpeg를 룸당 2개(child/ai) spawn. post-mix는 세마포어(2)로 보호되나 stem은 전역 상한 없음 → ffmpeg 수 = 2 × 활성 녹음 룸으로 선형 증가 |
없음 | S3 |
| C2 | broadcastRoomList 증폭 broadcast-utils.ts:48,166,177 — 다수 이벤트 핸들러가 호출. 매번 O(룸×peer) 재계산 + io.emit으로 네임스페이스 전체에 전송. 1 이벤트 → 전체 대시보드 emit |
부분 | S2 S4 |
| C3 | hot-path 로깅 session-handlers.ts:208·219·227·238·246, monitoring-handlers.ts:347·413 — speaking/transcript/transport-state 매 이벤트 logger.info + 객체 직렬화. 고빈도·상한 없음 |
없음 | S2 S4 |
| C4 | 워커 수 = CPU 코어 전부 sfu-config.ts:34 — workerCount = os.cpus().length(dev=2). 다수 룸 몰리면 기저 CPU 높음. MEDIASOUP_WORKER_COUNT로만 조정 |
설계 | S1 |
| C5 | 메트릭 수집 15s 타이머 metrics.ts:243 — collectGauges가 전체 룸 순회 + 전체 워커 getResourceUsage() await. 룸 많으면 15s마다 부하 |
부분 | S1 |
io.emit룸 한정 아님핫패스(speaking/talking)는 의도적으로 broadcast를 호출하지 않지만, auto-response·VAD 설정·모니터링 토글 같은 중빈도 이벤트가 몰리면 재계산 비용 × 전체 클라이언트 수로 CPU가 급증한다.
룸당 child+ai 2개 프로세스가 무제한 누적. dev 2vCPU에서는 소수 룸으로도 CPU가 포화될 수 있다. (post-mix는 세마포어로 2개 제한되지만 stem은 아님)
| ID | 위치 · 로직 | 가드/구멍 | 검증 |
|---|---|---|---|
| D1 | stem .ogg 세션 중 무제한 생성 recordingManager.ts:1048,1089 — producer 교체마다 새 세그먼트 생성 + stemSegmentPaths에 push. 세션 종료 시에만 일괄 삭제 |
세션 중 정리 없음 | S3 |
| D2 | Redis peer/room 키 TTL 부재 peer-store.ts:114-133, room-store.ts:154-182 — 명시적 disconnect + reaper(leader+heartbeat)에만 의존 |
안전망 TTL 없음 | S5 |
| D3 | 인메모리 Router peerCount 드리프트 routerManager.ts:234-258 — peerCount≤0에만 close. decrement 누락 경로(replaced skip 등) 시 Router+worker 영구 잔존, reaper 없음 |
인메모리 backstop 없음 | S5 |
| D4 | captionTurns 무제한 누적 recordingManager.ts:842 — STT 턴마다 전사 텍스트 push. 세션 종료 시에만 GC |
종료 시 해제 | S3 |
| D5 | cleanStaleTmpFiles 정리 구멍 recordingManager.ts:275 — readdir 실패 시 catch{return}로 고아 파일 정리 통째 skip. 좀비 종료는 fire-and-forget(재시도 없음) |
구멍 | 로그 관찰 |
| D6 | startRecording 실패 시 stem 미정리 recordingManager.ts:682-694 — 시작 실패 catch에서 sdp만 unlink, 생성된 stem .ogg는 누락. 2h stale backstop에만 의존 |
2h backstop | S3 |
-t 6h·15분 타임아웃·손상 stem 드롭)로 막혔다.
이번 감사가 겨냥하는 건 그와 다른 축 — 동시성(C1)·증폭(C2)·누적(D1~D4)으로 리소스가 서서히/급격히 고갈되는 경로다.
TranscodeSemaphore(2) + 15분 SIGKILL + -t 6h 상한 + nice -n 19| 시나리오 | 부하 | 겨냥 위험 |
|---|---|---|
| S1 연결/JOIN 스케일 | 게스트 연결 50→100→200→400 (시그널링만) | C4 C5 |
| S2 broadcast+로깅 [핵심] | 모니터 50~100 + 설정 이벤트 초당 10→50→100. 로깅 ON/OFF A·B 비교 | C2 C3 |
| S3 녹음+재접속 폭주 | 실 브라우저 2~5 세션, 새로고침·네트워크 토글로 producer 교체 반복 | C1 D1 D4 D6 |
| S4 복합(실전 근사) | S1+S2+S3 동시 인가, prod 스파이크 패턴 재현 대조 | C1 C2 C3 |
| S5 비정상 종료 | disconnect ack 없이 강제 종료, reaper 회수 관측 | D2 D3 |
/metrics + REST)15초 주기 gauge, 모든 메트릭에 instance_id 라벨. 부하 인가 전 30초 유휴 기준선 포함해 10초 간격 스크레이프.
| 지표 | 용도 |
|---|---|
ppi_socket_ffmpeg_processes | C1 — 녹음 ffmpeg 개수 vs CPU 상관 |
ppi_socket_broadcast_room_list_total · _duration_seconds | C2 — broadcast 호출 빈도·재계산 소요(계측 신규 추가) |
ppi_socket_connected_clients · ppi_socket_rooms_active | S1 — 연결/룸 스케일 |
ppi_mediasoup_{routers,transports,producers,consumers} | D3 — 부하 해제 후에도 안 떨어지면 누수 |
ppi_mediasoup_worker_cpu_user · /workers/stats | C4 — 워커별 CPU |
process_resident_memory_bytes · event loop lag | D4 — RSS 증가, 이벤트 루프 지연 |
/rooms · /debug/redis-state | D2 — 메모리 vs Redis 대조, 키 잔존 확인 |
broadcastRoomList 디바운스/코얼레싱(예: 100ms 윈도 병합), 전체 io.emit → 대시보드 구독 룸 한정logger.info → debug 강등 또는 샘플링t4g.small(2 vCPU / 2GB, arm64), 워커 2개. 디스크 크기 terraform 미지정 → 배포 후 df -h로 실제 확인 필요 terraform/branch-deploy/main.tf:224,280wss://ppi-1106-loadtest-socket-dev.dubuhealth.in/sfu (websocket 전용, 인증 없음)socketio_adapter=redis, room_meta_source=redis .github/workflows/dev.yml:142-144*-socket-dev.dubuhealth.in 한정. prod URL(ppisocketprod)에는 절대 부하를 걸지 않는다.