K8s 메모리 request와 limit, 어떻게 설정해야 할까
1. 서론 (Context & Tension)
Kubernetes를 운영하다 보면 파드(Pod)가 OOMKilled로 재시작되는 현상을 흔히 겪습니다. 이때 가장 먼저 드는 의문은 이런 것들입니다.
- “노드에 메모리가 텅텅 비어 있는데 왜 내 파드는 죽었지?”
- “CPU는 Limit을 걸지 말라고 배웠는데, 메모리는 왜 Limit을 꼭 걸라고 할까?”
- “비용 아까운데
request는 낮게 잡고limit만 높게 주면 안 될까?”
이전 글 K8s Resource Limit 톺아보기에서는 CPU 쓰로틀링 문제를 다뤘다면, 이번에는 메모리(Memory) 자원의 특성과 올바른 설정 전략을 다룹니다. 결론부터 말하자면, 메모리는 CPU와 관리 철학이 완전히 달라야 합니다.
2. 문제 분해 (Problem Decomposition)
메모리 설정을 제대로 이해하기 위해 다음 세 가지 핵심 질문을 던져봅니다.
- 자원의 특성: 왜 CPU와 메모리는 다르게 취급해야 하는가? (Compressible vs Incompressible)
- 생존 우선순위: 노드 메모리가 부족할 때 커널은 누구를 먼저 죽이는가? (QoS Class & OOM Score)
- 설계 판단: 그래서
request와limit을 어떻게 설정하는 것이 최선인가? (Guaranteed vs Burstable)
3. 탐구 과정 (Investigation)
3.1. 압축 가능성(Compressibility)의 차이
가장 근본적인 차이는 자원의 성격에 있습니다. Kubernetes 공식 문서는 자원을 두 가지로 분류합니다.
- 압축 가능한 자원 (Compressible Resource): CPU가 대표적입니다. 사용량이 한도에 도달하면, 커널은 파드를 죽이는 대신 CPU 시간을 덜 줍니다(Throttling). 파드는 느려질지언정 죽지는 않습니다.
- 압축 불가능한 자원 (Incompressible Resource): 메모리가 대표적입니다. 메모리는 이미 데이터가 저장되어 있기 때문에 “압축”해서 공간을 만들 수 없습니다. 파드가 한도를 초과하거나 노드 메모리가 가득 차면, 커널은 공간을 확보하기 위해 프로세스를 강제 종료(OOM Kill) 해야만 합니다.
이러한 특성 때문에 CPU는 Limit을 풀어서 유연하게 쓰고, 메모리는 Limit을 걸어서 생존을 보장하는 전략이 유효합니다.
3.2. 누가 먼저 죽는가? (QoS 클래스와 OOM Score)
메모리 부족 상황(Node Pressure)이 오면 Linux 커널의 OOM Killer가 활동을 시작합니다. 이때 누구를 죽일지 결정하는 기준이 바로 oom_score_adj 값입니다. Kubernetes는 파드의 request와 limit 설정에 따라 QoS(Quality of Service) 클래스를 부여하고, 이에 맞춰 oom_score_adj 점수를 매깁니다.
점수가 높을수록 먼저 죽습니다. (Range: -1000 ~ 1000)
| QoS Class | 조건 | oom_score_adj | 생존 우선순위 |
|---|---|---|---|
| Guaranteed | 모든 컨테이너의 request == limit | -997 | 최상 (Platinum) |
| Burstable | request < limit 또는 limit 미설정 | 2 ~ 999 | 중간 (Gold/Silver) |
| BestEffort | request, limit 모두 미설정 | 1000 | 최하 (Bronze) |
-
Guaranteed (-997): “나는 돈 낸 만큼만 쓸 테니 건드리지 마.” 가장 마지막까지 살아남습니다. 시스템 데몬 등을 제외하면 사실상 면책특권을 가집니다.
-
BestEffort (1000): “남는 거 있으면 좀 쓸게요.” 메모리가 부족하면 가장 먼저 희생됩니다.
-
Burstable (2 ~ 999): 여기가 복잡합니다. 공식은 다음과 같습니다.
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
3.3. 왜 Request == Limit (Guaranteed)을 권장하는가?
Google(GKE)이나 AWS 등 클라우드 벤더들이 프로덕션 환경에서 request와 limit을 동일하게 설정(Guaranteed) 하라고 권장하는 이유는 명확합니다.
-
예측 가능성 (Predictability):
Guaranteed파드는 오직 자신이 설정한limit을 넘었을 때만 죽습니다. (자살)Burstable파드는 자신이 메모리를 적게 쓰고 있어도, “옆집 파드(Noisy Neighbor)“가 메모리를 많이 쓰면 노드 전체가 위험해져서 억울하게 죽을 수 있습니다. (타살)- 운영 관점에서는 “내가 뭘 잘못했는지 명확한” 자살이, “언제 죽을지 모르는” 타살보다 훨씬 낫습니다.
-
NUMA 아키텍처 최적화 (Memory Manager):
- 고성능 워크로드의 경우, Kubernetes Memory Manager는 오직
Guaranteed파드에 대해서만 NUMA 노드 정렬(Topology Alignment)을 보장합니다. 이를 통해 메모리 접근 지연 시간을 최소화할 수 있습니다.
- 고성능 워크로드의 경우, Kubernetes Memory Manager는 오직
-
오버커밋(Overcommit)의 위험 회피:
request는 낮게,limit은 높게 잡으면 노드에 물리적 메모리보다 더 많은 파드를 구겨 넣을 수 있습니다(Overcommit).- 평시에는 비용 효율적이지만, 트래픽이 몰려 모든 파드가 동시에
limit만큼 메모리를 요구하면 대규모 연쇄 OOM이 발생하여 서비스 전체 장애로 이어집니다.
4. 개념 연결 (Concept Linking)
이 문서는 다음의 핵심 개념들과 연결됩니다.
- K8s Resource Model: Request와 Limit의 기본 정의
- K8s QoS Classes: Guaranteed, Burstable, BestEffort의 상세 분류
- Linux OOM Killer: 커널 레벨에서의 메모리 회수 메커니즘
5. 결정과 트레이드오프 (Decision & Trade-offs)
전략: 프로덕션은 Guaranteed, 개발기는 Burstable
결정 (Decision):
- Production (Critical): 반드시
memory request == limit으로 설정하여 Guaranteed 등급을 확보합니다. - Dev/Staging (Non-Critical): 비용 절감을 위해
request < limit(Burstable)을 허용하여 오버커밋을 활용합니다.
트레이드오프 (Trade-offs):
- Guaranteed 선택 시: 안정성은 최대화되지만, 메모리 예약(Reservation) 비용이 발생하여 리소스 유휴 공간(Slack)이 생길 수 있습니다. (비용 증가)
- Burstable 선택 시: 리소스 효율은 좋지만, 노드 메모리 부족 시 “누가 죽을지 예측하기 어려운” 불안정성을 감수해야 합니다. (안정성 감소)
버스트(Burst) 워크로드는 어떻게?
가끔 메모리를 2배로 쓰는 작업이 있다면 어떻게 해야 할까요?
- 나쁜 방법:
limit만 2배로 늘린다. (평소엔 Burstable로 동작하다가, 중요할 때 OOM 우선순위가 높아져 죽을 위험이 큼) - 좋은 방법:
request도 같이 늘리거나, HPA (Horizontal Pod Autoscaler) 를 통해 파드 개수를 늘려서 부하를 분산합니다.
6. 결론 (Takeaways)
- 메모리는 타협하지 마라: CPU와 달리 메모리는 부족하면 죽습니다. 프로덕션 환경에서는
request와limit을 동일하게 설정하여 Guaranteed QoS를 받는 것이 정신 건강에 좋습니다. - OOM은 점수제다:
oom_score_adj는 잔인합니다.limit보다request를 적게 잡을수록 당신의 파드는 OOM Killer의 우선 타겟이 됩니다. - 자살이 타살보다 낫다: 파드가 죽는다면, 노드 메모리가 부족해서(타살) 죽는 것보다 내 할당량을 초과해서(자살) 죽는 것이 원인 분석과 디버깅이 훨씬 쉽습니다.
실무 팁:
kubectl describe node <node-name>명령어로 노드의Allocated resources를 확인해 보세요. Limits의 합계가 노드 물리 메모리의 100%를 훌쩍 넘고 있다면, 당신의 클러스터는 시한폭탄을 안고 있는 것일 수 있습니다.
참고 문헌: