Java OOM 트러블슈팅
문제 상황#
온프렘 K8s 클러스터(mini-might 워커 노드)에서 Java 앱의 메모리 부족으로 OOM Killer가 호출되어 노드가 다운된 이슈 분석 및 해결 방안.
증상#
- k9s에서 mini-might 노드의 모든 파드가
Unknown상태로 표시 - 재부팅 후 파드들이 순차적으로 복구됨
kubectl describe node mini-mightEvents에서Rebooted확인
NodeNotReady 5분 전 - 노드가 죽음
Rebooted 58초 전 - 재부팅 감지
NodeReady 17초 전 - 복구 완료
원인 분석#
1. dmesg에서 OOM 로그 확인#
mini-might 노드에 SSH 접속 후:
sudo grep -i "oom\|killed\|out of memory" /var/log/kern.log | tail -50
OOM 로그 발견:
2026-03-22T15:00:03 mini-might kernel: oom-kill:constraint=CONSTRAINT_MEMCG
task=java,pid=90057,uid=0
Memory cgroup out of memory: Killed process 90057 (java)
total-vm:2007864kB, anon-rss:518828kB
2. 분석 결과#
| 항목 | 값 |
|---|---|
| 프로세스 | java (PID 90057) |
| OOM 타입 | CONSTRAINT_MEMCG (cgroup 메모리 제한 초과) |
| 메모리 사용 | ~507MB (anon-rss: 518828kB) |
| 가상 메모리 | ~2GB (total-vm: 2007864kB) |
3. 문제가 된 파드 설정 확인#
kubectl describe pod java-cloud-gateway-6558b496dd-nhhfw -n dev-webs
java-service 컨테이너 리소스:
resources:
limits:
cpu: 500m
memory: 512Mi # <- 문제!
requests:
cpu: 100m
memory: 128Mi
JVM 옵션:
env:
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
4. 근본 원인#
| 설정 | 값 | 설명 |
|---|---|---|
| Memory Limit | 512Mi | 컨테이너 최대 메모리 |
| MaxRAMPercentage | 75% | JVM 힙 비율 |
| 계산된 힙 | ~384Mi | 512 × 0.75 |
| 실제 사용량 | ~507MB | OOM 시점 |
문제점:
- Java는 힙(384Mi) 외에도 Metaspace, Thread Stack, Native Memory 사용
- 힙 + 비힙 메모리가 512Mi limit을 초과 → OOM Killer 호출
해결#
옵션 1: Memory Limit 증가 (권장)#
resources:
limits:
cpu: 500m
memory: 1Gi # 512Mi → 1Gi
requests:
cpu: 100m
memory: 512Mi # 128Mi → 512Mi
옵션 2: JVM 힙 비율 낮추기#
env:
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0"
옵션 3: 힙 크기 직접 지정#
env:
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -Xmx256m -Xms256m"
노드 리소스 현황#
mini-might 노드 상태 (복구 후)#
| 항목 | 값 | 상태 |
|---|---|---|
| 메모리 | 28GB 중 7.4GB 사용 | 정상 |
| 디스크 | 98GB 중 55GB 사용 (59%) | 정상 |
| CPU Limits | 118% 오버커밋 | 주의 |
| Memory Limits | 40% (11GB/28GB) | 정상 |
| Swap | 비활성화 (0B) | 주의 |
주의사항:
- Swap 비활성화 상태에서 메모리 부족 시 바로 OOM 발생
- CPU 오버커밋(118%)은 burst 상황에서 경쟁 발생 가능
디버깅 명령어 정리#
노드 상태 확인#
kubectl get nodes
kubectl describe node <node-name>
OOM 로그 확인 (노드에서 직접)#
# 재부팅 후에도 파일 로그는 남아있음
sudo grep -i "oom\|killed\|out of memory" /var/log/kern.log | tail -50
sudo grep -i "oom\|killed" /var/log/syslog | tail -50
# dmesg는 재부팅 시 초기화됨
sudo dmesg | grep -i "oom\|killed\|out of memory"
kubelet 로그 확인#
sudo journalctl -u kubelet --since "1 hour ago" | tail -100
메모리/디스크 상태#
free -h
df -h
파드 리소스 설정 확인#
kubectl describe pod <pod-name> -n <namespace> | grep -A10 -iE "limits:|requests:|env:"
Java 컨테이너 메모리 가이드#
JVM 메모리 구성#
Total Container Memory
├── Heap (힙)
│ └── -Xmx 또는 MaxRAMPercentage로 제어
├── Metaspace (메타스페이스)
│ └── 클래스 메타데이터, 기본 무제한
├── Thread Stacks (스레드 스택)
│ └── 스레드당 ~1MB
├── Native Memory
│ └── JNI, 네이티브 라이브러리
└── Direct Buffers
└── NIO 버퍼
권장 설정#
| Container Limit | MaxRAMPercentage | 계산된 힙 | 비힙 여유 |
|---|---|---|---|
| 512Mi | 50% | 256Mi | 256Mi |
| 1Gi | 75% | 768Mi | 256Mi |
| 2Gi | 75% | 1.5Gi | 512Mi |
안전한 JVM 옵션 예시#
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:MaxMetaspaceSize=128m
-XX:+ExitOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError: OOM 발생 시 JVM이 즉시 종료되어 K8s가 재시작 가능
교훈#
Java가 특히 OOM Killed에 취약한 이유#
Java는 JVM(가상 머신) 위에서 동작하기 때문에 메모리를 이중으로 관리한다:
컨테이너 memory limit (예: 512Mi)
├── JVM Heap (-Xmx) ← GC가 관리하는 영역
├── JVM Non-Heap ← Metaspace, CodeCache, Thread Stack 등
├── Native Memory ← JNI, NIO DirectBuffer 등
└── OS/기타 ← JVM 자체 오버헤드
- JVM은 Heap만 제한하고 나머지는 제한 없이 사용한다.
-Xmx256m으로 힙을 256MB로 잡아도 Non-Heap + Native 메모리가 추가로 100~200MB를 사용할 수 있다. - GC(Garbage Collection)는 Heap 내에서만 동작한다. 컨테이너 전체 메모리가 limit에 근접해도 JVM은 이를 인지하지 못하고 GC를 트리거하지 않는다.
- 결과적으로 컨테이너 메모리가 limit을 초과하면 커널의 OOM Killer가 JVM 프로세스를 강제 종료시킨다. Java 입장에서는 OutOfMemoryError를 던질 기회도 없이 SIGKILL(kill -9)로 즉사한다.
Go, Python 등 다른 언어는 OS 메모리를 직접 사용하기 때문에 메모리 사용량이 예측 가능하지만, Java는 JVM이 중간에서 메모리를 관리하기 때문에 컨테이너 limit과 JVM 설정 사이의 갭에서 OOM이 발생한다.
실무 가이드라인#
Java 컨테이너는 memory limit의 75% 이하로 힙 설정: 비힙 메모리 여유 필요
OOM 로그는 /var/log/kern.log에서 확인: dmesg는 재부팅 시 초기화됨
Swap 비활성화 환경에서 메모리 관리 중요: K8s 노드는 보통 swap off
Memory Request도 적절히 설정: 스케줄링 시 노드 선택에 영향
모니터링 필수: Grafana에서 메모리 사용 추이 주기적 확인
수정 대상 파일#
303-goormgb-k8s-helm/charts/java-cloud-gateway/values.yaml- resources.limits.memory: 512Mi → 1Gi
- resources.requests.memory: 128Mi → 512Mi