# VPA 권고로 request를 정할 때 - 권고가 맞는 경우와 틀리는 경우
파드의 requests를 정하는 일은 대체로 근거가 약합니다. 처음에는 넉넉하게 잡고, 그 값이 몇 년 동안 아무도 건드리지 않은 채 남습니다. 노드 사용률이 30%인데 스케줄러는 자리가 없다고 하는 상태가 여기서 만들어집니다.
VPA는 실제 사용량을 관찰해 권고값을 계산합니다. Goldilocks는 그 권고를 네임스페이스 단위로 모아 보여 줍니다. 도구를 붙이면 숫자가 나오는데, 그 숫자를 그대로 적용하면 안 되는 경우가 생각보다 많습니다. 이 글에서는 권고가 어떻게 만들어지는지, 어디서 틀리는지, 그리고 자동 적용을 켜면 무엇이 일어나는지 정리합니다.
# 1. VPA의 세 조각과 Goldilocks의 위치
VPA는 세 컴포넌트로 나뉩니다.
- recommender: 사용량 이력을 보고 권고값을 계산합니다.
- updater: 현재 값이 권고와 다른 파드를 축출합니다.
- admission controller: 파드가 생성될 때 권고값을 주입합니다.
VPA가 어디까지 동작할지는 updateMode가 정합니다. Off면 recommender만 돌고 권고만 기록됩니다. Auto나 Recreate면 updater가 실제로 파드를 갈아 끼웁니다.
Goldilocks가 하는 일은 단순합니다. 라벨이 붙은 네임스페이스의 워크로드마다 updateMode: Off인 VPA 오브젝트를 자동 생성하고, 그 권고를 대시보드로 모아 보여 줍니다. 즉 Goldilocks는 관측 도구이지 적용 도구가 아닙니다. 이 구분이 이 글의 출발점입니다.
# Goldilocks가 만드는 것과 같은 형태 - 권고만 계산한다
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
updatePolicy:
updateMode: "Off"
# 2. 자동 적용을 켜면 무엇이 일어나는가
Auto가 매력적으로 보이지만, 문서의 알려진 제약을 읽으면 켜기 전에 알아야 할 것이 여럿입니다.
Whenever VPA updates the pod resources, the pod is recreated, which causes all running containers to be recreated.
Auto/Recreate 모드에서는 권고를 적용하려면 파드를 다시 만들어야 합니다. 오랫동안 컨테이너 리소스는 실행 중인 파드에서 바꿀 수 없는 필드였기 때문입니다. 지금은 사정이 달라져서, 쿠버네티스의 in-place 파드 리사이즈(InPlacePodVerticalScaling)가 v1.27 알파, v1.33 베타(기본 활성), v1.35 GA로 들어왔고, VPA도 InPlaceOrRecreate 모드(VPA v1.4.0 알파, v1.5.0 베타, v1.6.0 GA)로 먼저 재생성 없이 값을 바꾸고 실패하면 재생성으로 넘어갑니다. 반면 Recreate와, 현재 Recreate와 같게 동작하며 deprecated된 Auto에서는 VPA가 값을 조정할 때마다 재시작이 일어납니다. 상태를 들고 있거나 시작이 느린 워크로드에는 그 자체가 비용입니다.
VPA cannot guarantee that pods it evicts or deletes (when configured in
AutoandRecreatemodes) will be successfully recreated.
축출한 파드가 다시 뜬다는 보장이 없습니다. 그리고 못 뜨는 가장 흔한 이유가 바로 다음 항목입니다.
VPA recommendation might exceed available resources (e.g. Node size, available size, available quota) and cause pods to go pending.
권고값이 노드 크기나 쿼터를 넘으면 파드가 영원히 Pending에 머뭅니다. 순서를 정리하면 이렇습니다. 사용량이 늘어 권고가 올라가고, updater가 파드를 축출하고, 새 파드는 커진 request 때문에 스케줄되지 못합니다. 부하가 높아진 시점에 워크로드가 사라지는 결과입니다.
여기에 조건이 하나 더 붙습니다. VPA는 축출로 값을 바꾸므로 PodDisruptionBudget을 지켜야 하고, 레플리카가 하나뿐이면 축출할 수 없거나 축출하는 순간 서비스가 끊깁니다. 자동 모드는 레플리카가 여럿인 무상태 워크로드에서만 고려할 만합니다.
기본 운영 방식을 정하면 이렇습니다.
| 워크로드 | 모드 | 이유 |
|---|---|---|
| 상태 있음, 단일 레플리카 | Off | 축출이 곧 중단 |
| 시작이 느림 (JVM, 모델 로딩) | Off | 재시작 비용이 큼 |
| 무상태, 레플리카 다수 | Auto 검토 | 축출을 흡수 가능 |
| 배치 잡 | Initial | 생성 시 주입, 도중 축출 없음 |
Off로 두고 권고를 사람이 Git에 반영하는 것이 기본값이어야 합니다. 권고를 읽는 것과 적용하는 것을 분리하면 3~5절의 문제가 배포 전에 걸러집니다.
# 3. HPA와 같은 지표를 두고 싸운다
Vertical Pod Autoscaler should not be used with the Horizontal Pod Autoscaler (HPA) on the same resource metric (CPU or memory)
이유는 두 컨트롤러가 같은 값을 반대 방향으로 밀기 때문입니다. HPA의 CPU 기반 스케일링은 request 대비 사용률을 봅니다. 여기서 순환이 생깁니다.
CPU 사용률이 높다
→ HPA가 레플리카를 늘린다
→ 레플리카당 부하가 줄어 실제 사용량이 준다
→ VPA가 "request가 과하다"고 판단해 낮춘다
→ request가 낮아지니 사용률(사용량/request)이 다시 올라간다
→ HPA가 또 늘린다
두 컨트롤러가 서로의 입력을 바꾸는 구조라 안정점이 없습니다. 한 리소스에는 한 컨트롤러만 두어야 합니다. 실무적인 조합은 이렇습니다.
- CPU는 HPA가 담당, 메모리는 VPA 권고를 사람이 반영
- 또는 둘 다 사람이 정하고 VPA는
Off로 관측만
커스텀 지표(큐 길이, 초당 요청 수)로 HPA를 돌리면 이 충돌이 사라집니다. 사용률 기반이 아니므로 VPA가 request를 바꿔도 HPA의 판단이 흔들리지 않습니다. HPA를 사용률이 아닌 지표로 옮기는 것이 근본 해법에 가깝습니다.
# 4. CPU와 메모리는 다르게 다뤄야 한다
권고값은 CPU와 메모리에 똑같은 형식으로 나오지만, 두 리소스의 성질이 다릅니다.
| CPU | 메모리 | |
|---|---|---|
| 성격 | 압축 가능 | 압축 불가 |
| limit 초과 시 | 스로틀 (느려짐) | OOM kill (죽음) |
| 과소 설정의 결과 | 지연 증가 | 프로세스 종료 |
| 권장 | request만, limit은 신중히 | request = limit |
메모리는 넘으면 죽으므로 여유가 곧 안전 마진입니다. CPU는 넘으면 느려질 뿐이라 여유가 성능 마진입니다. 같은 권고 숫자를 받아도 어느 쪽에 얼마의 여유를 더할지는 다르게 판단해야 합니다.
메모리를 request = limit으로 두는 이유와 그것이 노드 안정성에 어떤 영향을 주는지는 메모리 request=limit이 노드를 지키는 이유 (opens new window)에 정리했습니다. VPA 권고를 반영할 때도 이 원칙은 그대로입니다. 권고의 상한값을 메모리 limit으로 쓰고 request도 같게 맞추는 것이 안전합니다.
VPA 권고는 보통 세 값으로 나옵니다. 하한, 목표, 상한입니다. 목표값만 보고 반영하면 피크를 못 견디고, 상한값을 CPU request로 그대로 쓰면 낭비가 큽니다. 실용적인 조합은 이렇습니다.
- CPU request: 목표값 근처
- CPU limit: 설정하지 않거나 넉넉하게
- 메모리 request/limit: 상한값에 여유를 더해 동일하게
# 5. 권고가 틀리는 경우
여기가 도구를 쓰는 사람이 판단해야 하는 부분입니다. 권고는 관측된 사용량의 함수이므로, 관측이 실제 필요량과 다르면 권고도 틀립니다.
관측 기간에 피크가 없었던 경우. 월말 정산이나 주간 배치처럼 드물게 부하가 몰리는 워크로드는, 관측 창이 그 주기보다 짧으면 피크를 못 봅니다. 권고를 그대로 적용하면 다음 피크에서 OOM이 납니다. 주기가 있는 워크로드는 최소 두 주기 이상 관측해야 합니다.
트래픽이 없던 기간이 섞인 경우. 신규 서비스나 장애 복구 직후처럼 실제 부하가 없던 구간이 이력에 들어가면 권고가 낮게 나옵니다. 도입 직후의 권고를 믿으면 안 되는 이유입니다.
런타임이 limit에 반응해 메모리를 잡는 경우. 이것이 가장 헷갈리는 경우입니다. JVM의 기본 힙 상한은 컨테이너 메모리 limit에 비례해 정해집니다(MaxRAMPercentage 기본 25%). Go의 GC는 GOMEMLIMIT을 설정하지 않으면 컨테이너 limit을 알지 못하고 GOGC 기준으로만 힙 목표를 잡으므로, GOMEMLIMIT을 limit에 맞춰 준 경우에만 여기에 해당합니다. limit을 크게 주면 런타임이 더 많이 쓰고, 그러면 관측된 사용량이 늘고, 권고가 다시 올라갑니다. 관측이 자기 자신을 근거로 삼는 순환입니다.
반대 방향도 성립합니다. limit을 줄이면 GC가 더 자주 돌아 사용량이 줄어들고, 권고가 더 낮아집니다. 이때 실제로는 CPU를 더 쓰고 지연이 늘어나는데, 메모리 권고만 보면 개선된 것처럼 보입니다. 런타임 옵션(힙 상한, GC 목표)을 컨테이너 limit에 맞춰 명시적으로 고정해 두어야 권고가 의미가 있습니다.
여러 VPA가 겹치는 경우.
Multiple VPA resources matching the same pod have undefined behavior.
Goldilocks가 네임스페이스에 VPA를 자동 생성하는 상태에서 같은 워크로드에 수동 VPA를 만들면 이 조건에 걸립니다. 권고 숫자가 오락가락하면 겹친 VPA가 없는지부터 봐야 합니다.
# 6. 운영 절차
정리하면 이런 흐름이 됩니다.
Off모드로 관측을 시작한다. Goldilocks를 쓰면 네임스페이스에 라벨만 붙이면 됩니다.- 주기의 두 배 이상 기다린다. 배치가 있으면 그 주기 기준입니다.
- 권고를 읽되 4절의 규칙으로 변환한다. 목표값과 상한값을 리소스 성격에 맞게 나눕니다.
- Git에 반영하고 배포한다. 자동 적용이 아니라 리뷰를 거칩니다.
- 다시 관측한다. 값이 바뀌면 사용량도 바뀝니다(5절의 순환). 한 번으로 끝나지 않습니다.
4번에서 한 번에 다 바꾸지 않는 것도 중요합니다. request를 줄이는 변경은 노드 배치를 바꿉니다. 여러 워크로드의 request를 동시에 줄이면 스케줄러가 파드를 더 촘촘히 배치하고, 그 상태에서 하나가 튀면 이웃까지 영향을 받습니다. 몇 개씩 나눠 적용하고 노드 상태를 확인하는 편이 안전합니다.
# 7. 직접 확인하는 방법
권고값을 보는 것부터 시작합니다.
# 권고 요약 - 컨테이너별 하한/목표/상한
kubectl get vpa -A -o custom-columns=\
'NS:.metadata.namespace,NAME:.metadata.name,MODE:.spec.updatePolicy.updateMode,'\
'CPU:.status.recommendation.containerRecommendations[0].target.cpu,'\
'MEM:.status.recommendation.containerRecommendations[0].target.memory'
# 상세 - lowerBound / target / upperBound 전부
kubectl get vpa myapp -o jsonpath='{.status.recommendation.containerRecommendations}' | jq
권고와 현재 값의 차이를 계산하면 우선순위가 나옵니다.
# 현재 request와 권고를 나란히 본다
kubectl get vpa myapp -o json | jq -r '
.status.recommendation.containerRecommendations[] |
"\(.containerName) target cpu=\(.target.cpu) mem=\(.target.memory) upper mem=\(.upperBound.memory)"'
kubectl get deploy myapp -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{" req="}{.resources.requests}{"\n"}{end}'
관측 기간이 충분한지도 확인해야 합니다. VPA 오브젝트의 생성 시각이 관측 시작점입니다.
kubectl get vpa -A -o custom-columns='NAME:.metadata.name,AGE:.metadata.creationTimestamp'
적용 후에는 실제로 개선됐는지 노드 관점에서 봅니다.
# 노드별 request 대비 실사용 - 이 간극이 낭비의 크기다
kubectl describe node <node> | grep -A6 'Allocated resources'
kubectl top nodes
Allocated resources의 request 비율과 kubectl top의 실사용 비율 차이가 줄어들었다면 효과가 있는 것입니다. 두 값이 여전히 크게 벌어져 있으면 아직 과대 설정이 남아 있습니다.
겹친 VPA는 이렇게 찾습니다.
# 같은 targetRef를 가리키는 VPA가 둘 이상인지
kubectl get vpa -A -o json | jq -r '.items[] |
"\(.spec.targetRef.kind)/\(.spec.targetRef.name)@\(.metadata.namespace)"' | sort | uniq -d
# 8. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| 파드가 계속 재시작 | Auto 모드의 축출 | Off로 바꾸고 수동 반영 |
| 부하가 오르자 파드가 Pending | 권고가 노드 크기 초과 | 상한 설정, 노드 타입 검토 |
| 레플리카 수가 요동 | HPA와 VPA가 같은 지표 | 지표 분리 또는 커스텀 지표 HPA |
| 권고 적용 후 OOM | 관측 기간에 피크가 없었음 | 주기의 두 배 이상 관측 |
| 권고가 계속 올라감 | 런타임이 limit에 맞춰 사용 | 힙·GC 옵션을 limit에 고정 |
| 권고가 오락가락함 | 여러 VPA가 같은 파드 매칭 | 중복 VPA 정리 |
| 도입 직후 권고가 너무 낮음 | 무부하 구간이 이력에 포함 | 충분히 기다린 뒤 판단 |
| request를 줄였더니 이웃이 불안정 | 배치가 촘촘해짐 | 나눠서 적용, 노드 상태 확인 |
# 9. 마무리
- Goldilocks는 관측 도구입니다. VPA를
Off로 깔아 권고만 모읍니다. 적용은 별개의 결정입니다. - 자동 모드는 파드를 다시 만들어야 값을 바꿉니다. 그리고 축출된 파드가 다시 뜬다는 보장이 없습니다.
- 가장 위험한 실패는 권고가 노드 크기를 넘어 파드가 Pending에 머무는 것입니다. 부하가 오른 시점에 워크로드가 사라집니다.
- HPA와 VPA를 같은 리소스에 걸면 서로의 입력을 바꾸며 발진합니다. 지표를 분리하거나 HPA를 커스텀 지표로 옮깁니다.
- CPU와 메모리는 같은 권고 숫자라도 다르게 반영합니다. 메모리는 상한 기준으로 request=limit, CPU는 목표 기준으로 request만.
- 권고는 관측의 함수입니다. 피크가 없던 기간, 무부하 구간, 그리고 limit에 반응하는 런타임이 권고를 틀리게 만듭니다. 숫자를 그대로 믿지 않는 것이 이 도구를 쓰는 방법입니다.
# 참고
- Kubernetes Autoscaler - Vertical Pod Autoscaler (opens new window)
- VPA - Known limitations (opens new window)
- Goldilocks 공식 문서 (opens new window)
- VPA - Features (In-Place Updates
InPlaceOrRecreate) (opens new window) - Kubernetes - Resize CPU and Memory Resources assigned to Containers (opens new window)
- Go - A Guide to the Go Garbage Collector (opens new window)
- Kubernetes - Resource Management for Pods and Containers (opens new window)
- Kubernetes - Horizontal Pod Autoscaling (opens new window)