K8s 메모리 request와 limit, 어떻게 설정해야 할까
메모리는 CPU와 관리 철학이 완전히 달라야 한다. 이전 탐구에서 CPU는 limits를 제거하고 requests만 정확하게 잡는 전략이 유리하다는 걸 확인했다. 메모리도 같은 전략이 통할까. OOMKilled를 마주하면 그 답이 명확해진다. 메모리는 초과하면 프로세스가 즉시 죽는다.
CPU와 메모리가 다르게 취급되는 근본 이유
Kubernetes 공식 문서는 자원을 두 가지로 분류한다.
압축 가능한 자원(Compressible Resource): CPU가 대표적이다. 사용량이 한도에 도달하면 커널은 파드를 죽이는 대신 CPU 시간을 덜 준다(Throttling). 파드는 느려질지언정 죽지는 않는다.
압축 불가능한 자원(Incompressible Resource): 메모리가 대표적이다. 메모리는 이미 데이터가 저장되어 있어 “압축”해서 공간을 만들 수 없다. 파드가 한도를 초과하거나 노드 메모리가 가득 차면, 커널은 공간을 확보하기 위해 프로세스를 강제 종료(OOM Kill) 해야만 한다.
이 특성이 설정 전략의 차이를 만든다. CPU는 limits를 풀어서 유연하게 쓰고, 메모리는 limits를 걸어서 생존을 보장하는 전략이 유효하다.
노드 메모리 부족 시 누가 먼저 죽는가
노드 메모리 부족(Node Pressure)이 오면 Linux 커널의 OOM Killer가 활동을 시작한다. 누구를 죽일지 결정하는 기준이 oom_score_adj 값이다. Kubernetes는 파드의 requests와 limits 설정에 따라 QoS(Quality of Service) 클래스를 부여하고, 이에 맞춰 점수를 매긴다. 점수가 높을수록 먼저 죽는다(-1000 ~ 1000 범위).
| QoS Class | 조건 | oom_score_adj | 생존 우선순위 |
|---|---|---|---|
| Guaranteed | 모든 컨테이너의 request == limit | -997 | 최상 |
| Burstable | request < limit 또는 limit 미설정 | 2 ~ 999 | 중간 |
| BestEffort | request, limit 모두 미설정 | 1000 | 최하 |
Guaranteed(-997)는 시스템 데몬 등을 제외하면 사실상 면책특권을 가진다. BestEffort(1000)는 메모리가 부족하면 가장 먼저 희생된다. Burstable이 복잡하다. 공식은 다음과 같다.
min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
쉽게 말해, request보다 더 많이 쓸수록(limit에 가까워질수록) 점수가 높아져 죽을 확률이 올라간다.
graph TD Node[Node Memory Pressure] -->|Trigger OOM Killer| CheckScore CheckScore{Check oom_score_adj} CheckScore -->|Score 1000| BE[Kill BestEffort Pods First] BE -->|Not enough?| BU[Kill Burstable Pods] BU -->|Target High Usage| BUHigh[Kill Pods using > Request] BU -->|Not enough?| GU["Kill Guaranteed Pods (Last Resort)"] style BE fill:#ffcccc,stroke:#333,stroke-width:2px style GU fill:#ccffcc,stroke:#333,stroke-width:2px
Guaranteed가 권장되는 세 가지 이유
Google(GKE)이나 AWS 등 클라우드 벤더들이 프로덕션 환경에서 request == limit (Guaranteed) 설정을 권장하는 이유는 명확하다.
첫째, 예측 가능성이다. Guaranteed 파드는 오직 자신이 설정한 limit을 넘었을 때만 죽는다. 반면 Burstable 파드는 자신이 메모리를 적게 쓰고 있어도, “옆집 파드(Noisy Neighbor)“가 메모리를 많이 써서 노드 전체가 위험해지면 억울하게 죽을 수 있다. 운영 관점에서는 “내가 뭘 잘못했는지 명확한” 자살이, “언제 죽을지 모르는” 타살보다 훨씬 낫다.
둘째, 오버커밋 위험 회피다. request는 낮게, limit은 높게 잡으면 노드에 물리적 메모리보다 더 많은 파드를 구겨 넣을 수 있다. 평시에는 비용 효율적이지만, 트래픽이 몰려 모든 파드가 동시에 limit만큼 메모리를 요구하면 대규모 연쇄 OOM이 발생해 서비스 전체 장애로 이어진다.
셋째, NUMA 아키텍처 최적화다. 고성능 워크로드의 경우, Kubernetes Memory Manager는 오직 Guaranteed 파드에 대해서만 NUMA 노드 정렬(Topology Alignment)을 보장한다. 메모리 접근 지연 시간을 최소화할 수 있다.
kubectl describe node <node-name>으로 노드의 Allocated resources를 확인해보자. Limits의 합계가 노드 물리 메모리의 100%를 훌쩍 넘고 있다면, 클러스터는 시한폭탄을 안고 있는 것일 수 있다.
프로덕션은 Guaranteed, 개발기는 Burstable
설정 전략은 환경에 따라 나눈다.
| 환경 | 전략 | 이유 |
|---|---|---|
| Production (Critical) | memory request == limit (Guaranteed) | 안정성 최우선. OOM 원인 추적 가능 |
| Dev/Staging (Non-Critical) | request < limit (Burstable) | 비용 절감. 오버커밋 허용 |
Guaranteed를 선택하면 안정성은 최대화되지만 메모리 예약(Reservation) 비용이 발생해 리소스 유휴 공간(Slack)이 생긴다. Burstable을 선택하면 리소스 효율은 좋지만 “누가 죽을지 예측하기 어려운” 불안정성을 감수해야 한다.
가끔 메모리를 2배로 쓰는 버스트 워크로드는 어떻게 해야 할까. limit만 2배로 늘리는 것은 나쁜 방법이다. 평소엔 Burstable로 동작하다가 중요할 때 OOM 우선순위가 높아져 죽을 위험이 커진다. request도 같이 늘리거나, HPA(Horizontal Pod Autoscaler) 로 파드 개수를 늘려서 부하를 분산하는 것이 올바른 방법이다.
이 탐구가 남긴 것
메모리는 타협하지 않는다. CPU와 달리 메모리는 부족하면 죽는다. 프로덕션 환경에서는 request와 limit을 동일하게 설정해 Guaranteed QoS를 받는 것이 정석이다.
OOM은 점수제다. oom_score_adj는 잔인하다. limit보다 request를 적게 잡을수록 파드는 OOM Killer의 우선 타겟이 된다. 그리고 limit보다 request를 낮게 잡은 이유가 “비용 절감”이라면, 그 비용은 장애 복구 비용으로 돌아올 수 있다.
자살이 타살보다 낫다. 파드가 죽는다면, 노드 메모리가 부족해서(타살) 죽는 것보다 내 할당량을 초과해서(자살) 죽는 것이 원인 분석과 디버깅이 훨씬 쉽다.
앞으로의 방향
프로덕션 파드를 Guaranteed로 전환하면 메모리 예약량이 늘어난다. 적정 request 값을 VPA(Vertical Pod Autoscaler)로 자동 조정하는 구성을 검토 중이다. VPA의 권장값을 기준으로 request == limit을 유지하면 Guaranteed를 지키면서도 과도한 예약을 줄일 수 있다.
남은 질문
request == limit(Guaranteed)이 권장이라면, HPA와 함께 쓸 때targetAverageUtilization기준은 어떻게 잡아야 하는가. request 자체가 최대치라면 utilization 80%는 이미 위험한 수준 아닌가.- 메모리 limit을 타이트하게 잡으면 JVM이나 Go 런타임처럼 자체 GC를 가진 언어는 어떤 영향을 받는가. 힙 크기를 컨테이너 limit 기준으로 자동 조정하는가, 아니면 별도 설정이 필요한가.
참고 자료
- Kubernetes Docs: Resource Management for Pods and Containers
- Kubernetes Docs: Configure Quality of Service for Pods
정보 공백 요약
| # | 필요한 정보 | 이유 | 위치 |
|---|---|---|---|
| 1 | 프로덕션 파드의 실제 oom_score_adj 값 확인 스크린샷 | ”QoS 클래스별 점수” 주장의 실증적 근거 | QoS 섹션 |
| 2 | Burstable → Guaranteed 전환 전후 OOMKilled 빈도 비교 | 전략 변경의 실제 효과 수치화 | 결론 섹션 |