마지막 업데이트 2026-09-17
증상 신고는 손범 1건뿐이었으나, 같은 워커에서 밀린 수업 job은 24개다.
15:08:03 테스트 계정 '테스트아리'(d9ed7fb0…)가 수업 세션 열고 방치
→ job AJ_CLYSMmWdT876 (pid 720, activity newpp01_up_wjv1) 생성
→ 워커 AW_eg5RnPhNXC2r 의 슬롯 2개 중 1개를 계속 점유
15:28:04 realtime input audio commit correlation quarantined; reconnecting × 9
→ 고장 신호가 아니다. 20분 주기 커넥션 재활용(DEFAULT_MAX_SESSION_DURATION
= 20분)에 따른 정상 로그로, 모든 job 이 20분마다 찍는다.
→ 좀비 신호는 이 로그가 아니라 "이 로그 뒤로 대화 활동이 없다"는 점이다.
16:00:41 inference is slower than realtime 누적 시작 (livekit.plugins.silero)
17:20:03 이 시점 잡까지는 정상 (경고 0건) ← 촉발 요인 이전
17:32:53 이후 이 워커에 들어온 수업 job 24개 전부 실시간 추종 실패
→ 실효 처리율 1~6% · 최대 적체 5.9~83.6초
→ LLM 텍스트는 늦게라도 도착, TTS 첫 프레임은 세션 종료 전 미도달
→ 아동 화면: "핑퐁이가 말을 안 해요"
18:12:48 방치 페이지 닫힘 → participant disconnect → 좀비 job 종료
→ 이후 신규 job 정상
| 사실 | 근거 |
|---|---|
| 좀비 job 3시간 4분 45초 생존 | CloudWatch /ppi/livekit/prod/agent — received job request 15:08:03 → process exiting 18:12:48. 종료 사유 closing agent session due to participant disconnect |
| 밀린 수업 job 24개가 전부 같은 ECS 태스크 | @logStream agent/ppi-livekit-agent/b9238a5f30f04c3a9207df1601ba88bf 24/24 일치. 이 스트림 = 워커 AW_eg5RnPhNXC2r |
| 실시간 추종 실패 | inference is slower than realtime의 delay 필드(누적 적체 초). 실효 처리율 realtime 대비 1~6% |
| TTS 미도달 | 손범 51회기 게스트 lesson-log — 실패 4룸에서 agent_response_started·agent_transcript_done은 있고 tts_provider_stream_started·tts_node_first_audio_frame·agent_response_completed는 0건 |
| 워커 단위로 갈린다 | 손범 19룸 중 실패 4룸은 전부 이 워커, 정상 15룸은 전부 다른 워커 9대 (우연 확률 0.026%) |
| 시간대 요인 아님 | 박준호 1회기는 같은 창(17:33~17:48)·같은 진행자인데 다른 워커 5대에서 응답 57건 전부 정상 (첫 TTS 3.0~3.6초) |
| 좀비 종료 = 회복 | 18:12:31 마지막 지연 경고 → 18:12:48 process exiting → 이후 정상 |
남은 후보: 태스크 CPU 크레딧 소진 · 좀비 프로세스의 메모리 누적 · 호스트 경합. 확정하려면 해당 ECS 태스크의 17:00~18:30 CPU 할당량과 CPUUtilization·MemoryUtilization이 필요하다.
| 가설 | 기각 근거 |
|---|---|
| 워커 프로세스 열화 (메모리 누수 등 비가역 악화) | 재시작 없이 18:12:48에 자가 회복. 누적성 열화는 저절로 낫지 않음 |
| 호스트 이그레스(DNS·conntrack) 불량 | 지연이 CPU 기아 지표(VAD 추론)에 직접 나타남. 네트워크 구간 단독으로는 VAD 추론 지연을 설명 못 함 |
| Typecast TTS 서비스 장애 | 같은 시각 다른 워커 15룸에서 TTS 정상 |
| httpx 커넥션 풀 오염 | _build_tts()가 세션 구성마다 호출되어 job마다 새 AsyncClient 생성 (agent.py:1156,1328 → 4507) |
Consumer suspected silent 경고 | 전 건이 trigger:"consumer-created" 워밍업 프로브. ai-audio 컨슈머 40개 중 40개에서 뜨고 정상 세션도 동일하게 audioEnergyDelta:0 |
not dispatching agent job since no worker is available 586건 | JT_PUBLISHER/JT_PARTICIPANT 노이즈. ppi-agent는 JT_ROOM으로만 등록 |
job ended JS_FAILED "agent worker left the room" 292건 | 전 세션 정상 종료 패턴. job 프로세스 크래시는 error가 빈 문자열 |
apps/livekit-agent/agent.py:344-373PPI_AGENT_MAX_JOBS_PER_WORKER = 2 # 환경변수 미설정 시 기본값
def _ppi_agent_load(server):
active_jobs = len(server.active_jobs)
return min(active_jobs / PPI_AGENT_MAX_JOBS_PER_WORKER, 1.0) * PPI_AGENT_LOAD_THRESHOLD
server = AgentServer(load_fnc=_ppi_agent_load, load_threshold=0.99, num_idle_processes=2)
load 계산에 job 개수만 들어간다. 좀비 1개가 슬롯을 물고 있어도 load는 1/2 × 0.99 = 0.495라
LiveKit 눈에는 "절반 빈 여유 워커"다. 수업 job이 끝나면 즉시 다시 후보가 되므로 끊임없이 재선택된다
— 손범이 4번이나 이 워커를 다시 만난 이유다. 17:48:48의 failed to assign job to worker: worker not available은
그 순간 2슬롯이 다 찼다는 뜻이며, 이 워커 고유의 신호가 아니다(같은 시간대 다른 워커에도 발생).
apps/livekit-agent/typecast_tts.py:259-282tts_provider_stream_started는 Typecast HTTP 응답이 200 + content-type audio일 때만 발행된다.
실패 룸에서 이 이벤트가 0건이라는 것은 응답 헤더 수신 이전 단계에서 멈췄다는 뜻이다.
실패 시 typecast_tts_synthesis_failed의 error_class·http_status·elapsed_ms로
네트워크/제공자/타임아웃을 가른다(클라이언트 timeout=30.0, typecast_tts.py:88).
delay 필드의 정의 — livekit/plugins/silero/vad.py:452-461inference_duration = time.perf_counter() - start_time
extra_inference_time = max(0.0, extra_inference_time + inference_duration - window_duration)
if inference_duration > SLOW_INFERENCE_THRESHOLD: # 0.2초
logger.warning("inference is slower than realtime", extra={"delay": extra_inference_time})
window_duration은 512샘플 / 16kHz = 32ms. 즉 delay는
"오디오를 실시간보다 늦게 처리한 초과분의 누적합"이고, 경고 건수는
한 윈도 추론이 0.2초를 넘은 횟수다. 이 값은 _reset_state()에서 0으로 초기화되므로
톱니 모양 하강을 곧바로 "회복"으로 읽으면 안 된다.
/ppi/livekit/prod/agent에서
filter @message like /inference is slower than realtime/ 로 전량 수집.
조회 범위는 사건 시각보다 최소 3시간 앞까지 넓힌다 — 좀비 job의 시작점이 창 밖에 있으면 원인 job을 특정할 수 없다.job_id·pid별로 건수·최대 delay·room 집계.
건수가 수백이고 생존이 수십 분이면 상주 job, 건수 수십에 생존 수십 초면 피해 job, 1~2건 0.2~0.8초는 정상 범위.
비교는 반드시 분당 발생률로 정규화한다.received job request 로 각 job의 @logStream을 확인해 동일 ECS 태스크인지 확정.
워커당 job 2개가 한 스트림에 섞이므로 room UUID 또는 job_id 필터가 필수다.agent_response_started > 0 이면서 tts_node_first_audio_frame = 0 인 룸을 찾는다.assigned job to worker로 AJ_* → AW_* 조인.room 필드가 없다. room UUID로만 필터하면 TTS 로그가 통째로 누락된다 — job_id로 함께 걸어야 한다.
| 항목 | 내용 | 우선도 |
|---|---|---|
| job 최대 수명 제한 | 실수업 세션은 1시간을 넘지 않는다. 그 이상 생존한 job은 강제 종료. 15:28에 세션이 이미 비정상이었는데도 2시간 44분 더 살아남은 것이 이번 사건의 출발점이다. | P0 |
| 테스트 계정 격리 | 테스트 job이 운영 워커 풀을 공유하는 구조 자체가 위험. 전용 워커 풀 또는 별도 agent name으로 분리. | P1 |
| 동시 실행 수 재검토 | 좀비 사후에도 동시 2개에서 5.9초 지연이 남는다. MAX_JOBS_PER_WORKER=2가 실제 CPU 여력 대비 과다한지 태스크 메트릭으로 확인. | P1 |
| load에 부하 실측 반영 | 현재 load는 job 개수만 본다. 실시간 추종 실패가 연속 감지되면 load를 올려 신규 job 수락만 중단(self-drain)하고 진행 중 세션은 유지. | P2 |
| 관측 | 워커별 "응답 대비 TTS 첫 프레임 도달률" 대시보드. 현재 이 실패는 어떤 알람에도 걸리지 않는다. | P2 |
tts_node_first_audio_frame 유무와 워커 집중도로 가른다.apps/livekit-agent/agent.py (load_fnc · 세션 구성)apps/livekit-agent/typecast_tts.py (TTS 스트림 시작 조건 · 실패 로깅)/ppi/livekit/prod/agent — 스트림 agent/ppi-livekit-agent/b9238a5f30f04c3a9207df1601ba88bfppi-livekit-20260915_1700-1800.log (assigned job to worker)lesson-log-20260915.jsonl.gz, 박준호 1회기