운영 중인 쿠버네티스(Kubernetes) 클러스터에서 갑자기 파드가 죽어버리고 OOMKilled 상태를 보게 된다면, 정말 당황스러울 수 있습니다. 특히 노드에 여유 메모리가 충분해 보이는데도 이런 현상이 발생하면 더욱 혼란스럽죠.

OOMKilled는 쿠버네티스 환경에서 가장 빈번하게 발생하는 문제 중 하나로, 제대로 된 대응책을 마련하지 않으면 지속적으로 발생할 수 있는 골치 아픈 이슈입니다. 이번 포스트에서는 OOMKilled 에러의 근본 원인부터 실질적인 해결 방법까지 구체적인 메모리/리소스 관리방법을 제공해 드리고자 합니다.

 

1. OOMKilled 에러란 무엇인가?

OOMKilled는 “Out Of Memory Killed”의 줄임말로, 컨테이너가 할당된 메모리 한계를 초과했을 때 리눅스 커널의 OOM Killer가 해당 프로세스를 강제 종료하는 현상입니다.

OOMKilled의 정확한 동작 방식

많은 분들이 쿠버네티스가 직접 이 시그널을 보낸다고 생각하시는데, 실제로는 리눅스 커널이 메모리 한계를 감지하고 컨테이너를 종료시킵니다:

  1. 컨테이너가 메모리 limit를 초과
  2. 리눅스 커널이 SIGKILL(신호 9) 발송
  3. Kubelet이 종료를 감지하고 쿠버네티스 API에 알림
  4. 파드 상태가 “OOMKilled”로 업데이트

이때 나타나는 Exit Code 137은 128+9(SIGKILL)의 결과로, OOMKilled의 확실한 증거입니다.

실제 로그에서 확인하는 방법

# 파드 상태 확인
kubectl describe pod <pod-name> -n <namespace>

# 이벤트 로그에서 다음과 같은 내용을 확인할 수 있습니다
Events:
  Type     Reason     Age   From     Message
  ----     ------     ----  ----     -------
  Warning  Killing    32s   kubelet  Killing container with id...
  Normal   Pulled     30s   kubelet  Container image pulled
  Warning  BackOff    15s   kubelet  Back-off restarting failed container

 

 

2. OOMKilled가 발생하는 주요 원인들

실제 운영 환경에서 발생하는 OOMKilled의 원인을 분석해보면 크게 다섯 가지로 나눌 수 있습니다.

잘못된 메모리 리소스 설정

가장 흔한 원인입니다. 애플리케이션이 실제로 필요한 메모리보다 훨씬 낮게 limit를 설정하는 경우가 많습니다.

실제 사례: 한 핀테크 회사의 사기 탐지 시스템에서 메모리 limit를 2Gi로 설정했으나, 피크 시간대에는 3.5Gi가 필요했던 사례가 있었습니다.

애플리케이션 내 메모리 누수

코드 레벨에서 발생하는 메모리 누수는 시간이 지날수록 메모리 사용량이 점진적으로 증가하다가 결국 OOMKilled를 발생시킵니다.

언어별 특성:

  • Java: JVM 가비지 컬렉터가 컨테이너 제약을 완전히 인식하지 못할 때
  • Go: Go 1.19부터 GOMEMLIMIT 환경변수로 개선되었지만 여전히 주의 필요
  • Node.js: 이벤트 루프와 관련된 메모리 누수 패턴

예기치 못한 트래픽 급증

평상시에는 정상적으로 동작하다가 갑작스러운 트래픽 증가로 메모리 사용량이 급상승하는 경우입니다.

실제 사례: 온라인 쇼핑몰의 결제 서비스가 플래시 세일 기간 중 사용자 급증으로 인해 메모리 사용량이 폭증하여 파드가 종료된 사례가 있었습니다.

노드 레벨 메모리 경합

같은 노드에 있는 다른 파드들과의 메모리 경합으로 인해 발생할 수 있습니다. 노드 전체 메모리가 부족해지면 QoS(Quality of Service) 클래스에 따라 우선순위가 낮은 파드부터 종료됩니다.

QoS 우선순위:

  1. BestEffort: 가장 먼저 종료
  2. Burstable: 중간 우선순위
  3. Guaranteed: 가장 나중에 종료

리소스 오버커밋

노드에서 실행 중인 모든 파드의 메모리 request 합계가 노드 용량을 초과하는 상황입니다.

 

 

3. 메모리 관리의 핵심 원칙들

효과적인 메모리 관리를 위해서는 몇 가지 핵심 원칙을 이해해야 합니다.

Memory Request vs Limit의 올바른 설정

2025년 현재 베스트 프랙티스memory limit = memory request로 설정하는 것입니다. 이는 CPU와는 다른 접근 방식입니다.

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: my-app
    resources:
      requests:
        memory: "2Gi"
        cpu: "500m"
      limits:
        memory: "2Gi"  # request와 동일하게 설정
        # CPU limit는 설정하지 않음 (권장사항)

왜 메모리는 limit=request로 설정해야 할까요?

피자 파티에 비유해보면, 각 손님이 피자 2조각을 주문했는데 실제로는 4조각까지 먹을 수 있다고 허용한다면, 파티 중간에 피자가 부족해져서 일부 손님은 아예 못 먹게 될 수 있습니다. 메모리도 마찬가지로, 실제 사용량이 request를 초과하면 예측 불가능한 상황이 발생할 수 있습니다.

QoS 클래스 활용하기

쿠버네티스는 리소스 설정에 따라 세 가지 QoS 클래스를 자동으로 할당합니다:

# Guaranteed QoS - 최고 우선순위
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "500m"

# Burstable QoS - 중간 우선순위  
resources:
  requests:
    memory: "512Mi"
    cpu: "250m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

# BestEffort QoS - 최저 우선순위
# requests와 limits를 모두 설정하지 않음

메모리 백킹 볼륨 주의사항

memory-backed emptyDir을 사용할 때는 특별한 주의가 필요합니다:

volumes:
- name: memory-volume
  emptyDir:
    medium: Memory
    sizeLimit: 1Gi

이런 볼륨에 저장된 파일들은 메모리를 직접 사용하므로, 파일 크기가 커질수록 컨테이너의 메모리 사용량도 증가합니다.

 

 

4. 모니터링 도구 및 설정 가이드

OOMKilled 문제를 해결하려면 먼저 정확한 모니터링이 필요합니다. 2025년 현재 가장 효과적인 모니터링 스택을 소개해드리겠습니다.

Prometheus + Grafana 스택 구축

Prometheus는 쿠버네티스 메트릭 수집의 사실상 표준이 되었습니다. 다음과 같이 설치할 수 있습니다:

# Helm을 사용한 kube-prometheus-stack 설치
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set prometheus.prometheusSpec.retention=30d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi

핵심 메모리 메트릭들

컨테이너 레벨 메트릭:

  • container_memory_usage_bytes: 현재 메모리 사용량
  • container_memory_working_set_bytes: OOM 결정에 사용되는 실제 메트릭
  • container_spec_memory_limit_bytes: 설정된 메모리 한계

파드 레벨 메트릭:

  • kube_pod_container_resource_limits: 리소스 제한값
  • kube_pod_container_resource_requests: 리소스 요청값
  • kube_pod_status_phase: 파드 상태

실용적인 PromQL 쿼리 모음

메모리 사용률 모니터링:

# 파드별 메모리 사용률 (%)
100 * (
  container_memory_working_set_bytes{job="kubelet", container!=""}
  /
  container_spec_memory_limit_bytes{job="kubelet", container!=""}
)

# 네임스페이스별 총 메모리 사용량
sum by (namespace) (container_memory_working_set_bytes{job="kubelet", container!=""})

# 메모리 사용률이 80% 이상인 파드 감지
(
  container_memory_working_set_bytes{job="kubelet", container!=""}
  /
  container_spec_memory_limit_bytes{job="kubelet", container!=""}
) > 0.8

OOMKilled 파드 감지:

# 최근 1시간 내 OOMKilled 발생 횟수
increase(kube_pod_container_status_restarts_total{reason="OOMKilled"}[1h])

# OOMKilled로 재시작된 파드 목록
kube_pod_container_status_restarts_total{reason="OOMKilled"} > 0

Grafana 대시보드 구성

메모리 모니터링 패널 구성:

패널 유형 메트릭 용도
Time Series 메모리 사용률 트렌드 시간대별 패턴 분석
Stat 현재 메모리 사용량 실시간 상태 확인
Table OOMKilled 파드 목록 문제 파드 식별
Heatmap 노드별 메모리 분포 리소스 균형 확인

 

 

5. 실제 문제 해결 방법과 예제

이제 실제 상황에서 OOMKilled 문제를 어떻게 해결할 수 있는지 구체적인 예제와 함께 살펴보겠습니다.

단계별 문제 진단 과정

1단계: 현재 상황 파악

# 파드 상태 확인
kubectl get pods -n <namespace> | grep -E "(OOMKilled|Error|CrashLoopBackOff)"

# 상세 정보 확인
kubectl describe pod <pod-name> -n <namespace>

# 리소스 사용량 확인 (metrics-server 필요)
kubectl top pods -n <namespace> --sort-by=memory
kubectl top nodes --sort-by=memory

2단계: 로그 분석

# 이전 컨테이너 로그 확인 (OOMKilled 발생 직전)
kubectl logs <pod-name> -n <namespace> --previous

# 이벤트 확인
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep OOMKilled

3단계: 메트릭 분석

# Prometheus에서 메모리 사용 패턴 확인
curl -G 'http://prometheus:9090/api/v1/query' \
  --data-urlencode 'query=container_memory_working_set_bytes{pod="<pod-name>"}'

실제 해결 사례별 대응 방법

사례 1: 데이터 처리 애플리케이션의 메모리 스파이크

문제: 대용량 파일 처리 시 메모리 사용량이 급증하여 OOMKilled 발생

해결책:

# 기존 설정
resources:
  requests:
    memory: "1Gi"
  limits:
    memory: "2Gi"

# 개선된 설정
resources:
  requests:
    memory: "4Gi"
  limits:
    memory: "4Gi"
  
# 추가: 메모리 백킹 볼륨을 디스크 기반으로 변경
volumes:
- name: temp-storage
  emptyDir:
    medium: ""  # 메모리 대신 디스크 사용
    sizeLimit: 10Gi

사례 2: Java 애플리케이션의 힙 메모리 관리

문제: JVM이 컨테이너 메모리 제한을 올바르게 인식하지 못함

해결책:

env:
- name: JAVA_OPTS
  value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
resources:
  requests:
    memory: "2Gi"
  limits:
    memory: "2Gi"

사례 3: Node.js 애플리케이션의 메모리 누수

문제: 시간이 지날수록 메모리 사용량이 점진적으로 증가

해결책:

# 메모리 제한 설정
env:
- name: NODE_OPTIONS
  value: "--max-old-space-size=1536"  # 1.5GB로 제한
resources:
  requests:
    memory: "2Gi"
  limits:
    memory: "2Gi"

# 주기적 재시작으로 메모리 누수 완화 (임시 방편)
spec:
  template:
    spec:
      containers:
      - name: nodejs-app
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]

Vertical Pod Autoscaler (VPA) 활용

VPA 설정 예제:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Auto"  # 자동으로 파드 재시작하여 리소스 조정
    # updateMode: "Off"  # 권장사항만 제공
  resourcePolicy:
    containerPolicies:
    - containerName: my-app
      minAllowed:
        memory: "100Mi"
        cpu: "100m"
      maxAllowed:
        memory: "8Gi"
        cpu: "2"
      controlledResources: ["memory", "cpu"]

메모리 프로파일링 도구 활용

Go 애플리케이션 프로파일링:

// 애플리케이션에 pprof 엔드포인트 추가
import _ "net/http/pprof"

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
# 힙 프로파일 수집
kubectl port-forward pod/<pod-name> 6060:6060 &
go tool pprof http://localhost:6060/debug/pprof/heap

# 메모리 사용 패턴 분석
(pprof) top
(pprof) list <function-name>

 

 

6. OOMKilled 예방책과 체계적 해결 방법

OOMKilled 문제를 사전에 방지하기 위한 체계적인 접근 방법을 살펴보겠습니다.

개발 단계에서의 예방책

1. 로컬 환경에서의 메모리 프로파일링

# Docker 컨테이너에서 메모리 사용량 모니터링
docker stats <container-id>

# 애플리케이션별 메모리 사용 패턴 분석
# Java: JVisualVM, JProfiler
# Node.js: clinic.js, heapdump
# Go: go tool pprof
# Python: memory_profiler, pympler

2. 적절한 메모리 테스트

# 부하 테스트 시나리오에 메모리 체크 포함
apiVersion: v1
kind: ConfigMap
metadata:
  name: load-test-config
data:
  test-script: |
    # 점진적 부하 증가로 메모리 사용 패턴 확인
    for i in {1..100}; do
      echo "테스트 $i 진행중..."
      # 메모리 사용량 기록
      kubectl top pod <pod-name> >> memory-usage.log
      sleep 30
    done

운영 환경에서의 모니터링 전략

1. 프로액티브 알림 설정

# PrometheusRule 예제
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: memory-alerts
spec:
  groups:
  - name: memory.rules
    rules:
    - alert: HighMemoryUsage
      expr: |
        (
          container_memory_working_set_bytes{job="kubelet", container!=""}
          /
          container_spec_memory_limit_bytes{job="kubelet", container!=""}
        ) > 0.8
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "컨테이너 메모리 사용률이 80%를 초과했습니다"
        description: "{{ $labels.namespace }}/{{ $labels.pod }}의 메모리 사용률이 {{ $value | humanizePercentage }}입니다"
    
    - alert: OOMKilledDetected
      expr: |
        increase(kube_pod_container_status_restarts_total{reason="OOMKilled"}[5m]) > 0
      labels:
        severity: critical
      annotations:
        summary: "OOMKilled 이벤트가 감지되었습니다"
        description: "{{ $labels.namespace }}/{{ $labels.pod }}에서 OOMKilled가 발생했습니다"

2. 자동화된 대응 시스템

# HorizontalPodAutoscaler로 메모리 기반 스케일링
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: memory-based-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 70  # 메모리 사용률 70%에서 스케일링
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

인프라 레벨의 최적화

1. 노드 리소스 예약

# kubelet 설정에서 시스템 리소스 예약
apiVersion: v1
kind: Node
metadata:
  name: worker-node-1
spec:
  # 시스템과 kubelet을 위한 리소스 예약
  allocatable:
    memory: "7Gi"  # 8Gi 노드에서 1Gi를 시스템용으로 예약
    cpu: "3500m"   # 4 vCPU에서 500m을 시스템용으로 예약

2. 노드 어피니티와 안티-어피니티 활용

# 메모리 집약적 애플리케이션을 별도 노드에 분산
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["memory-intensive-app"]
              topologyKey: kubernetes.io/hostname
      nodeSelector:
        node-type: memory-optimized  # 메모리 최적화 노드 선택

애플리케이션 아키텍처 개선

1. 마이크로서비스 분리

# 메모리 집약적 기능을 별도 서비스로 분리
# 기존: 하나의 파드에서 모든 기능 처리
apiVersion: apps/v1
kind: Deployment
metadata:
  name: monolithic-app
spec:
  template:
    spec:
      containers:
      - name: app
        resources:
          limits:
            memory: "8Gi"  # 높은 메모리 요구사항

# 개선: 기능별로 서비스 분리
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: data-processor
spec:
  template:
    spec:
      containers:
      - name: processor
        resources:
          limits:
            memory: "4Gi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      containers:
      - name: server
        resources:
          limits:
            memory: "1Gi"

2. 캐싱 전략 최적화

# Redis를 사용한 외부 캐싱으로 애플리케이션 메모리 부담 감소
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  config.yaml: |
    cache:
      type: redis
      redis:
        host: redis-service
        port: 6379
        max_memory_policy: allkeys-lru
      # 인메모리 캐시 최소화
      local_cache:
        max_size: 100MB
        ttl: 300s

 

 

7. 에러를 방지하기 위한 메모리 관리 기법

Memory QoS (Quality of Service) 활용

쿠버네티스 1.27부터 도입된 Memory QoS 기능을 활용하면 더욱 정교한 메모리 관리가 가능합니다.

# Memory QoS를 활용한 설정
apiVersion: v1
kind: Pod
metadata:
  annotations:
    # 파드별로 Memory QoS 비활성화 (필요시)
    qos.memory.kubernetes.io/disabled: "true"
spec:
  containers:
  - name: my-app
    resources:
      requests:
        memory: "1Gi"
      limits:
        memory: "2Gi"

Memory QoS는 cgroups v2의 memory.high 설정을 통해 OOM이 발생하기 전에 메모리 사용을 제한합니다.

메모리 압박 상황 대응

Pressure Stall Information (PSI) 모니터링:

# kubelet에서 PSI 기능 활성화
--feature-gates=KubeletPSI=true

# PSI 메트릭 확인
curl http://kubelet:10250/metrics/cadvisor | grep container_pressure_memory

파드 우선순위를 통한 리소스 보호:

# 높은 우선순위 파드 설정
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000
globalDefault: false
description: "중요한 애플리케이션을 위한 높은 우선순위"

---
apiVersion: v1
kind: Pod
spec:
  priorityClassName: high-priority
  containers:
  - name: critical-app
    resources:
      requests:
        memory: "2Gi"
      limits:
        memory: "2Gi"

메모리 누수 자동 감지 시스템

eBPF를 활용한 연속 프로파일링:

# Parca 배포로 연속 프로파일링 시스템 구축
apiVersion: v1
kind: ConfigMap
metadata:
  name: parca-config
data:
  parca.yaml: |
    object_storage:
      bucket:
        type: FILESYSTEM
        config:
          directory: "./tmp"
    
    scrape_configs:
    - job_name: 'kubernetes-pods'
      kubernetes_sd_configs:
      - role: pod
      relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_profiles_grafana_com_memory_scrape]
        action: keep
        regex: true

이제 마무리 부분으로 넘어가겠습니다.

 

 

OOMKilled 에러는 쿠버네티스 환경에서 피할 수 없는 문제이지만, 올바른 이해와 체계적인 접근을 통해 충분히 관리할 수 있습니다. 핵심은 사전 예방에 있습니다. 적절한 리소스 설정, 지속적인 모니터링, 그리고 프로액티브한 알림 시스템을 갖추면 대부분의 OOMKilled 문제를 미연에 방지할 수 있습니다. 특히memory limit = memory request 설정과 Memory QoS 기능을 적극 활용하여 더욱 안정적인 메모리 관리가 가능해졌습니다. Prometheus와 Grafana를 통한 모니터링 스택도 더욱 성숙해져서 실시간으로 메모리 사용 패턴을 파악하고 대응할 수 있습니다.

 

댓글 남기기