Overview
Kubernetes를 운영하다 보면 누구나 한 번쯤 마주하는 상황이 있다. 바로 `OOMKilled` (Out of Memory Killed) 에러이다. 파드가 설정된 메모리 제한(Limit)을 초과하여 강제로 종료되는 이 현상은, 많은 DevOps 엔지니어들에게 골치 아픈 문제이다.
대부분의 팀이 이 문제를 어떻게 해결할까요? "일단 메모리를 2배로 늘리고 다시 배포해보자"는 식의 주먹구구식 대응이 흔하다. 하지만 이는 근본적인 해결책이 아니며, 리소스 낭비와 비용 증가로 이어진다.
이 글에서는 OOMKilled 문제에 대한 체계적인 접근 방법과 실무에서 바로 적용할 수 있는 해결 전략을 다룬다. 단순히 리소스를 늘리는 것이 아니라, 왜 발생했는지 파악하고, 적정 리소스를 산정하며, 재발을 방지하는 프로세스를 구축하는 것이 목표이다.

1. 흔한 잘못된 대응 방식
"메모리 2배 증설 + 기도" 패턴
많은 팀에서 OOMKilled가 발생하면 다음과 같이 대응한다.
# Before
resources:
limits:
memory: "512Mi"
# After (에러 발생 후)
resources:
limits:
memory: "1Gi" # 그냥 2배로 늘림
이 방식의 문제점
- 근본 원인을 파악하지 못함
- 메모리 누수(Memory Leak)가 있다면 시간이 지나면 또 OOM 발생
- 리소스 낭비와 클러스터 비용 증가
- 재발 가능성이 높음
왜 이런 방식이 일반적일까?
- 가시성 부족: 실제 메모리 사용량을 정확히 모름
- 시간 압박: 프로덕션 장애 상황에서 빠른 조치 필요
- 책임 회피: 개발팀 vs 인프라팀 간의 책임 소재가 불분명
- 도구 부재: 적정 리소스를 산정할 방법을 모름
2. 올바른 접근법 : ① 자동화 도구 활용
VPA (Vertical Pod Autoscaler) - 추천 모드
VPA를 "Recommendation Only" 모드로 실행하면 실제 사용량을 기반으로 적정 리소스를 제안받을 수 있다.
설치 및 설정
# VPA 설치
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh
# vpa-recommendation.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # 추천만 하고 자동 적용 안 함
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
memory: "128Mi"
maxAllowed:
memory: "8Gi"
결과 확인
kubectl describe vpa my-app-vpa
# 출력 예시
Recommendation:
Container Recommendations:
Container Name: my-app
Lower Bound:
Memory: 256Mi
Target:
Memory: 512Mi # 추천값
Uncapped Target:
Memory: 512Mi
Upper Bound:
Memory: 1Gi
Best Practice
- 최소 2주~1개월 정도 데이터 수집 후 적용
- 피크 타임의 사용량도 고려
- 추천값에 20-30% 버퍼 추가
Goldilocks - VPA를 쉽게 시각화
Goldilocks는 VPA 추천값을 Grafana처럼 대시보드로 보여주는 도구이다.
# Helm으로 설치
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install goldilocks fairwinds-stable/goldilocks \
--namespace goldilocks \
--create-namespace
네임스페이스 활성화
kubectl label namespace production goldilocks.fairwinds.com/enabled=true
대시보드 접속
kubectl -n goldilocks port-forward svc/goldilocks-dashboard 8080:80
# http://localhost:8080 접속
- 대시보드에서 모든 워크로드의 현재 설정값 vs 추천값을 한눈에 비교할 수 있다.
- ingress를 사용해도 된다.
- 자세한 설치 방법은 https://github.com/FairwindsOps/charts 해당 링크를 참고하면 된다.
KRR (Kubernetes Resource Recommender)
Robusta에서 만든 CLI 도구로, Prometheus 메트릭을 분석하여 추천값을 제공한다
# 설치
pip install krr
# 실행
krr simple --prometheus-url http://prometheus:9090
# 출력 예시
| Namespace | Name | Container | Current Memory | Recommended | Severity |
|-------------|---------------|-----------|----------------|-------------|----------|
| production | payment-api | app | 1Gi | 512Mi | HIGH |
| production | user-service | app | 512Mi | 2Gi | CRITICAL |
3. 올바른 접근법 : ② 애플리케이션 최적화
메모리 누수 탐지
단순히 메모리를 늘리는 것만으로는 해결되지 않는 경우가 많다. 메모리 누수(Memory Leak)를 의심해야 한다.
증상
- 메모리 사용량이 시간이 지날수록 계속 증가
- 재시작 직후엔 정상, 며칠 후 OOM
- Limit을 늘려도 결국 다시 OOM
Continuous Profiling 도구
Grafana Pyroscope 활용
# pyroscope-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
env:
# Pyroscope 프로파일링 활성화
- name: PYROSCOPE_SERVER_ADDRESS
value: "http://pyroscope:4040"
- name: PYROSCOPE_APPLICATION_NAME
value: "my-app"
- Pyroscope는 Flamegraph를 통해 어떤 함수가 메모리를 많이 사용하는지 시각적으로 보여준다.
언어별 최적화 팁
Go 언어
// 잘못된 예 - 메모리 누수
resp, err := http.Get(url)
// resp.Body.Close() 호출 안 함!
// 올바른 예
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close() // 반드시 Close 호출
// GOMEMLIMIT 설정
// 컨테이너 메모리의 80% 정도로 설정
// deployment.yaml에 환경변수 추가
env:
- name: GOMEMLIMIT
value: "800MiB" # 컨테이너 limit이 1Gi인 경우
Node.js
// package.json scripts 수정
{
"scripts": {
"start": "node --max-old-space-size=512 app.js"
}
}
# deployment.yaml
containers:
- name: app
resources:
limits:
memory: "1Gi"
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768" # 컨테이너 limit의 75%
Java
# deployment.yaml
containers:
- name: app
resources:
limits:
memory: "2Gi"
env:
- name: JAVA_OPTS
value: "-Xmx1536m -Xms512m" # 최대 힙 크기를 limit의 75%로
4. 올바른 접근법 : ③ 프로세스 개선
사전 예방: 부하 테스트
배포 전에 리소스 사용량을 미리 파악한다.
k6를 사용한 부하 테스트
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // 2분간 100 VU로 증가
{ duration: '5m', target: 100 }, // 5분간 유지
{ duration: '2m', target: 200 }, // 2분간 200 VU로 증가
{ duration: '5m', target: 200 }, // 5분간 유지
],
};
export default function () {
let res = http.get('http://my-app:8080/api/users');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
GitLab CI/CD 통합
# .gitlab-ci.yml
performance-test:
stage: test
image: grafana/k6:latest
script:
- k6 run --out json=results.json load-test.js
# 메모리 사용량 수집
- kubectl top pod -l app=my-app -n staging
artifacts:
reports:
performance: results.json
only:
- merge_requests
모니터링 및 알림 설정
Prometheus 알림 규칙
# prometheus-rules.yaml
groups:
- name: memory-alerts
rules:
- alert: PodMemoryUsageHigh
expr: |
(container_memory_working_set_bytes / container_spec_memory_limit_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} memory usage is above 90%"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is using {{ $value | humanizePercentage }} of its memory limit"
- alert: PodMemoryUsageCritical
expr: |
(container_memory_working_set_bytes / container_spec_memory_limit_bytes) > 0.95
for: 2m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} memory usage is above 95%"
Slack 알림 연동
- 메모리 사용률이 90% 이상이면 개발팀에 자동 알림
- OOMKilled 발생 시 즉시 알림 + 해당 파드의 최근 메모리 그래프 첨부
5. 실전 대응 플레이북
Step 1: 긴급 대응 (5분 이내)
# 1. 현재 상태 확인
kubectl get pod -n production | grep -E "OOMKilled|Error"
# 2. 이벤트 로그 확인
kubectl describe pod <pod-name> -n production | grep -A 10 "Events:"
# 3. 메모리 사용량 확인
kubectl top pod <pod-name> -n production
# 4. 임시 조치 (긴급한 경우)
kubectl scale deployment <deployment-name> -n production --replicas=5
# 또는 메모리 limit 임시 증설
Step 2: 원인 분석 (30분 이내)
# 1. Prometheus에서 메모리 트렌드 확인
# 쿼리: container_memory_working_set_bytes{pod=~"my-app.*"}
# 2. 애플리케이션 로그 확인
kubectl logs <pod-name> -n production --previous # 이전 파드 로그
# 3. VPA 추천값 확인
kubectl describe vpa my-app-vpa
# 4. 프로파일링 데이터 확인 (Pyroscope 대시보드)
```
### Step 3: 근본 원인 해결 (1-2일)
**체크리스트:**
- [ ] 메모리 누수가 있는가? (프로파일링 결과 확인)
- [ ] 언어별 메모리 설정이 적절한가? (GOMEMLIMIT, XMX 등)
- [ ] 부하 테스트 결과는? (피크 시간대 사용량)
- [ ] VPA 추천값은? (최소 2주 데이터)
- [ ] 애플리케이션 최적화 가능한가? (불필요한 캐싱, 큰 객체 등)
---
## 6. 조직 문화 개선
### 개발팀과의 협업 체계
**명확한 책임 분리:**
```
[인프라팀]
- 클러스터 리소스 관리
- 모니터링 시스템 구축
- 적정 리소스 산정 도구 제공
[개발팀]
- 애플리케이션 메모리 최적화
- 부하 테스트 수행
- 메모리 누수 수정
PR 승인 프로세스 개선
## PR Checklist
- [ ] 부하 테스트 완료 (k6 결과 첨부)
- [ ] 메모리 사용량 프로파일링 완료
- [ ] 리소스 request/limit 값 적절성 검토
- [ ] VPA 추천값과 비교 (±20% 이내)
마무리
Kubernetes의 OOMKilled 문제는 단순히 "메모리를 늘리면 해결"되는 것이 아니다. 이는 애플리케이션의 리소스 사용 패턴을 이해하고, 적절한 도구로 가시성을 확보하며, 개발팀과 협업하여 근본 원인을 해결해야 하는 엔지니어링 문제이다.
핵심 요약
- 도구 활용: VPA, Goldilocks, KRR로 데이터 기반 의사결정
- 애플리케이션 최적화: 메모리 누수 탐지 및 언어별 설정 최적화
- 프로세스 구축: 부하 테스트, 모니터링, 알림 체계 수립
- 조직 문화: 개발팀과의 명확한 책임 분리 및 협업
무작정 메모리를 2배로 늘리고 기도하는 대신, 체계적인 접근으로 안정적이고 비용 효율적인 Kubernetes 클러스터를 운영하시길 바란다.
Reference
- https://github.com/FairwindsOps/charts
- Kubernetes VPA Documentation
- Goldilocks - Fairwinds
- KRR - Kubernetes Resource Recommender
- Grafana Pyroscope
- k6 Load Testing
- Go Memory Management
- Node.js Memory Best Practices
- Java Memory Management in Containers
Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist
'Container Orchestration > Kubernetes' 카테고리의 다른 글
| Kubernetes 클러스터로의 외부 트래픽 흐름 완벽 가이드 (0) | 2026.06.04 |
|---|---|
| Cilium CNI 환경에서의 Kubernetes 네트워크 문제 해결 (0) | 2026.05.28 |
| Kubernetes 내부 네트워크 완벽 분석: IPVS 모드 환경에서의 패킷 플로우 추적 (0) | 2026.05.21 |
| ingress-nginx → nginx-gateway-fabric 마이그레이션 실전 기록 (온프레미스 K8s, 11개 인스턴스) (0) | 2026.05.14 |
| EKS 프로덕션 배포 가이드: 502 에러 제로 달성기 (0) | 2026.05.12 |