Docker 기반 스트림 녹화 서비스, 라이브도 아닌데 “녹화중”으로 며칠씩 멈춰있던 문제

증상

셀프호스팅으로 운영 중인 스트림 자동 녹화 서비스(yt-dlp + FastAPI 기반, Docker 컨테이너로 구동)의 UI를 열어보니, 특정 채널 2곳이 실제로는 라이브 방송 중이 아닌데도 “RECORDING” 상태로 표시되고 있었다. 경과 시간(elapsed) 카운터는 한 채널은 약 13.5일, 다른 채널은 약 10.5일째 계속 올라가는 중이었는데, 정작 디스크에는 그 시간 동안 새로 쓰인 녹화 파일이 없었다.

다른 채널들은 정상적으로 녹화 → 종료 → 다음 녹화 순으로 잘 돌아가고 있었기 때문에, 서비스 전체가 죽은 건 아니고 특정 채널만 개별적으로 어딘가 걸려있는(hang) 상태라는 게 짐작됐다.

진단 과정

1. 상태 파일(state.json) 확인

앱이 녹화 세션 이력을 기록하는 state.json을 열어봤다. 최근 항목들을 보니 문제의 두 채널에 대해 status: "completed"로 정상 마감된 세션은 있는데, UI에 떠 있는 “지금 녹화 중”인 세션에 해당하는 항목 자체가 아예 없었다. 즉 디스크에 기록된 이력과 UI의 실시간 상태가 따로 놀고 있었다.

2. 컨테이너 로그에서 채널별 이력 추적

docker logs <container_name> 2>&1 | grep -i "<채널_식별자>"

채널별로 “Recording started” 로그를 시간순으로 쭉 뽑아보니, 두 채널 모두 어느 시점을 마지막으로 그 이후 시작 로그가 완전히 끊겨 있었다. 다른 채널들은 방송이 끝나면 곧바로 다음 시작 로그가 찍히는 반면, 문제의 두 채널만 그 시점 이후로 아무 시도도 없었다.

3. 결정적 증거 — 시간 역산

마지막 “Recording started” 시각부터 진단 시점까지의 경과 시간을 계산해보니, UI에 표시된 elapsed 카운터와 초 단위까지 정확히 일치했다.

마지막 시작 시각 + 경과일수 = 현재 확인 시각

이걸로 확정됐다: 그 시점에 시작된 다운로드 프로세스가 정상 종료도, 에러도 없이 그냥 멈춘 채(hang) 방치되고 있었던 것이다.

4. 소스코드 확인

앱 소스(main.py)를 열어 관련 로직을 찾아보니, 원래 이런 상황을 감지하는 기능이 이미 구현돼 있었다.

async def _poll_size():
    ...
    while True:
        await asyncio.sleep(get_size_poll_interval())
        # 녹화 파일 크기를 주기적으로 체크
        ...
        # 120초(Pi 모드는 240초) 동안 파일 크기가 안 늘어나면 "정체"로 판단
        stall_timeout = 240 if _is_pi() else 120
        if time.time() - last_change > stall_timeout:
            rec["log"].append(f"[StreamRec] WARNING — no data received for {stall_timeout}s...")
            if settings.get("auto_stop_stalled", False) and not rec.get("stopping"):
                rec["log"].append("[StreamRec] Auto-stop-stalled enabled — stopping recording.")
                _stop_rec(rec, force=False)

핵심은 여기였다. settings.auto_stop_stalled라는 설정값이 있고, 이게 켜져 있으면 데이터가 120초 이상 안 들어올 때 자동으로 프로세스를 끊고 재시도 로직을 태우게 되어 있었다. 그런데 이 설정이 기본값 false로, 즉 꺼진 상태였다. 그래서 정체가 감지돼도 로그에 WARNING만 남기고 아무 조치 없이 방치되고 있었던 것.

근본 원인

다운로드 프로세스가 (아마도 네트워크 경로 이슈로) 데이터 수신이 멈췄는데, 이를 감지해 자동으로 정리하는 기능(auto_stop_stalled)이 비활성화돼 있어서 죽지도 살지도 않은 상태로 무기한 방치됨.

해결

  1. stall 자동 종료 기능 활성화
grep auto_stop_stalled state.json
sed -i 's/"auto_stop_stalled": false/"auto_stop_stalled": true/' state.json
  1. 컨테이너 재시작해서 새 설정 반영 및 hang된 상태 초기화
docker restart <container_name>

여기서 얻은 교훈 / 부수적으로 발견한 문제들

  • 모니터링 시스템에 “재시도 카운트/정체 감지”가 있어도, 기본값이 꺼져 있으면 무용지물이다. 배포할 때 이런 안전장치성 옵션들은 꼭 한 번씩 검토해서 켜둘 필요가 있다.
  • 트러블슈팅 도중 앱이 실제로 읽고 쓰는 상태 파일 경로가 환경변수(RECORDINGS_DIR)로 리다이렉트되고 있다는 걸 뒤늦게 발견했다. docker-compose에 남아있던 볼륨 마운트 경로 하나는 사실 앱이 전혀 참조하지 않는 죽은 경로였는데, 처음엔 이걸 실제 데이터 파일로 착각해서 시간을 좀 썼다. compose 파일과 실제 코드의 경로 설정이 항상 일치하는 건 아니라는 점, 헷갈리면 소스에서 직접 grep으로 확인하는 게 가장 빠르다.
  • 파일/디렉토리 소유권이 전부 root였던 것도 이번에 같이 정리했다. 컨테이너를 root로 그냥 띄우기보다는 user: "<UID>:<GID>"를 지정해서 최소 권한으로 돌리는 게, 이후 호스트 쪽에서 sudo 없이 파일을 만지기에도 편하고 보안상으로도 낫다.

정리

항목내용
증상라이브 아닌 채널이 UI상 “녹화중”으로 며칠째 고정
원인다운로드 hang 감지는 있으나 자동 종료 옵션이 꺼져 있었음
해결auto_stop_stalled: true로 변경 + 컨테이너 재시작
근본 원인 (미해결)애초에 왜 데이터 수신이 멈췄는지(네트워크/스트림 쪽)는 별도 조사 필요