목차
- 세 후보를 놓고 본 비교 기준
- VPA 컴포넌트와 모드 구분
- EKS에 VPA 설치하기
- 실제 적용 흐름과 2주 결과
- Auto 모드를 끝까지 안 쓴 이유
- 흔히 만나는 함정 몇 가지
- 지금 바로 해볼 수 있는 것
EKS에서 FastAPI 워커 8개를 굴리던 중, 한 주 OOMKilled가 47건 찍혔다. 같은 기간 평균 CPU 사용률은 23%, 평균 메모리 사용률은 31%였다. 자원이 부족한 게 아니라 메모리 한도가 어긋나 있었다는 뜻이다. Kubernetes VPA 설정으로 OOMKilled 해결을 시도하기 전, 팀에서는 수동 튜닝, HPA 도입, VPA 도입 세 가지를 후보로 놓고 일주일을 비교했다.
또한, 결론부터 보면 VPA를 골랐고, 2주 뒤 OOMKilled는 3건으로 줄었다. 비용도 노드 한 대가 줄면서 월 $876에서 $730으로 내려갔다. 다만 그 과정에서 Initial 모드와 Auto 모드 사이에서 또 한 번 갈렸고, 결국 Auto는 끝까지 안 썼다. 이유는 뒤에 따로 정리한다.
세 후보를 놓고 본 비교 기준
리소스 문제를 해결할 때 가장 먼저 떠오르는 건 limit을 올리는 일이다. 가장 빠르고, 추가 컴포넌트도 없다. 단점은 over-provisioning이 굳어진다는 것. 한 번 800Mi로 올리면 실제 사용량이 400Mi여도 그대로 둔다. 노드 비용은 request 기준으로 잡히기 때문에 request도 같이 올리지 않으면 스케줄링이 꼬이고, 같이 올리면 노드가 늘어난다.
한편, HPA는 수평 확장이다. 파드 수를 늘려서 부하를 분산한다. 그런데 우리 워크로드는 단일 요청 처리 중에 메모리가 튀는 패턴이었다. 요청 자체가 많은 게 아니라, 특정 요청이 큰 페이로드를 받으면 메모리가 순간적으로 600Mi까지 올라간다. HPA로 파드를 늘려도 단일 요청이 큰 건 해결이 안 된다.
VPA는 파드 단위로 request/limit을 자동으로 조정한다. 메모리 패턴을 학습해서 적절한 값을 추천하고, 모드에 따라 자동 반영까지 한다. 도입 비용(러닝커브, 운영 부담)이 셋 중 가장 높지만, 우리 패턴엔 맞았다.
| 기준 | 수동 튜닝 | HPA | VPA |
|---|---|---|---|
| OOMKilled 해결 | 한시적 | 부분적 | 직접적 |
| over-provisioning | 굳어짐 | 보통 | 줄어듦 |
| 운영 부담 | 낮음 | 중간 | 중간~높음 |
| 단일 요청 메모리 스파이크 대응 | limit 상향 가능 | 부적합 | 가능 |
| 도입 난이도 | 낮음 | 중간 | 중간 |
(2026년 5월 기준, VPA v1.1.2, Kubernetes 1.29 환경 기준)
VPA 컴포넌트와 모드 구분
VPA는 단일 컨트롤러가 아니다. Recommender, Updater, Admission Controller 세 컴포넌트로 나뉜다. Recommender가 metrics-server 데이터를 보고 추천값을 계산한다. Updater는 추천값과 현재 값이 다르면 파드를 evict한다. Admission Controller는 파드가 새로 만들어질 때 추천값을 주입한다. 세 개가 각자 도는 구조라 한 컴포넌트만 끄면 그 동작만 빠진다.
Off 모드 — 추천만 받기
updateMode: "Off"로 두면 Recommender만 동작한다. 파드는 건드리지 않고 추천값을 status에 기록한다. 운영 중인 서비스에 VPA를 처음 붙일 때 이 모드부터 시작하는 게 안전하다.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: fastapi-worker-vpa
namespace: prod
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: fastapi-worker
updatePolicy:
updateMode: "Off" # 추천만 받고, 파드는 그대로 둔다
resourcePolicy:
containerPolicies:
- containerName: '*'
controlledResources: ["cpu", "memory"]
minAllowed:
cpu: 100m
memory: 256Mi
maxAllowed:
cpu: 2
memory: 2Gi
kubectl describe vpa fastapi-worker-vpa -n prod로 추천값을 확인할 수 있다. lowerBound, target, upperBound 세 가지가 나온다. target이 현재 사용 패턴에 맞는 권장값이고, upperBound는 limit 산정에 쓰인다.
Initial 모드 — 신규 파드에만 적용
updateMode: "Initial"은 새로 생성되는 파드에만 추천값을 적용한다. 기존 실행 중인 파드는 안 건드린다. 배포 주기가 잦은 서비스라면 Initial만으로도 효과가 크다. 우리는 하루 평균 4번 배포하기 때문에 Initial 모드로 충분했다.
Auto/Recreate 모드 — 실행 중인 파드도 재시작
updateMode: "Auto"(현재 구현상 Recreate와 동일)는 실행 중인 파드도 evict한다. PDB(PodDisruptionBudget)와 같이 써야 안전하다. 안 그러면 한 번에 여러 파드가 죽으면서 서비스 가용성이 떨어진다.
EKS에 VPA 설치하기
EKS는 VPA가 기본으로 안 들어있다. helm으로 설치한다. fairwinds 차트가 가장 무난하다.
# helm repo 추가
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm repo update
# kube-system 네임스페이스에 설치
helm install vpa fairwinds-stable/vpa \
--namespace kube-system \
--set recommender.enabled=true \
--set updater.enabled=true \
--set admissionController.enabled=true
설치 후 세 파드가 떠야 한다.
kubectl get pods -n kube-system | grep vpa
# vpa-admission-controller-xxx 1/1 Running
# vpa-recommender-xxx 1/1 Running
# vpa-updater-xxx 1/1 Running
즉, :::tip
EKS 1.29 이상에서는 metrics-server가 기본 포함이 아니다. VPA Recommender는 metrics-server 데이터를 사용하므로 먼저 metrics-server를 설치해야 한다. kubectl top pods가 동작하는지부터 확인하면 된다.
:::
CRD가 안 깔리는 경우가 있다. fairwinds 차트는 installCRDs: true가 기본이지만, 이전 버전이 깔려 있으면 conflict가 난다. kubectl get crd | grep autoscaling.k8s.io 결과로 확인하고, 이미 깔려 있으면 차트 옵션의 CRD를 끄거나 기존 걸 삭제하고 다시 설치하는 식이 안전하다.
실제 적용 흐름과 2주 결과
따라서, 처음 일주일은 Off 모드로 돌렸다. 추천값이 안정화되기를 기다렸다. 첫날에는 target memory가 380~720Mi 사이에서 출렁였고, 5일이 지나니 540~610Mi로 좁아졌다.
# 추천값 확인
kubectl describe vpa fastapi-worker-vpa -n prod | grep -A 12 "Recommendation"
# 출력 예시
# Recommendation:
# Container Recommendations:
# Container Name: fastapi-worker
# Lower Bound:
# Cpu: 180m
# Memory: 450Mi
# Target:
# Cpu: 320m
# Memory: 580Mi
# Upper Bound:
# Cpu: 520m
# Memory: 780Mi
5일째에 Initial 모드로 전환했다. 그 다음 배포부터 새 파드는 request memory 580Mi, limit memory 780Mi로 떴다. (limit은 upperBound 기준으로 잡힌다)
| 항목 | 도입 전 (2주) | 도입 후 (2주) |
|---|---|---|
| OOMKilled 발생 | 47건 | 3건 |
| 평균 메모리 사용률 | 31% | 68% |
| 노드 수 (m5.xlarge) | 6대 | 5대 |
| 월 예상 비용 (단순 환산) | $876 | $730 |
OOMKilled 3건은 평소 패턴에 없던 큰 페이로드 요청 때문이었다. 그건 페이로드 크기 제한 미들웨어로 따로 처리했다. VPA가 메모리 패턴을 학습해도, 통계 분포의 꼬리에 있는 이상값까지 잡아주지는 못한다.
Auto 모드를 끝까지 안 쓴 이유
Initial 모드로도 효과를 봤지만, 한 번은 Auto 모드를 시도해봤다. 결과는 좋지 않았다. Auto는 파드를 evict해서 재시작시키는데, FastAPI 워커가 startup에 평균 22초 걸렸다. import가 무겁고 ML 모델 한 개를 메모리에 올리는 워커였다. PDB로 minAvailable: 80%를 걸어둬도, 추천값이 자주 바뀌면 evict가 연쇄적으로 일어났다.
특히 트래픽 패턴이 일정하지 않은 시간대에 Updater가 공격적으로 evict하면서 응답 지연이 늘었다. p99 latency가 평소 180ms에서 410ms까지 튀었다.
In-place 리소스 조정(KEP-1287)이 안정화되면 이 문제는 풀릴 가능성이 있어 보인다. Kubernetes 1.33부터 InPlacePodVerticalScaling이 베타로 올라왔지만, VPA가 이 기능을 정식으로 활용하려면 시간이 더 필요해 보인다. 현재는 Initial 모드로 운영 중인 상태로 두고 있다.
흔히 만나는 함정 몇 가지
HPA와 VPA를 같은 메트릭(CPU 또는 memory)으로 동시에 걸면 충돌한다. HPA가 파드 수를 늘리려는데 VPA가 파드를 evict해서 재시작하는 식이다. 같이 쓰려면 HPA는 외부 메트릭(예: 요청 큐 길이), VPA는 CPU/memory를 잡도록 분리하는 게 안전하다.
# 충돌을 피하기 위한 분리 예시
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fastapi-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fastapi-worker
minReplicas: 4
maxReplicas: 20
metrics:
- type: External # HPA는 외부 메트릭만
external:
metric:
name: queue_depth
target:
type: AverageValue
averageValue: "50"
특히, 두 번째 함정은 JVM 같은 런타임이다. 컨테이너 메모리 limit을 자동으로 인식하지 못하는 옵션으로 돌아갈 때가 있다. 이때 VPA가 limit을 낮춰버리면 JVM이 OS 메모리 기준으로 힙을 잡고 OOM이 더 자주 난다. 우리 워크로드는 Python이라 이 이슈는 없었지만, JVM 서비스에 VPA 붙일 거면 -XX:+UseContainerSupport와 -XX:MaxRAMPercentage 설정을 먼저 확인하면 된다.
한 가지 더 — minAllowed/maxAllowed는 반드시 명시한다. 안 정해두면 추천값이 의외의 방향으로 흐른다. 우리는 memory minAllowed를 256Mi로 잡았는데, 트래픽이 거의 없는 시간대에 추천값이 180Mi까지 떨어진 적이 있다. 트래픽이 다시 올라올 때 OOM 위험이 있어서 하한을 둔 게 도움이 됐다.
DaemonSet에는 VPA를 붙이지 않는 게 좋다. 노드마다 한 개씩 도는 워크로드라 evict가 일어나면 노드 단위로 영향이 생긴다. fairwinds 차트의 vpaRecommender도 자기 자신에게는 VPA를 안 거는 게 기본 동작이다.
지금 바로 해볼 수 있는 것
따라서, 운영 중인 EKS 클러스터에 VPA가 없다면 다음 순서로 시작해보면 된다.
- metrics-server 설치 확인.
kubectl top pods가 동작하는지부터 본다. - fairwinds VPA 차트를
kube-system에 설치한다. - OOMKilled가 가장 잦은 워크로드 하나만 골라
updateMode: Off로 VPA를 붙인다. - 5~7일 추천값이 안정화되기를 기다린다. 너무 짧게 보면 추천값이 튄다.
- 배포 주기가 잦으면 Initial 모드로 전환하고, 아니면 추천값을 deployment 매니페스트에 수동으로 반영한다.
다만 Auto 모드는 in-place 리소스 조정이 GA가 되기 전까지는 더 지켜봐야 한다.
관련 글
- EKS KEDA 설치 설정 회고: SQS·Kafka 트리거로 오토스케일링 다시 설계한 3개월 – HPA만으로 버티던 EKS 워커 클러스터에 KEDA를 도입한 3개월의 기록이다. SQS와 Kafka Lag 기반으로 스케일링을 다시 설계했…
- EKS 비용 최적화 실전: Karpenter, Spot, 리소스 요청 3단계 비교 – EKS 월 청구서가 세 배 가까이 뛴 뒤 Karpenter 전환, Spot 혼합, 리소스 요청 튜닝을 3단계로 적용해 약 40% 줄인 기록…
- 쿠버네티스 비용 최적화 방법 — Spot·HPA·Karpenter로 월 33% 절감한 과정 – EKS 클러스터 월 비용이 $4,200을 찍었다. Spot 인스턴스를 성급하게 적용했다가 40분 장애를 겪고, Karpenter와 HPA를…