Kubernetes 환경 OOMKilled 원인 분석

Kubernetes 환경 OOMKilled 원인 분석

개요

프로젝트를 진행하다 보면 OOM 또는 OOMKilled를 마주치는 경우가 있습니다. 특히 Kubernetes 환경에서는 "Pod가 OOMKilled로 인해 재시작되었다"는 상황을 종종 접하게 됩니다.

OOMKilled는 단순히 서버가 재시작되는 것처럼 보일 수 있지만, 원인을 파악하지 못한 채 방치하면 서비스 부하가 높은 상황에서 반복적으로 발생하여 치명적인 장애로 이어질 수 있습니다.

이 문서는 프로젝트를 진행하면서 마주친 OOMKilled가 무엇인지, 왜 발생하는지, 그리고 어떻게 분석하고 해결해야 하는지 조사하고 검토한 내용을 정리한 것입니다.

OOM과 OOMKilled의 차이

OOM이든 OOMKilled이든 둘 다 메모리 할당과 관련하여 발생하는 이슈입니다. 그러나 둘은 발생 주체와 동작 방식에서 차이가 있습니다.

OOM (Out Of Memory Error)은 JVM 내부에서 발생합니다. Heap 메모리가 가득 차 더 이상 객체를 할당할 수 없을 때 JVM이 java.lang.OutOfMemoryError를 발생시키며 프로세스가 종료됩니다. 즉, JVM 스스로 한계를 인지하고 에러를 던지는 것입니다.

OOMKilled는 JVM 외부, 즉 Kubernetes와 같은 Container 환경에서 발생합니다. Container에 설정된 메모리 Limit을 초과하는 순간 Kubernetes가 해당 프로세스를 강제 종료합니다. JVM이 에러를 인지하기 전에 외부에서 먼저 kill하는 것이기 때문에, OOM과 달리 별도의 에러 로그가 남지 않아 원인 파악이 더 어렵습니다.

구분

OOM

OOMKilled

발생 주체

JVM 내부

Kubernetes (cgroup)

원인

Heap 메모리 고갈

Container Limit 초과

에러 로그

OutOfMemoryError 로그 남음

로그 없이 강제 종료

Exit Code

1

137

대상 메모리

Heap 영역

Heap + Non-Heap + Off-Heap 전체

OOMKilled 발생 시 Kubernetes의 재시작 정책(restartPolicy)에 따라 Pod이 자동으로 재시작되므로, 단순 트래픽이 낮은 시간대에 발생한다면 큰 문제가 없을 수도 있습니다. 그러나 서비스 부하가 높은 상황에서 OOMKilled가 발생하면 상황이 심각해질 수 있습니다. Pod 상황에 따라 재시작 중에는 서비스가 중단될 수도 있으며, 재시작 직후 트래픽이 다시 몰리면서 메모리가 빠르게 차올라 연속적인 OOMKilled가 반복될 수 있습니다. 결국 CPU와 메모리 부하가 가중되면서 서비스 전체에 치명적인 장애로 이어질 수 있습니다.

메모리의 구분

OOMKilled와 관련하여 메모리를 지칭할 때는 Heap 메모리 할당 뿐 아니라 Container, JVM 등 좀더 세부적인 메모리에 대한 이해가 필요합니다.

Container 메모리

Kubernetes 환경에서 주의해야 할 점은 메모리 설정의 범위입니다. Container에 설정하는 메모리 Limit은 JVM만을 위한 것이 아니라 Container 전체를 대상으로 합니다.
즉 Container 메모리 > JVM 메모리 관계를 반드시 인지해야 합니다.

Container 메모리 
│
├── JVM 프로세스 메모리
│   ├── Heap
│   ├── Non-Heap
│   └── Off-Heap (Direct Buffer)
│
├── OS 및 시스템 라이브러리
└── 사이드카 Container (Istio sidecar 등) 등 기타…

JVM 메모리

JVM 메모리는 크게 세 가지 영역으로 구분됩니다.

Heap 영역

Java 애플리케이션에서 생성되는 객체가 저장되는 공간입니다. GC(Garbage Collection)의 대상이 되는 영역이기도 합니다.

지표

설명

Heap Used

현재 실제로 사용 중인 메모리

Heap Committed

JVM이 OS로부터 미리 확보해둔 메모리. GC 후에도 즉시 반납하지 않음

Heap Max

JVM이 사용 가능한 최대 Heap 크기

세 지표의 관계는 항상 Used ≤ Committed ≤ Max이고 Committed는 Full GC 시 정리됩니다.

Non-Heap 영역

JVM 자체 운영을 위한 영역으로, GC 대상이 아닙니다.

영역

설명

Metaspace

클래스 메타데이터, Static 변수 저장

Metaspace는 -XX:MaxMetaspaceSize를 설정하지 않으면 이론상 무제한으로 증가할 수 있습니다.

Off-Heap 영역 (Direct Buffer)

Heap도 Non-Heap도 아닌 영역으로, JVM이 OS에 직접 요청하여 할당하는 메모리입니다. NIO 기반의 파일 처리나 네트워크 I/O에서 주로 사용됩니다.

GC의 대상이 아니므로 명시적으로 해제되지 않으면 누적될 수 있으며, -XX:MaxDirectMemorySize로 상한을 설정하지 않으면 무제한으로 증가할 수 있습니다.

Container 메모리와 JVM 메모리의 관계

JVM의 Heap 크기가 Container Limit보다 작게 설정되어 있으면 문제없다고 생각하기 쉽습니다. 그러나 Heap뿐만 아니라 Non-Heap, Off-Heap 모두 Container Limit 안에서 함께 관리되기 때문에 세 영역을 합산하여 신중하게 계산해야 합니다. 특히 시스템의 성향에 따라 파일 처리가 많은 서비스는 Off-Heap을, 클래스 로딩이 많은 서비스는 Non-Heap을 많이 사용할 수 있기 때문에 단순히 Heap 크기만 볼 것이 아니라 자신의 시스템 특징을 충분히 파악한 후 메모리를 설계해야 합니다.

기본적으로 Non-Heap이 200MB, Off-Heap이 100MB, JVM 외 기타 메모리가 50MB를 사용하는 시스템에서 Container Limit을 1Gi로, -XX:MaxRAMPercentage=80으로 설정한다면 아래와 같은 상황이 발생합니다.

Heap Max (80%)  :  819MB  ← JVM이 사용 가능하다고 인식하는 Heap 한도
Non-Heap        :  200MB
Off-Heap        :  100MB
JVM 외 기타      :   50MB
─────────────────────────
Heap 외 합계     :  350MB
실질적으로 Heap에 허용되는 메모리 : 1,024MB - 350MB = 674MB

JVM은 Heap을 819MB까지 사용할 수 있다고 인식하고 Full GC 없이 메모리를 계속 할당합니다. 그러나 Kubernetes는 Heap + Non-Heap + Off-Heap + 기타를 모두 합산하여 Container Limit과 비교하기 때문에, Heap 사용량이 674MB를 초과하는 순간 Container Limit 1Gi를 넘어서게 되고 JVM이 인지하기 전에 Kubernetes가 프로세스를 강제 종료합니다. 이것이 OOMKilled입니다.

JVM 설정

아래는 관련 OOMKilled와 관련된 JVM 옵션으로 간략하게 정의만 하고 넘어가겠습니다.

MaxRAMPercentage

  • Container Limit 대비 Heap Max 크기를 비율로 설정합니다. 너무 작게 하면 메모리를 낭비할 수 있고 너무 높게 설정하면 Non-Heap, Off-Heap의 소비량이 합산되어 Container Limit을 초과할 수 있습니다.

MaxMetaspaceSize

  • Non-Heap 영역 중 Metaspace의 최대 크기를 제한합니다. 설정하지 않으면 Metaspace는 이론상 무제한으로 증가할 수 있습니다. 현재 사용량에 여유를 두어 상한선을 설정하는 것을 권장합니다.

MaxDirectMemorySize

  • Off-Heap(Direct Buffer)의 최대 크기를 제한합니다. NIO 기반 파일 처리나 네트워크 I/O를 많이 사용하는 서비스에서 설정하지 않으면 Direct Buffer가 무제한으로 증가하여 Container Limit을 초과할 수 있습니다. 이 옵션을 설정하면 한도 초과 시 OutOfMemoryError: Direct buffer memory 로그가 남아 원인 파악이 쉬워지는 부가적인 장점도 있습니다.

OOMKilled 서버 분석 Case

아래는 OOMKilled가 발생한 서비스의 메모리 현황을 입력으로 AI로 분석한 결과를 요약한 내용입니다. 최종 JVM 옵션 수정 적용 후 OOMKilled 문제는 해결되었습니다.

원본 노션 링크 : https://app.notion.com/p/vizend/Pod-OOMKilled-38b35bc54c13806699e8e3ae329bf38f

메모리 현황 분석

1. Container 리소스 설정

항목

Memory Limit

1Gi (1,073 MB)

Memory Request

256Mi

QoS Class

Burstable

2. JVM 메모리 현황 (현재 시점)

영역

MB 환산

Heap Used

376,595,248 bytes

~359 MB

Heap Committed

512,229,376 bytes

~488 MB

Heap Max

859,308,032 bytes

~819 MB

Non-Heap Used

210,018,744 bytes

~200 MB

└ Metaspace

127,576,696 bytes

~122 MB

3. 핵심 문제 분석

Container Limit          :  1,073 MB  (1Gi)
────────────────────────────────────────
Heap Max (JVM 설정)       :    819 MB
Non-Heap Used (현재)      :    200 MB
────────────────────────────────────────
합산 최대 사용 가능       :  1,019 MB  ← 한계에 매우 근접
나머지 여유               :    ~54 MB  ← 위험 수준

JVM의 Heap Max + Non-Heap만 합쳐도 Container Limit의 약 95% 를 차지합니다.

여기에 JVM 내부 오버헤드 (Thread Stack, GC 메타데이터 등)가 추가되면 1Gi를 초과하여 OOMKilled가 발생합니다.

4. GC 방식 확인

Actuator 응답에 PS Eden Space, PS Old Gen, PS Survivor Space가 보입니다.

G1GC 사용을 권장합니다.

5. 원인 요약

원인

설명

Heap Max 과다

819MB Heap + 200MB Non-Heap = 1,019MB로 Limit 초과 직전

GC 방식 부적합

Parallel GC → Container 메모리 인식 미흡

JVM 옵션 미설정

-XX:MaxRAMPercentage 등 Container 인식 옵션 부재 추정

QoS Burstable

Request ≠ Limit → 메모리 보장 없음, OOM 우선 대상

MaxRAMPercentage 시뮬레이션 결과

MaxRAMPercentage를 현재 80%에서 60%까지 비교 분석했습니다.

Heap 사용량

항목

80%

70%

65%

60%

Container Limit

1,073MB

1,073MB

1,073MB

1,073MB

Heap Max

858MB

~751MB

~698MB

~644MB

Non-Heap 실측

~200MB

~200MB

~200MB

~200MB

Thread Stack 등 오버헤드

~50~80MB

~50~80MB

~50~80MB

~50~80MB

합계 추정

~1,108~1,131MB

~1,001~1,031MB

~948~978MB

~894MB

여유

~42~72MB

~95~125MB

~179MB

옵션별 비교

설정

Heap Max

여유

평가

비교

현재 80%

~858MB

~15MB

위험

Non-Heap, Off-Heap 추가 사용 시 문제 발생

70%

~751MB

~42~72MB

불안정

G1GC와 함께 Container Limit 증가 (1.5Gi)

60%

~644MB

~149~179MB

권장

G1GC 적용

50%

~536MB

~257MB

안전하나 Heap 부족 가능

검토 제외

  • 가장 안전한 접근은 60%로 설정 후 모니터링하면서 필요시 올립니다.

  • Non-Heap 또는 Off-Heap 200MB는 고정 값이 아니며, 추가적인 파일 업로드 등 부하 상황에서 OOMKilled를 유발할 수 있습니다.

권장 JVM 옵션 (최종)

# 현재
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=80
-XX:+UseParallelGC
# 권장
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=60          # Heap ~644MB로 제한
-XX:+UseG1GC                     # Container 친화적 GC
-XX:MaxMetaspaceSize=192m        # Metaspace 상한 설정 (현재 122MB 사용 중)
-XX:MaxDirectMemorySize=140m     # Direct Buffer 상한 설정 (현재 108MB 사용 중)
  • Heap Max: ~644MB

  • Non-Heap: ~200MB

  • 합계: ~844MB → Container Limit 1Gi 내에서 여유 확보

sby

Site footer