개요
gif lower는 사용자가 업로드한 GIF 파일을 FFmpeg로 변환해 용량을 줄인 결과물을 제공하는 서비스입니다. 변환 기능 자체는 정상적으로 동작했지만, 여러 요청이 동시에 들어오는 상황에서는 FFmpeg 프로세스가 한꺼번에 실행될 수 있는 구조였습니다.
문제 상황
각 변환 요청은 CPU를 많이 사용하는 FFmpeg 작업을 생성합니다. 요청 하나만 처리할 때는 문제가 없었지만, 여러 사용자의 요청이 겹치면 실행 중인 프로세스가 증가하고 API 서버까지 영향을 받을 가능성이 있었습니다.
영향 범위
변환 자체의 실패보다 더 큰 위험은 같은 Node.js 서비스가 제공하는 작업 생성·상태 조회·다운로드·헬스체크 응답까지 느려지는 것이었습니다. 따라서 목표를 “FFmpeg 성공”이 아니라 “변환 요청이 겹쳐도 API 제어 흐름을 유지”하는 것으로 잡았습니다.
확인된 증거
- GIF 후보를 만들 때마다 별도
ffmpeg자식 프로세스를 실행합니다. - 한 작업도 여러 품질 프리셋을 순서대로 시험할 수 있습니다.
- 작업 생성 API는 완료를 기다리지 않고
202 Accepted와 작업 ID를 반환합니다. - 현재 소스는
pendingJobs,activeJobs,MAX_CONCURRENT_JOBS로 실행 수를 통제합니다.
src/server.js, src/compressor.js, 커밋 0eddbc9·2fd5bdf. 과거 CPU 퍼센트와 응답시간 원본은 남아 있지 않습니다.가설을 나눴습니다
- 업로드 파일 크기 때문에 전송 구간이 느린가
- FFmpeg 한 작업 자체의 프리셋 탐색이 오래 걸리는가
- 여러 작업이 동시에 자원을 경합하는가
- 완료 파일과 임시 파일이 누적돼 디스크까지 압박하는가
단일 작업 최적화와 동시 요청 제어는 별개 문제로 보고, 먼저 서버 전체에 영향을 주는 동시성 경계를 해결했습니다.
진단 순서
- 요청 한 건이 생성하는 자식 프로세스 흐름을 확인했습니다.
- 후보 프리셋 반복이 한 작업의 실행 시간을 늘릴 수 있음을 확인했습니다.
- 요청 수가 그대로 동시 실행 수가 되는 구조인지 점검했습니다.
- HTTP 응답과 실제 변환 실행을 분리할 수 있는지 검토했습니다.
실제 원인
FFmpeg 자체의 오류가 아니라 CPU 집약적인 작업에 동시성 경계가 없던 실행 구조가 원인이었습니다. HTTP 요청이 들어오는 속도와 서버가 변환을 처리할 수 있는 속도가 다르므로, 둘 사이에 대기열과 실행 상한이 필요했습니다.
대안을 비교했습니다
선택한 조치
작업 생성 요청에는 즉시 작업 ID를 반환하고, 실제 변환은 pendingJobs에 넣었습니다. runNextJob()은 실행 중 작업이 설정값보다 적을 때만 다음 작업을 시작합니다.
const maxConcurrentJobs = Math.max(
1,
Number(process.env.MAX_CONCURRENT_JOBS) || 2
);
function runNextJob() {
while (activeJobs < maxConcurrentJobs && pendingJobs.length > 0) {
const next = pendingJobs.shift();
activeJobs += 1;
void processJob(next.job, next.targetBytes);
}
}큐만 넣고 끝내지 않았습니다
/health에서 실행·대기 작업 수를 확인할 수 있습니다.- IP별 작업 생성은 한 시간에 12개로 제한했습니다.
- 업로드는 GIF 한 개, 최대 100MB로 제한했습니다.
- 완료·실패 작업과 임시 파일은 15분 뒤 정리합니다.
finally에서 실행 수를 감소시켜 실패해도 큐가 계속 진행됩니다.
검증과 결과
node --test실행 결과 4개 테스트가 모두 통과했습니다.- FPS 파싱, 품질 프리셋 순서, 크기 표시, 해상도 프리셋을 검증했습니다.
- 대기 작업과 실행 작업이 별도 상태로 관리됩니다.
- 실행 중 작업 수는
MAX_CONCURRENT_JOBS의 상한 안에서만 증가합니다.
운영 안전장치
동시성 상한, 요청 제한, 업로드 제한, 실패 처리, 파일 보존 시간을 함께 두었습니다. CPU 집약 작업을 추가할 때도 “접수 → 대기 → 실행 → 완료/실패 → 정리”의 수명주기를 먼저 설계하도록 기준을 남겼습니다.
기록 한계와 다음 단계
- 장애 당시 CPU·메모리·응답시간 시계열은 남아 있지 않아 수치를 만들지 않았습니다.
- 현재 큐는 단일 서버 메모리에 있어 재시작하면 대기 작업을 잃을 수 있습니다.
- 다중 인스턴스로 확장할 때는 Redis 기반 큐와 별도 Worker가 필요합니다.
- 다음 증거는 동시 요청 부하 테스트와 p95 응답시간 비교입니다.
배운 점
기능이 정상적으로 실행되는 것과 운영 환경에서 안전하게 실행되는 것은 다른 문제였습니다. CPU 집약 작업은 요청 수를 그대로 프로세스 수로 바꾸지 말고, 서버 용량에 맞는 명시적인 실행 경계를 가져야 합니다.