증상
셀프호스팅으로 운영 중인 스트림 자동 녹화 서비스(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)이 비활성화돼 있어서 죽지도 살지도 않은 상태로 무기한 방치됨.
해결
- stall 자동 종료 기능 활성화
grep auto_stop_stalled state.json
sed -i 's/"auto_stop_stalled": false/"auto_stop_stalled": true/' state.json
- 컨테이너 재시작해서 새 설정 반영 및 hang된 상태 초기화
docker restart <container_name>
여기서 얻은 교훈 / 부수적으로 발견한 문제들
- 모니터링 시스템에 “재시도 카운트/정체 감지”가 있어도, 기본값이 꺼져 있으면 무용지물이다. 배포할 때 이런 안전장치성 옵션들은 꼭 한 번씩 검토해서 켜둘 필요가 있다.
- 트러블슈팅 도중 앱이 실제로 읽고 쓰는 상태 파일 경로가 환경변수(
RECORDINGS_DIR)로 리다이렉트되고 있다는 걸 뒤늦게 발견했다. docker-compose에 남아있던 볼륨 마운트 경로 하나는 사실 앱이 전혀 참조하지 않는 죽은 경로였는데, 처음엔 이걸 실제 데이터 파일로 착각해서 시간을 좀 썼다. compose 파일과 실제 코드의 경로 설정이 항상 일치하는 건 아니라는 점, 헷갈리면 소스에서 직접grep으로 확인하는 게 가장 빠르다. - 파일/디렉토리 소유권이 전부 root였던 것도 이번에 같이 정리했다. 컨테이너를 root로 그냥 띄우기보다는
user: "<UID>:<GID>"를 지정해서 최소 권한으로 돌리는 게, 이후 호스트 쪽에서 sudo 없이 파일을 만지기에도 편하고 보안상으로도 낫다.
정리
| 항목 | 내용 |
|---|---|
| 증상 | 라이브 아닌 채널이 UI상 “녹화중”으로 며칠째 고정 |
| 원인 | 다운로드 hang 감지는 있으나 자동 종료 옵션이 꺼져 있었음 |
| 해결 | auto_stop_stalled: true로 변경 + 컨테이너 재시작 |
| 근본 원인 (미해결) | 애초에 왜 데이터 수신이 멈췄는지(네트워크/스트림 쪽)는 별도 조사 필요 |