[Kubernetes] Pod CPU Limit과 FFmpeg Thread 최적 조정 - 3. 배경지식: 영상 처리와 FFmpeg
이전 글까지 CPU 시간의 성격, CFS 스케줄러의 throttling 메커니즘, cgroup과 K8s의 리소스 관리를 살펴봤다. 이제 마지막 퍼즐 조각인 ffmpeg을 볼 차례다. ffmpeg이 영상을 어떻게 처리하고, 내부적으로 스레드를 어떻게 사용하는지 알아야, CPU limit과의 상호작용을 이해할 수 있다.
ffmpeg의 영상 처리 파이프라인과 멀티스레드 아키텍처도 파고 들면 깊은 주제다. 여기서는 CPU limit 환경에서 왜 문제가 되는지를 설명할 수 있을 정도로만 다룬다.
TL;DR
- ffmpeg은 decode → process → encode 파이프라인으로 영상을 처리한다
- 기본적으로 멀티스레드로 동작하며,
-threads미지정 시 시스템 CPU 코어 수 기반으로 스레드 수를 결정한다 - ffmpeg의 speed는
처리한 영상 시간 / 경과한 벽시계 시간으로, 처리 효율을 나타낸다 - ffmpeg 처리는 대부분 CPU-bound 작업이며,
time명령으로 워크로드 특성을 파악할 수 있다 - CPU limit 환경에서 ffmpeg이 과도한 스레드를 생성하면, quota를 빠르게 소진해 throttling이 심화된다
FFmpeg이란
ffmpeg은 영상과 오디오를 변환, 인코딩, 디코딩, 필터링하는 범용 멀티미디어 프레임워크다. 이 문제에서는 영상에서 프레임을 추출하고(JPEG 이미지로 저장), 썸네일을 생성하는 데 사용하고 있다.
영상 처리 파이프라인
ffmpeg의 영상 처리는 세 단계 파이프라인으로 구성된다.
[Decode] → [Process] → [Encode]
입력 처리 출력
| 단계 | 역할 | CPU 부하 |
|---|---|---|
| Decode | 압축된 영상 데이터를 원시 프레임으로 변환 | 높음 (코덱에 따라 다름) |
| Process | 스케일링, 필터 적용 등 프레임 가공 | 중~높음 (필터에 따라 다름) |
| Encode | 가공된 프레임을 출력 형식으로 인코딩 | 높음 (코덱에 따라 다름) |
이전 글에서 본 프레임 추출 명령을 이 파이프라인에 대입하면:
ffmpeg -i video.mp4 -filter_complex [0]scale=1280:720[s0] -map [s0] -q:v 2 -vsync vfr frames/frame_%06d.jpg -y
- Decode: H.264로 인코딩된 mp4 파일을 읽어 원시 프레임(YUV)으로 디코딩
- Process: scale 필터로 1280x720 해상도로 변환
- Encode: 각 프레임을 JPEG(품질 2)으로 인코딩하여 디스크에 저장
세 단계 모두 CPU를 많이 사용하는 작업이다.
멀티스레드 동작
순차 처리 vs. 병렬 처리
스레드가 1개라면 모든 프레임을 순차적으로 처리한다.
Thread 1: [Decode F1][Process F1][Encode F1][Decode F2][Process F2][Encode F2]...
스레드가 여러 개면 파이프라인 단계를 중첩(overlap)하여 처리할 수 있다.
Thread 1: [Decode F1] [Decode F2] [Decode F3] ...
Thread 2: [Process F1] [Process F2] [Process F3] ...
Thread 3: [Encode F1] [Encode F2] ...
Thread 4: [Write F1] ...
충분한 스레드가 있으면, Thread A가 Frame 10을 Decode하는 동안 Thread B가 Frame 9를 Process하고, Thread C가 Frame 8을 Encode하는 완전 중첩(full overlap)이 가능하다. 이 상태에서 모든 파이프라인 단계가 동시에 진행되어 대기 시간이 최소화된다.
-threads 옵션
ffmpeg의 병렬 처리 스레드 수는 -threads 옵션으로 제어한다.
ffmpeg -threads N -i input.mp4 ...
| 값 | 동작 |
|---|---|
0 (기본값) |
시스템 CPU 코어 수 기반으로 자동 결정 |
1 |
싱글 스레드 |
N |
명시적으로 N개 스레드 사용 |
기본값이 0(auto)이라는 점이 중요하다. ffmpeg은 별도로 옵션을 지정하지 않으면, 시스템의 CPU 코어 수를 감지하여 그에 맞는 스레드를 생성한다.
다만 -threads가 제어하는 것은 주로 디코더/인코더의 스레드 수이며, ffmpeg 내부의 스레딩은 이보다 복잡하다.
| 스레딩 유형 | 동작 | -threads로 제어 |
|---|---|---|
| Frame-level threading | 여러 프레임을 동시에 디코딩/인코딩 | O |
| Slice-level threading | 하나의 프레임을 여러 슬라이스로 분할하여 병렬 처리 | O |
| Filter threading | 필터 그래프 내부의 병렬 처리 | X (별도 스레드 풀) |
따라서 실제 생성되는 OS 스레드 수는 -threads 값보다 많을 수 있다. -threads 1로 설정하더라도 필터나 muxer가 별도 스레드를 사용할 수 있으므로, 스레드 수를 완전히 1개로 제한하는 것은 어렵다. 이 점은 이후 실험에서 -threads 설정과 실제 스레드 수의 관계를 관찰할 때 유의해야 한다.
이전 글에서 작성한 코드에는 -threads 옵션을 지정하지 않았다. ffmpeg이 실제로 몇 개의 스레드를 만들어 동작하는지는 이후 실험에서 확인한다.
이 부분을 알게 되었을 때, “아차” 싶었다. 코드에서
-threads옵션 하나 빠진 것이 이렇게 큰 성능 차이로 이어질 줄은 몰랐다. 컨테이너가 호스트의 코어 수를 “보는” 것과 “쓸 수 있는” 것이 다르다는 사실을, 뼈저리게 배웠다.
영상 처리 용어
ffmpeg의 처리 성능을 해석하려면 몇 가지 용어를 알아야 한다.
speed
speed는 ffmpeg이 영상을 얼마나 빠르게 처리하는지 나타내는 지표로, 처리한 영상 시간을 경과한 벽시계 시간으로 나눈 값이다. 여기서 “영상 시간”이란 각 프레임에 기록된 PTS(Presentation Time Stamp) 기준의 시간 축이다. PTS는 각 프레임이 영상 내에서 언제 표시되어야 하는지를 나타내는 타임스탬프다.
speed = 처리한 영상 시간(PTS 기준) / 경과한 벽시계 시간
ffmpeg은 처리 중 주기적으로 현재 speed를 계산하여 표시한다.
현재 speed = 현재까지 처리한 마지막 프레임의 PTS / 현재까지 경과한 벽시계 시간
예를 들어, 60초 영상을 처리할 때 진행에 따라 speed가 다음과 같이 변한다.
| 처리 진행 | 처리한 영상 시간 (PTS) | 벽시계 경과 시간 | speed |
|---|---|---|---|
| 20% 완료 | 12초 | 1초 | 12x |
| 50% 완료 | 30초 | 3초 | 10x |
| 100% 완료 | 60초 | 6초 | 10x |
최종적으로 로그에 표시되는 speed는 마지막 프레임 PTS / 총 처리 시간이다. 즉 100% 완료 시점의 값이 최종 speed다.
| 처리한 영상 시간 | 실제 걸린 시간 | speed | 의미 |
|---|---|---|---|
| 60초 영상 | 6초 | 10x | 10배속 처리 |
| 60초 영상 | 60초 | 1x | 실시간 처리 |
| 60초 영상 | 120초 | 0.5x | 절반 속도 |
파일 배치 처리 목적이라면, speed는 단순히 처리 효율이다. 높을수록 빠르게 끝난다. 실시간 스트리밍 목적이라면 speed가 최소 1x 이상이어야 하는데, 지금 다루는 문제는 배치 처리이므로 단순히 “빠를수록 좋다”고 보면 된다.
fps와 기타 용어
| 용어 | 의미 | 비고 |
|---|---|---|
| fps | 벽시계 1초당 처리하는 프레임 수 | throughput 지표. 높을수록 빠름 |
| codec | 영상 압축/해제 알고리즘 | H.264, H.265, VP9 등. 복잡할수록 디코딩 부하 증가 |
| 해상도 | 프레임의 가로 x 세로 픽셀 수 | 1280x720, 1920x1080 등. 높을수록 연산량 증가 |
| GOP | Group of Pictures. 키프레임 간격 | seek 성능에 영향 |
| FPS (원본) | 영상 자체의 초당 프레임 수 | 30fps 영상이면 1초당 30개 프레임 존재 |
이 문제에서는 코덱, 해상도, GOP 등 영상 자체의 특성을 최적화하는 단계까지는 다루지 않는다. 핵심은 ffmpeg이 CPU를 얼마나 사용하는지, 그리고 그 사용 패턴을 어떻게 제어할 수 있는지다.
ffmpeg stat 해석
ffmpeg 실행 중 출력되는 진행 상태(stat)를 읽을 수 있어야 한다.
frame= 3723 fps= 32 q=2.0 time=00:02:04.25 speed=1.07x
| 필드 | 의미 | 이 문제에서의 해석 |
|---|---|---|
| frame | 지금까지 처리한 프레임 수 | 추출된 이미지 개수 |
| fps | 벽시계 1초당 처리 프레임 수 | 처리 throughput. throttling 시 급격히 떨어짐 |
| q | 품질 계수 | q:v=2 설정값 반영 (1-31, 낮을수록 고품질) |
| time | 처리한 영상 시간 (PTS 기준) | 영상의 어느 지점까지 처리했는지 |
| speed | 영상 시간 / 벽시계 시간 | 처리 효율. 높을수록 빠름 |
throttling이 발생하면 fps가 떨어지고, speed가 낮아진다. 이 지표들의 변화를 관찰하면 CPU 제한이 ffmpeg 성능에 미치는 영향을 직접 확인할 수 있다.
ffmpeg 워크로드 특성
CPU-bound 작업
CPU-bound 작업이란 처리 시간의 대부분이 CPU 연산에 의해 결정되는 작업으로, I/O 대기보다 CPU 계산 자체가 병목인 워크로드를 말한다. ffmpeg의 영상 처리는 대부분 CPU-bound다. 파이프라인의 각 단계를 CPU 시간 관점에서 보면:
| 단계 | user time | sys time | 설명 |
|---|---|---|---|
| 영상 디코딩/인코딩 | 매우 높음 | 낮음 | 순수 CPU 연산 (DCT, 양자화 등) |
| 파일 읽기/쓰기 | 낮음 | 높음 | 시스템 콜, 페이지 캐시 |
| 네트워크 처리 | 낮음 | 중간 | 소켓, TCP 등 |
디코딩과 인코딩이 전체 처리 시간의 대부분을 차지하며, 이 과정에서 user time이 급증한다.
time 명령으로 워크로드 분석
time 명령과 함께 실행하면 워크로드 특성을 파악할 수 있다.
time ffmpeg -i video.mp4 -filter_complex [0]scale=1280:720[s0] -map [s0] -q:v 2 -vsync vfr frames/frame_%06d.jpg -y
결과를 아래와 같이 해석해 볼 수 있다.
| 패턴 | 가능한 원인 |
|---|---|
| user가 높다 | ffmpeg 인코딩/디코딩 자체가 무거움 (코덱, 해상도, 필터 문제) |
| sys가 높다 | 디스크 I/O, overlayfs, VM 환경 문제 |
| real만 높고 user/sys는 낮다 | CPU throttling 또는 I/O wait |
세 번째 패턴이 이 문제에서 핵심이 될 수 있다. throttling이 발생하면 프로세스가 강제 대기하므로, CPU가 실제로 일한 시간(user+sys)은 크지 않은데 벽시계 시간(real)만 길어진다.
이전 글에서 도출한 코어 수 추정 공식 (user + sys) / real도 여기에 적용할 수 있다. 이 비율이 Pod의 CPU limit보다 현저히 낮다면, throttling으로 인해 CPU 시간을 제대로 활용하지 못하고 있다는 뜻이다.
정리: 퍼즐 조각 맞추기
세 편의 배경지식을 종합하면, 이전 글의 37배 성능 차이에 대한 가설을 세울 수 있다.
- ffmpeg은 기본적으로 호스트 코어 수 기반으로 많은 스레드를 생성한다 (post 03)
- Pod CPU limit
1000m은 cgroupcpu.max로 변환되어 1코어 분량의 quota가 설정된다 (post 02) - 다수의 스레드가 동시에 실행되면 quota를 순식간에 소진하고, 나머지 시간은 throttling으로 대기한다 (post 01)
- goroutine 3개가 ffmpeg 2개(프레임 추출 + 썸네일) + 업로드 1개를 병렬 실행하므로, CPU 경합이 더 심해진다 (post 00)
ffmpeg 스레드(호스트 코어 수 기반) × goroutine 2~3개
→ 1코어 quota를 순식간에 소진
→ 거의 매 period마다 throttling
→ 대부분의 시간을 대기하며 보냄
→ 4.5초 → 148초 (37배)
하지만 이것은 가설이다. I/O, 메모리, 네트워크 등 다른 요인의 영향이 없는지도 확인해야 한다. 다음 글에서는 이 가설을 실험으로 검증한다.
세 편의 배경지식은 각각 독립적인 주제지만, 이 문제에서는 서로 맞물려 동작한다. CPU의 시간적 성격 → CFS bandwidth control의 quota/period → cgroup을 통한 컨테이너 리소스 제한 → ffmpeg의 호스트 기준 스레드 생성. 이 연결 고리를 이해해야 단순히 “느리다”는 현상 너머에 있는 구조적 원인에 도달할 수 있다.
댓글남기기