[Dev] Pod CPU Limit과 FFmpeg Thread 최적 조정 - 4.0. 문제 원인 파악: 실험

4 분 소요


배경지식을 정리했으니, 이제 실험으로 가설을 검증할 차례다. 처음 목격한 상황에서 세운 가설들 중 어떤 것이 실제 원인인지 좁혀 나간다.

솔직히 말하면, 배경지식을 정리하면서 “아, CPU limit이 원인이겠구나”라는 심증은 이미 굳어져 있었다. 그래도 다른 가능성을 배제하지 않고 실험으로 확인하는 과정이 필요했다. 확증 편향에 빠지지 않기 위해서라도.


TL;DR

  • 환경별 요청 처리 시간 측정 결과, TTFB는 모든 환경에서 유사(~0.2초). 클라이언트/네트워크는 병목이 아님
  • 동일 노드에서 K8s Pod(CPU limit 1000m)과 Docker 컨테이너(제한 없음) 비교 시 처리 시간 차이 극명
  • K8s Pod의 CPU limit을 해제하면 Docker 컨테이너와 동일한 성능 회복
  • CPU throttling이 성능 저하의 핵심 원인임을 확인


실험 설계

측정 방법

두 가지 방법으로 요청 처리 시간을 측정한다.

1. time curl: 전체 수행 시간 측정

time curl -X 'POST' \
  'http://<host>:<port>/api/train/manual/init' \
  -H 'accept: application/json' \
  -H 'Content-Type: multipart/form-data' \
  -F 'model_name=test' \
  -F 'video=@play.mp4;type=video/mp4'

time curl에서 주의할 점이 있다. 여기서 측정되는 user/sys는 클라이언트(curl 프로세스)의 CPU 시간이지, 서버의 CPU 시간이 아니다. curl은 요청을 보낸 후 응답이 올 때까지 거의 sleep 상태이므로 user/sys는 0에 가깝고, real만 의미 있다.

real    2m30.302s  ← 서버가 응답할 때까지 대기한 시간
user    0m0.008s   ← curl 프로세스의 user space CPU 시간 (거의 0)
sys     0m0.031s   ← curl 프로세스의 kernel space CPU 시간 (거의 0)

즉, time curl의 real은 사실상 서버의 전체 처리 시간이다.


2. curl -w: 단계별 타이밍 측정

curl -X 'POST' \
  'http://<host>:<port>/api/train/manual/init' \
  -H 'Content-Type: multipart/form-data' \
  -F 'model_name=test' \
  -F 'video=@play.mp4;type=video/mp4' \
  -o /dev/null -s \
  -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n"
항목 의미
time_namelookup DNS 조회 시간
time_connect TCP 연결 완료까지 시간
time_starttransfer 첫 바이트 도착(TTFB). 파일 업로드 + 서버 처리 시작 후 첫 응답까지
time_total 전체 요청 완료

TTFB가 유사하다면 네트워크 구간은 병목이 아니고, Total - TTFB가 서버의 실제 처리 시간에 해당한다.


테스트 영상

항목
길이 약 2분
크기 22.7MB
해상도 1280 x 720
FPS 29.97
코덱 H.264 (High)


실험 1: 환경별 요청 처리 시간

목적

서로 다른 환경에서 동일한 요청의 처리 시간이 어떻게 다른지 확인한다.


실험 환경

환경 설명 CPU 제한
A Ubuntu 22.04, 로컬 Docker 컨테이너 없음
B Ubuntu 22.04, VirtualBox VM 위 Docker 컨테이너 없음
C Ubuntu 22.04, K3s 클러스터 Pod 1000m (1 core)
  • 클라이언트: 클러스터 외부 로컬 PC
  • A, B, C 모두 동일한 이미지로 실행


결과


분석

  • TTFB는 모든 환경에서 유사 (~0.2초): 네트워크 전송과 요청 수신까지는 환경 간 차이가 없다
  • 차이가 벌어지는 구간은 서버 처리 시간: Total - TTFB
  • time curl의 user/sys가 모든 환경에서 거의 0: curl은 그냥 기다린 것뿐, 클라이언트는 병목이 아님

K8s Pod 환경(C)이 로컬 Docker(A)보다 현저히 느리다. VM 위 Docker(B)보다도 느리다.


실험 2: 동일 노드에서 K8s Pod vs. Docker 컨테이너

목적

실험 1에서는 환경이 다른 노드에 분산되어 있었으므로, 하드웨어 차이의 영향을 배제할 수 없었다. 동일한 노드에서 K8s Pod과 Docker 컨테이너를 비교하여 순수하게 런타임 환경의 차이만 확인한다.


실험 환경

K8s Pod이 실행 중인 노드에 동일한 이미지로 Docker 컨테이너를 추가로 띄웠다.

환경 실행 위치 CPU 제한
K8s Pod worker-node (20코어, 128GB) 1000m (1 core)
Docker 컨테이너 동일 노드 없음
  • 클라이언트: 동일 PC에서 동일 노드 IP로 요청 (포트만 다름)
  • 노드 사양: Intel Xeon (20코어), 128GB RAM, Ubuntu 22.04, Kernel 6.8
  • 동일한 영상, 동일한 이미지, 동일한 API


결과

1. time curl 결과

지표 K8s Pod (CPU limit 1 core) Docker 컨테이너 (제한 없음)
real 2m28.263s 0m4.539s
user 0m0.011s 0m0.018s
sys 0m0.010s 0m0.000s

2. curl -w 결과

지표 K8s Pod Docker 컨테이너
DNS 0.000017s 0.000015s
Connect 0.002364s 0.000631s
TTFB 0.209851s 0.205074s
Total 148.835s 4.584s

time curl에서 user/sys가 양쪽 모두 거의 0이다. curl 클라이언트 자체는 CPU를 거의 쓰지 않았고, real 시간 대부분을 서버 응답을 기다리는 데 보냈다는 뜻이다. 즉 병목은 클라이언트가 아니라 서버 측이다.

curl -w에서 DNS, Connect, TTFB는 거의 동일한데 Total만 극명하게 다르다. TTFB(첫 바이트 수신까지)가 동일하다는 것은 네트워크 경로나 서버 초기 처리에는 차이가 없다는 뜻이고, Total의 차이는 서버가 영상을 처리하는 시간 자체에서 발생한 것이다.

동일 노드에서 약 32배 차이.


분석

동일 하드웨어, 동일 이미지, 동일 영상에서 처리 시간 차이가 극명하다. 하드웨어, I/O(같은 디스크), 네트워크(같은 NIC)가 동일하므로, 차이의 원인은 런타임 환경 설정에 있다.

두 환경의 유일한 차이: K8s Pod에는 CPU limit 1000m이 설정되어 있고, Docker 컨테이너에는 없다.


실험 3: K8s Pod CPU limit 해제

목적

CPU limit이 원인이라면, limit을 해제하면 성능이 회복되어야 한다. 이를 직접 확인한다.


실험 방법

Deployment 매니페스트에서 resources 항목을 제거하고 재배포했다.

# 변경 전
resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "2Gi"
    cpu: "1000m"

# 변경 후
resources: {}

QoS 클래스가 Burstable에서 BestEffort로 변경된 것을 확인했다.

kubectl describe pod <pod-name> -n <namespace> | grep QoS
# QoS Class: BestEffort


결과

1. time curl 결과

지표 BestEffort (CPU limit 해제)
real 0m4.655s
user 0m0.015s
sys 0m0.000s

2. curl -w 결과

지표 BestEffort (CPU limit 해제)
DNS 0.000015s
Connect 0.000687s
TTFB 0.189714s
Total 4.718s

실험 2의 Docker 컨테이너(4.584s)와 거의 동일한 수준으로 회복되었다.


분석

CPU limit을 해제하자 K8s Pod에서도 Docker 컨테이너와 동일한 수준의 성능이 나왔다. 예상했던 결과이긴 하지만, 직접 확인하니 확신이 들었다. 이로써 다른 가설들을 배제할 수 있다.

가설 검증 결과
CPU 리소스 제약 원인 확인. limit 해제 시 성능 회복
I/O 성능 차이 배제. 동일 노드에서 Docker도 동일 PVC/디스크 사용 가능, limit 해제로 해결
메모리 압박 배제. 22.7MB 영상에 2Gi는 충분, limit 해제 시 메모리 설정도 제거했으나 성능 회복
네트워크 오버헤드 배제. TTFB 동일, 처리 시간 구간에서만 차이


결론

세 가지 실험을 통해 아래와 같은 점을 확인했다:

  1. TTFB는 모든 환경에서 ~0.2초로 동일 → 네트워크, 클라이언트는 병목이 아님
  2. 동일 노드에서도 K8s Pod이 Docker 컨테이너 대비 현저히 느림 → 하드웨어/I/O 차이가 아님
  3. CPU limit 해제 시 동일 성능 회복 → CPU limit 1000m이 원인

CPU limit에 의한 throttling이 성능 저하의 핵심 원인이라는 것까지는 확인했다. 다음 글에서는 실제로 throttling이 어떻게 발생하고 있는지, cgroup cpu.stat과 ffmpeg stat을 통해 직접 관찰한다.

실험 결과를 정리하면서, 처음 리소스 설정을 할 때 “CPU 1 core면 충분하겠지”라고 안이하게 판단했던 것이 떠올랐다. ffmpeg이 내부적으로 어떻게 동작하는지, CPU limit이 실제로 무엇을 의미하는지 모른 채 설정한 값이었다. 앞으로는 리소스를 설정할 때, 해당 워크로드의 특성을 먼저 파악하는 습관을 들여야겠다.



hit count

댓글남기기