업로드한 영상을 ffmpeg으로 H.264로 변환해 저장하는 기능이 있었다. 업로드 요청 안에서 변환까지 끝내고 응답을 주는 구조였다.
Table of contents
Open Table of contents
목표 — 응답 시간을 업로드 크기와 끊는다
파일이 커지면 응답이 몇 분씩 걸렸다. Nginx proxy_read_timeout 60초에 걸려 504로 끊기는 경우가 늘고 있었다.
응답 시간이 파일 크기에 비례하지 않게 만드는 것이 목표였다. ffmpeg 변환 자체를 빠르게 하는 것은 범위가 아니었다.
제약 — 운영 중이고 실패가 눈에 보여야 한다
이미 운영에서 쓰고 있는 기능이라 한 번에 전부 바꿀 수는 없는 상황이었다. ActiveMQ로 비동기로 돌리면 실패가 사용자에게 안 보이게 될 위험도 있었다.
선택지와 고른 것의 대가
방법이 둘이었다. 요청 안에서 하되 타임아웃을 늘리거나, 큐로 분리해 뒤에서 처리하는 것이다.
타임아웃을 늘리는 쪽은 proxy_read_timeout과 PHP max_execution_time만 올리면 된다. 대신 프록시와 서버 설정을 함께 늘려야 하고, 커넥션이 오래 물려 동시 처리량이 떨어진다.
ActiveMQ 큐로 분리하는 쪽은 업로드 응답이 파일 크기와 무관하게 즉시 돌아온다. 대신 MariaDB에 상태를 관리할 자리가 새로 생기고 실패를 알리는 경로를 따로 만들어야 한다.
ActiveMQ로 분리하는 쪽을 골랐다. 커넥션이 물리는 것이 다른 요청까지 느리게 만들고 있어서, 상태 관리가 늘어나는 대가를 받아들였다.
중간 상태와 실패 분류
처음에는 상태를 완료와 실패 둘로만 뒀는데, 그러자 아직 변환 중인 것이 화면에서 실패로 보였다.
상태를 pending·processing·done·failed 넷으로 나눠 MariaDB에 뒀다. 어느 단계에서 멈췄는지가 상태만 봐도 갈린다.
processing인 채로 30분 넘게 남아 있는 것을 찾는 쿼리도 함께 뒀다. 워커가 죽으면 그 상태로 그대로 남는다.
일시적 실패와 영구적 실패
failed 하나로 두니 재시도할 것과 소용없는 것이 섞였다. 디스크가 찬 것은 다시 하면 되고 코덱이 없는 파일은 몇 번을 해도 같다.
ffmpeg 종료 코드와 stderr를 보고 갈랐다. No space left on device는 재시도하고 Unknown decoder는 바로 영구 실패로 넘긴다. 일시적인 것만 자동 재시도하고 영구적인 것은 바로 사용자에게 알린다.
재시도 횟수에 상한을 뒀다. 상한을 넘으면 영구 실패로 옮기고 목록에 남긴다.
되돌릴 조건과 원본 보관
ActiveMQ 큐 적체가 30분을 넘으면 이 구조를 다시 보기로 정했다. 워커를 늘려도 안 되면 분리한 것이 답이 아니라는 뜻이다.
원본 파일은 변환에 성공한 뒤에도 S3 버킷에 30일 보관하도록 했다. 변환 결과가 잘못됐을 때 다시 할 근거가 원본뿐이다.
updated_at을 상태와 함께 남겼다. 그것이 없으면 processing인 것과 멈춘 것을 구분할 수 없다.
정리
- 요청 안에서 변환하면 응답 시간이 파일 크기에 비례한다
- 타임아웃을 늘리는 방법과 큐로 분리하는 방법이 있었다
- 타임아웃은 코드 변경이 적은 대신 커넥션이 오래 물린다
- 커넥션이 다른 요청까지 느리게 해서 분리 쪽을 골랐다
- 대가는 상태 관리 자리가 새로 생기고 알림 경로를 따로 만드는 것이다
- 중간 상태가 없으면 처리 중인 것이 실패로 읽힌다
- 실패를 일시적·영구적으로 갈라야 재시도가 의미를 갖는다
- 큐 적체가 30분을 넘으면 이 구조를 다시 본다