[Dev] Pod CPU Limit과 FFmpeg Thread 최적 조정 - 4.1. 문제 원인 파악: 현상 관찰
이전 글에서 CPU limit이 성능 저하의 원인임을 실험으로 확인했다. 이 글에서는 throttling이 실제로 어떻게 발생하고 있는지, 배경지식에서 다룬 cpu.stat과 ffmpeg stat을 통해 직접 관찰한다.
숫자로 직접 확인하고 나니, 배경지식에서 머릿속으로만 그렸던 그림이 현실이 되는 경험을 했다.
TL;DR
- cgroup
cpu.stat관찰 결과, 거의 매 period마다 throttling 발생. CPU가 일한 시간보다 대기한 시간이 2배 이상 - ffmpeg stat 비교: CPU limit 1 core에서 fps=32/speed=1.07x, 제한 없을 때 fps=931/speed=31.1x (약 29배 차이)
- ffmpeg이 호스트 코어 수(20) 기반으로 수십 개의 swscaler 스레드를 생성하는 것을 확인
time명령으로 워크로드가 CPU-bound임을 확인 (user » sys, I/O 대기 문제 아님)- 이후 실험 방향: CPU limit 조절 + ffmpeg 스레드 수 조절
cgroup cpu.stat 관찰
측정 방법
CPU limit 1000m이 설정된 Pod에 요청을 보내면서, 컨테이너 내부의 /sys/fs/cgroup/cpu.stat을 1초 간격으로 읽었다. 요청 처리 완료까지 약 148초가 소요되었다. cpu.stat은 누적값이므로, 요청 직전과 직후의 스냅샷을 비교한다.
watch -n 1로 수동 캡처했기 때문에 요청 시작/끝과 정확히 일치하지는 않는다. 정밀한 값보다는 전체적인 경향을 읽는 데 의미가 있다.
# Pod 내부에서 (또는 호스트에서 해당 컨테이너의 cgroup 경로에서)
watch -n 1 cat /sys/fs/cgroup/cpu.stat
결과
Before (요청 처리 전)
| 항목 | 값 |
|---|---|
| usage_usec | 4,363,848 |
| user_usec | 2,201,217 |
| system_usec | 2,162,630 |
| nr_periods | 3,106 |
| nr_throttled | 0 |
| throttled_usec | 0 |
After (요청 처리 후)
| 항목 | 값 |
|---|---|
| usage_usec | 124,702,078 |
| user_usec | 117,993,908 |
| system_usec | 6,708,170 |
| nr_periods | 4,499 |
| nr_throttled | 1,194 |
| throttled_usec | 263,624,194 |
Delta (요청 처리 구간)
cpu.stat은 컨테이너가 시작된 이후의 누적값이다. 따라서 특정 요청 처리 구간의 영향만 보려면, 처리 전후의 차이(delta)를 구해야 한다.
| 항목 | after - before | 해석 |
|---|---|---|
| usage_usec | 120,338,230 (~120초) | 이 요청 처리 동안 CPU가 실제로 일한 시간 |
| user_usec | 115,792,691 (~116초) | 대부분 사용자 모드 (ffmpeg 연산) |
| system_usec | 4,545,540 (~4.5초) | 커널 모드 비중은 낮음 |
| nr_periods | 1,393 | 이 구간에서 경과한 100ms period 횟수 |
| nr_throttled | 1,194 | 그 중 throttling이 발생한 period 횟수 |
| throttled_usec | 263,624,194 (~264초) | throttling으로 대기한 총 시간 |
- throttling 비율: nr_throttled / nr_periods = 1,194 / 1,393 = 85.7%. period 10번 중 8~9번은 quota를 소진하고 강제 대기했다.
- 대기 vs. 작업 비율: throttled_usec(264초) / usage_usec(120초) ≈ 2.2배. CPU가 일한 시간보다 놀면서 기다린 시간이 2배 이상이다.
- 벽시계 시간 검증: nr_periods × 100ms = 1,393 × 0.1초 ≈ 139초. 실험 1에서 측정한 real time(약 148초)과 대략 일치한다.
- 워크로드 특성: user_usec(116초) » system_usec(4.5초). I/O 대기가 아니라 순수 CPU 연산(ffmpeg 디코딩/스케일링)이 지배적이다.
분석
요청 처리 구간 동안의 delta를 계산하면 throttling의 심각도가 드러난다.
- nr_throttled / nr_periods: 거의 매 period(100ms)마다 throttle이 발생
- throttled_usec vs. usage_usec: CPU가 실제로 일한 시간보다 대기한 시간이 2배 이상
- 이 컨테이너는 이미 과거에도 상당한 throttling을 당한 상태 (cpu.stat 누적값이 큼)
배경지식에서 다룬 것처럼, 멀티스레드가 1코어 quota를 순식간에 소진하고 나머지 시간을 대기하는 패턴이 실제로 발생하고 있다.
ffmpeg stat 관찰
측정 방법
Pod 내부에서 동일한 ffmpeg 명령을 직접 실행하고 stat 출력을 비교했다. CPU limit 1000m인 Pod과 limit이 없는(BestEffort) Pod에서 각각 측정했다.
kubectl exec <pod-name> -n <namespace> -- \
ffmpeg -stats -i /data/video.mp4 \
-filter_complex [0]scale=1280:720[s0] \
-map [s0] -q:v 2 -vsync vfr /data/frames/frame_%06d.jpg -y
CPU Limit 1 core
frame= 3723 fps= 32 q=2.0 Lsize=N/A time=00:02:04.25 bitrate=N/A speed=1.07x
| 항목 | 값 | 해석 |
|---|---|---|
| frame | 3723 | 총 처리 프레임 수 (영상 전체) |
| fps | 32 | 초당 32프레임 처리 (원본 29.97fps 대비 약간 빠름) |
| speed | 1.07x | 실시간 대비 1.07배. 거의 실시간 속도 |
영상 길이가 약 124초이므로, speed 1.07x에서의 예상 처리 시간은 124 / 1.07 ≈ 116초다.
BestEffort (제한 없음)
frame= 3723 fps=931 q=2.0 Lsize=N/A time=00:02:04.25 bitrate=N/A speed=31.1x
| 항목 | 값 | 해석 |
|---|---|---|
| frame | 3723 | 동일한 프레임 수 |
| fps | 931 | 초당 931프레임 처리 |
| speed | 31.1x | 실시간 대비 31배 |
speed 31.1x에서의 예상 처리 시간은 124 / 31.1 ≈ 4초다.
비교
| 지표 | CPU Limit 1 core | BestEffort | 차이 |
|---|---|---|---|
| fps | 32 | 931 | 약 29배 |
| speed | 1.07x | 31.1x | 약 29배 |
| 예상 처리 시간 | ~116초 | ~4초 | 약 29배 |
speed 비율(31.1 / 1.07 = 29배)이 처리 시간 비율(116 / 4 = 29배)과 정확히 일치한다. ffmpeg의 처리 효율이 곧 전체 성능을 결정하고 있다.
swscaler 스레드 수 관찰
ffmpeg 실행 로그에서 주목할 부분이 있다. swscaler 인스턴스가 대량으로 생성되었다.
[swscaler @ 0x...] [swscaler @ 0x...] deprecated pixel format used ...
[swscaler @ 0x...] [swscaler @ 0x...] deprecated pixel format used ...
... (수십 줄)
각 swscaler 인스턴스는 별도의 워커 스레드다. 고유한 메모리 주소를 세어 보면, 약 20개 이상의 스레드가 생성되었음을 확인할 수 있다. 이는 호스트 노드의 CPU 코어 수(20)와 일치한다.
배경지식에서 다룬 것처럼, ffmpeg은 -threads 미지정 시 시스템 코어 수 기반으로 스레드를 생성한다. 컨테이너 내부에서도 호스트의 코어 수를 참조하여 20개 가까운 스레드를 만들었고, 이 스레드들이 1코어 quota를 두고 경쟁하면서 throttling이 심화된 것이다.
직접 눈으로 확인하니 허탈하기도 했다. 20개 스레드가 1코어 분량의 시간을 두고 다투고 있었으니, 느릴 수밖에 없었던 거다.
limit을 잡지만 않았어도, 혹은-threads옵션 하나만 지정했어도 이 상황을 피할 수 있었을 텐데.
speed 지표 해석 시 주의
speed=1.07x는 “ffmpeg이 일할 때의 속도”다. throttling으로 대기하는 시간은 ffmpeg의 speed 계산에 포함되지 않는다. 즉:
- ffmpeg이 CPU를 받아서 실행되는 동안에는 1.07x 속도로 처리
- 하지만 전체 벽시계 시간의 대부분은 throttling 대기
이는 cgroup cpu.stat 관찰 결과와도 일치한다. CPU가 일한 시간(usage_usec)보다 대기한 시간(throttled_usec)이 훨씬 길었다.
time 명령으로 워크로드 확인
측정 방법
CPU limit이 설정된 Pod 내부에서 time 명령과 함께 ffmpeg을 실행하여, 실제 CPU 시간 분포를 확인했다.
kubectl exec <pod-name> -n <namespace> -- \
time ffmpeg -i /data/video.mp4 \
-filter_complex [0]scale=1280:720[s0] \
-map [s0] -q:v 2 -vsync vfr /data/frames/frame_%06d.jpg -y
참고: 기본 백엔드 컨테이너 이미지에
time,sh등이 없어서, 디버깅용 이미지를 별도로 빌드하여 실험했다.
결과
63.94user 2.87system 1:06.91elapsed 99%CPU
(0avgtext+0avgdata 307512maxresident)k
0inputs+1005176outputs (2major+68987minor)pagefaults 0swaps
| 지표 | 값 | 의미 |
|---|---|---|
| user | 63.94초 | 사용자 모드 CPU 시간 (ffmpeg 연산) |
| system | 2.87초 | 커널 모드 CPU 시간 (시스템 콜 등) |
| elapsed | 1분 6.91초 (~67초) | 벽시계 경과 시간 |
| %CPU | 99% | 경과 시간 대비 CPU 사용 비율 |
| maxresident | 307,512 KB (~300MB) | 최대 메모리 사용량 |
이 측정은 CPU limit이 없는(BestEffort) Pod에서 실행한 결과다. limit이 없으므로 throttling이 발생하지 않아, ffmpeg 자체의 순수한 워크로드 특성을 확인할 수 있다.
분석
- user » sys: 대부분의 CPU 시간이 사용자 공간(ffmpeg 코덱 처리)에서 소비. 커널 공간(시스템 콜, I/O)은 미미
- real ≈ user: CPU가 놀지 않고 일하고 있었음. I/O 대기로 블로킹된 시간이 거의 없음
- %CPU ≈ 99%: 가용한 CPU 시간을 거의 전부 사용
이 결과는 이 워크로드가 CPU-bound이며, I/O 대기 문제가 아님을 확정짓는다. 배경지식에서 예상한 것처럼, ffmpeg 프레임 추출은 디코딩/스케일링/인코딩 과정에서 user time이 지배적인 CPU 집약 작업이다.
정리: throttling 메커니즘 확인
관찰 결과를 종합하면 다음과 같다.
- ffmpeg이 호스트 코어 수(20) 기반으로 ~20개 swscaler 스레드를 생성한다
- 1코어 quota (100ms/period)를 20개 스레드가 경쟁한다
- 매 period마다 quota를 순식간에 소진한다
- 나머지 시간 강제 대기한다 (throttling)
- CPU가 일한 시간(120초) < 대기한 시간(264초), 약 2배 이상
- 결과: speed 1.07x, fps 32 (제한 없을 때 대비 29배 느림)
이후 실험 방향
CPU throttling이 원인임을 확인했다. 이제 “어떻게 해결할 것인가”를 고민해야 한다.
| 관찰 | 원인 | 인사이트 |
|---|---|---|
| K8s Pod에서만 느림 | CPU limit 1000m throttling | CPU limit 조절 필요 |
| limit 해제 시 성능 회복 | 코어 수가 처리 속도에 직접 영향 | 코어 수와 처리 속도의 상관관계 확인 필요 |
| ffmpeg 기본 스레드 = 호스트 코어 수 | 1코어 제한에서 20개 스레드는 context switching 오버헤드만 증가 | ffmpeg 스레드 수 조절 가능성 |
단순히 CPU limit을 올리는 것도 방법이지만, 무한정 올릴 수는 없다. ffmpeg 스레드 수를 limit에 맞게 조절하면 throttling 오버헤드를 줄일 수 있을지도 모른다. 예를 들어:
- 시나리오 A: CPU 1 core + 20 스레드 → 20개 스레드가 1코어 경쟁, throttling 심각
- 시나리오 B: CPU 1 core + 2 스레드 → 경쟁 감소, context switching 비용 감소
다음 글에서는 CPU limit과 ffmpeg 스레드 수를 조합하여 튜닝 실험을 진행한다.
여기까지 원인을 파악하는 데에만 꽤 많은 시간을 들였다. 하지만 원인을 제대로 이해하지 못하면 해결책도 “감”에 의존하게 된다는 것을, Co-DETR 추론 서빙 개선 때 뼈저리게 경험했다. 이번에는 원인을 확실히 잡고 넘어갔으니, 최적의 해결책을 찾을 수 있으리라 기대한다.
댓글남기기