VPA와 LimitRange가 충돌할 때 실제로 무슨 일이 벌어지는가
프로덕션 네임스페이스에 VPA를 붙이고 며칠이 지났는데 갑자기 OOMKilled 알람이 쏟아졌던 경험, 저도 있습니다. 처음엔 메모리 릭을 의심했는데, 범인은 VPA와 LimitRange 사이에서 조용히 일어나고 있던 리소스 클리핑이었습니다. 어플리케이션 코드는 전혀 건드리지 않았는데 왜 Pod가 죽는지 한참을 뒤졌던 기억이 납니다.
VPA는 메트릭 기반으로 requests/limits를 자동 조정하고, LimitRange는 네임스페이스 단위 리소스 정책을 강제합니다. 두 오브젝트가 같은 네임스페이스에 공존하는 순간, 이 둘은 서로를 인지하지 못한 채 각자의 규칙을 관철하려 합니다. 문제는 이 충돌이 명확한 에러가 아니라 조용한 클리핑으로 나타난다는 점입니다. kubectl describe pod 출력에서 events를 꼼꼼히 읽지 않으면 넘어가기 쉽습니다.
이 글에서는 충돌이 일어나는 정확한 메커니즘, 실제 프로덕션에서 마주치는 시나리오별 원인과 해결 방법, 그리고 minAllowed·maxAllowed·LimitRange max를 어떤 순서로 산출해야 안전한지를 구체적인 수치 예시와 함께 풀어드립니다.
VPA가 LimitRange를 만났을 때 내부에서 일어나는 일
VPA의 세 컴포넌트와 역할 분담
VPA는 단일 컨트롤러가 아니라 세 컴포넌트의 협력으로 동작합니다.
Recommender는 기본적으로 Kubernetes Metrics API(metrics-server)로부터 CPU/메모리 사용량을 수집합니다. Prometheus를 직접 소스로 쓰려면 Prometheus Adapter 같은 별도 어댑터 설정이 필요합니다. 히스토그램을 분석해서 권고값을 VPA 오브젝트 status에 기록하면, Updater가 현재 Pod의 리소스 설정이 권고와 충분히 차이 나는지 판단해 evict합니다. 그리고 새 Pod가 뜰 때 Admission Controller(webhook)가 권고값을 resources 필드에 주입합니다.
LimitRange가 작동하는 지점이 바로 이 마지막 단계입니다. Admission Controller가 권고값을 주입한 뒤, API Server의 LimitRange admission plugin이 그 값을 검증하고 필요하면 클리핑합니다.
LimitRange가 강제하는 세 가지 제약
apiVersion: v1
kind: LimitRange
metadata:
name: prod-limits
namespace: production
spec:
limits:
- type: Container
min:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "4Gi"
maxLimitRequestRatio:
cpu: "4"
memory: "2"min/max: 컨테이너가 가질 수 있는 requests·limits의 절대 범위maxLimitRequestRatio:limits / requests비율의 상한. 위 예시에서 메모리maxLimitRequestRatio: 2는 limits가 requests의 2배를 넘을 수 없다는 의미입니다.
충돌의 세 가지 경로
처음엔 VPA가 추천한 값이 LimitRange max보다 크면 max로 잘리는 거 아닌가 하고 단순하게 생각했는데, maxLimitRequestRatio가 끼어들면 연쇄 클리핑이 발생합니다. requests가 올라가면 limits도 비례해서 올라가야 하는데, limits가 max에 막히면 requests까지 역으로 내려가는 구조입니다.
실전 시나리오별 원인과 해결 방법
시나리오 1: LimitRange max 초과로 OOMKilled 반복
상황: memory max 4Gi LimitRange + maxLimitRequestRatio: 2 설정 상태에서 VPA가 6Gi를 권고.
결과적으로 requests가 2Gi까지 내려가고, 실제 애플리케이션이 3~4Gi를 쓰니 OOMKilled가 반복됩니다.
해결: LimitRange max를 VPA maxAllowed와 maxLimitRequestRatio를 함께 고려해서 역산해야 합니다.
LimitRange max(memory) ≥ VPA maxAllowed(memory) × maxLimitRequestRatio(memory)더 실용적으로는, VPA maxAllowed를 먼저 정하고 LimitRange max가 위 식을 만족하도록 충분히 크게 설정하는 것이 안전합니다. 아래처럼 VPA maxAllowed를 LimitRange max / ratio 이하로 명시적으로 제한하면 클리핑 자체를 방지할 수 있습니다.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "InPlaceOrRecreate"
resourcePolicy:
containerPolicies:
- containerName: app
minAllowed:
cpu: "100m"
memory: "256Mi"
maxAllowed:
cpu: "2"
memory: "3Gi"
controlledValues: RequestsAndLimits시나리오 2: HPA와 VPA CPU 충돌
CPU 기반 HPA와 controlledValues: RequestsAndLimits VPA를 함께 쓰면 골치 아픈 상황이 생깁니다. VPA가 CPU requests를 올리면 limits도 같이 올라가고, HPA가 보는 utilization 분모(requests 합계)가 변해서 예상치 못한 scale-down이 발생합니다.
spec:
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: ["memory"]
controlledValues: RequestsAndLimits
minAllowed:
memory: "256Mi"
maxAllowed:
memory: "3Gi"CPU는 HPA가 담당하고, 메모리만 VPA가 조정하도록 역할을 나누는 패턴이 Kubernetes 공식 문서에서도 권장됩니다. CPU에 controlledValues: RequestsOnly를 쓰더라도 HPA와 함께라면 controlledResources에서 CPU를 아예 빼는 편이 더 안전합니다.
시나리오 3: ResourceQuota 초과로 Pod 생성 실패
VPA Admission Controller는 ResourceQuota를 체크하지 않습니다. 이건 구조적 한계입니다(GitHub Issue #8401). VPA가 limits를 올렸을 때 네임스페이스 전체 limits.memory quota를 넘으면 Pod 생성이 실패합니다.
방어적 설정 방법(개념적 예시):
VPA maxAllowed ≤ (ResourceQuota limits.memory) / 예상 최대 Pod 수 × 안전 마진예를 들어 quota가 20Gi이고 Pod가 최대 8개, 안전 마진 0.8을 잡는다면 20Gi / 8 × 0.8 = 2Gi를 maxAllowed 상한으로 잡는 식입니다. 안전 마진 값 자체는 워크로드의 변동성과 롤아웃 중 임시 초과분(surge)을 감안해 조정할 여지가 있습니다.
시나리오 4: 권고 없이 Recreate 즉시 활성화 → 비즈니스 시간대 대규모 eviction
팀에서 dry-run 없이 Recreate 모드를 바로 켰다가 낮 시간에 Pod가 줄줄이 evict된 사례가 있습니다.
Off 모드에서 최소 수일~1주일은 권고값 패턴을 지켜본 다음, minAllowed·maxAllowed를 조정하고 나서 업데이트 모드를 활성화하는 순서가 안전합니다.
권장 설정값 산출 방법
단계별 계산 절차
1단계 - 베이스라인 수집 (Prometheus 쿼리 예시)
container_memory_working_set_bytes는 gauge 지표이므로 rate() + histogram_quantile() 조합은 부적절합니다. gauge에서 분위수를 뽑을 때는 quantile_over_time을 씁니다.
# 최근 7일 메모리 P50
quantile_over_time(0.50,
container_memory_working_set_bytes{namespace="production", container="app"}[7d]
)
# 최근 7일 메모리 P99
quantile_over_time(0.99,
container_memory_working_set_bytes{namespace="production", container="app"}[7d]
)
# CPU는 rate가 적절 (counter 기반)
quantile_over_time(0.99,
rate(container_cpu_usage_seconds_total{namespace="production", container="app"}[5m])[7d:5m]
)2단계 - minAllowed / maxAllowed 산출 (개념적 예시)
아래 배수는 공식 문서에 명시된 값이 아니라, Off 모드로 관측한 뒤 조정하기 위한 출발점 예시입니다. 워크로드마다 스파이크 폭이 다르므로 실제 운영값은 관찰 후 튜닝해야 합니다.
| 파라미터 | 계산식(예시) | 비고 |
|---|---|---|
메모리 minAllowed |
7일 P50 × 1.2 | 저트래픽 시간대 여유 마진 |
메모리 maxAllowed |
7일 P99 × 1.5 | LimitRange max / ratio 이하 |
CPU minAllowed |
7일 P50 × 0.8 | 지나친 하향 시 스로틀 위험 있음 |
CPU maxAllowed |
7일 P99 × 2.0 | HPA 트리거 임계값과 겹치지 않도록 |
CPU minAllowed를 너무 낮게(예: P10 이하) 잡으면 저트래픽 시간에도 스로틀이 발생해 응답 지연이 늘어날 수 있습니다. P50 근처에서 시작해 스로틀 메트릭(container_cpu_cfs_throttled_periods_total)을 보며 하향 조정하는 편이 안전합니다.
3단계 - LimitRange max 역산
LimitRange max(memory) ≥ VPA maxAllowed(memory) × maxLimitRequestRatio(memory)예시: maxAllowed 3Gi, maxLimitRequestRatio 2이면 LimitRange max는 최소 6Gi 이상이어야 합니다. 이 계산을 놓치면 앞서 본 연쇄 클리핑이 발생합니다.
완성된 설정 예시
HPA가 CPU를 담당하므로 CPU requests/limits는 Deployment manifest에서 명시적으로 고정하고, VPA는 메모리만 조정하도록 합니다. LimitRange의 CPU max는 HPA로 확장될 최대 replica 시나리오와 노드 스펙을 고려해 잡습니다.
apiVersion: v1
kind: LimitRange
metadata:
name: production-limits
namespace: production
spec:
limits:
- type: Container
min:
cpu: "50m"
memory: "128Mi"
max:
cpu: "8"
memory: "8Gi" # VPA maxAllowed 3Gi × ratio 2 = 6Gi 이상
maxLimitRequestRatio:
cpu: "4"
memory: "2"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
template:
spec:
containers:
- name: app
resources:
requests:
cpu: "500m" # HPA가 이 값을 기준으로 utilization 계산
memory: "512Mi" # VPA가 조정
limits:
cpu: "1" # HPA 사용 시 requests와 함께 명시 고정
memory: "1Gi" # VPA가 ratio 비율 유지하며 조정
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "InPlaceOrRecreate"
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: ["memory"] # CPU는 HPA 담당
controlledValues: RequestsAndLimits
minAllowed:
memory: "512Mi"
maxAllowed:
memory: "3Gi"트레이드오프와 흔한 실수
controlledValues 선택의 트레이드오프
| 모드 | 동작 | 적합한 상황 | 주의점 |
|---|---|---|---|
RequestsOnly |
requests만 조정, limits 원본 유지 | CPU + HPA 병용, limits를 명시적으로 고정하고 싶을 때 | limits 과잉 설정 시 노드 리소스 낭비 |
RequestsAndLimits |
requests·limits를 원래 비율로 동시 조정 | 메모리 전용 VPA, limits 비율을 유지하고 싶을 때 | HPA CPU와 함께 쓰면 충돌 위험 |
업데이트 모드 선택
| 모드 | 재시작 여부 | 적합한 워크로드 | 비고 |
|---|---|---|---|
Off |
없음 | 초기 관찰 단계 | 권고만 생성 |
Initial |
최초 생성 시만 | 재시작 비용이 큰 워크로드 | 기존 Pod는 변경 없음 |
Recreate |
있음 (eviction) | Stateless 워크로드 | 기존 기본값 |
InPlaceOrRecreate |
조건부 없음* | Stateful 포함 워크로드 | Kubernetes 1.35+ (참고) |
*InPlaceOrRecreate는 kubelet이 in-place resize를 수용할 수 있는 범위 안에서는 재시작 없이 리소스를 조정하고, 그 범위를 벗어나거나 in-place가 실패하면 Recreate 방식(eviction 후 재생성)으로 폴백합니다.
Auto 모드는 2026년 9월 기준 Kubernetes 공식 문서에서 "현재는 Recreate와 동일하게 동작하며 향후 in-place 업데이트로 전환될 수 있다"고 안내됩니다. 의도를 명확히 하려면 Recreate 또는 InPlaceOrRecreate처럼 실제 동작을 직접 지정하는 편이 오해가 적습니다.
minAllowed 설정의 함정
너무 높게 잡으면 저트래픽 시간대에 리소스를 낭비하고, 너무 낮게 잡으면 정상 운영 중 성능 저하가 생깁니다. VPA의 권고값 신뢰도가 올라가려면 수일에서 1주일 이상의 메트릭 히스토리가 필요하기 때문에, 처음부터 완벽한 값을 찾으려 하지 말고 Off 모드로 데이터를 모은 다음 수렴해나가는 방식이 현실적입니다.
Goldilocks(Fairwinds)를 네임스페이스에 붙여두면 VPA Off 모드 권고값을 웹 UI로 시각화해줘서 초기 minAllowed/maxAllowed 값을 뽑아내기 편합니다.
마무리: 운영 진단 커맨드
이 주제는 결국 "적용한 값이 실제로 어떻게 클리핑되고 있는지" 눈으로 확인할 수 있어야 문제가 잡힙니다. 아래 커맨드는 문제 발생 시 가장 먼저 확인하면 좋은 지점들입니다.
# VPA가 실제로 어떤 권고값을 만들고 있는지
kubectl describe vpa my-app-vpa -n production
# Admission Controller가 주입한 최종 requests/limits 확인
kubectl get pod <pod-name> -n production -o jsonpath='{.spec.containers[*].resources}'
# LimitRange가 적용된 후의 이벤트 (클리핑 흔적)
kubectl describe pod <pod-name> -n production | grep -A5 Events
# 네임스페이스 quota와 실사용량 대조
kubectl describe resourcequota -n production
# CPU 스로틀 발생 여부 (minAllowed 검증)
kubectl exec <pod-name> -n production -- cat /sys/fs/cgroup/cpu.statVPA는 강력하지만, LimitRange·ResourceQuota·HPA와 조합할 때는 각 레이어가 서로를 어떻게 제약하는지 이해하고 설정해야 의도대로 동작합니다. 가장 안전한 시작 방법은 Off 모드로 데이터를 쌓고, 위의 역산 공식으로 경계값을 뽑고, 진단 커맨드로 실제 주입값을 검증한 뒤 점진적으로 업데이트 모드를 활성화하는 순서입니다.
참고 자료
- Vertical Pod Autoscaling | Kubernetes 공식 문서
- Limit Ranges | Kubernetes 공식 문서
- Resource Quotas | Kubernetes 공식 문서
- Kubernetes VPA: Architecture, Limitations, and Production Best Practices – ScaleOps
- K8s VPA: Limitations, Best Practices, and the Future of Pod Rightsizing – CloudPilot AI
- Vertical Pod Autoscaling: The Definitive Guide – Povilas Versockas
- Why Kubernetes 1.35 is a game-changer for stateful workload scaling – The New Stack
- VPA Doesn't respect the minAllowed and maxAllowed · Issue #4763 – GitHub
- A new parameter for the management of the limits that the VPA assigns to the new Pod · Issue #7790 – GitHub
- VPA: Pod memory limit exceeds recommendation and namespace quotas · Issue #8401 – GitHub
- Vertical Pod Autoscaling in Azure Kubernetes Service (AKS) – Microsoft Learn
- Goldilocks – FairwindsOps GitHub