- Published on
K8s CrashLoopBackOff·OOMKilled 원인별 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 죽었다가 다시 뜨는 현상은 겉으로는 비슷해 보여도, Kubernetes에서는 원인이 매우 다양합니다. 특히 CrashLoopBackOff와 OOMKilled는 운영에서 가장 자주 마주치는 상태값인데, 둘 다 “컨테이너가 반복적으로 재시작된다”는 공통점 때문에 초기 대응이 꼬이기 쉽습니다.
이 글은 원인별로 증상을 분류하고, kubectl로 증거를 확보한 뒤, 가장 비용 대비 효과가 큰 해결책부터 적용할 수 있도록 구성했습니다. (EKS를 포함한 일반 K8s 기준)
1) CrashLoopBackOff vs OOMKilled: 무엇이 다른가
CrashLoopBackOff
- 의미: 컨테이너가 비정상 종료(Exit Code != 0) 를 반복하며, kubelet이 재시작을 시도하지만 백오프(backoff) 지연이 붙는 상태
- 핵심: “왜 종료됐는지”는 CrashLoopBackOff 자체가 말해주지 않습니다. 이전 종료 원인(Exit Code/Signal)과 로그를 봐야 합니다.
OOMKilled
- 의미: 컨테이너가 메모리 제한(limit)을 초과해 Linux OOM killer에 의해 강제 종료됨
- 특징: Pod 상태에
Reason: OOMKilled, 종료 코드가 종종137(SIGKILL)로 나타납니다.
둘은 겹칠 수 있습니다. 예를 들어 메모리 초과로 죽으면 결과적으로 재시작이 반복되어 CrashLoopBackOff처럼 보일 수 있습니다. 따라서 가장 먼저 “이전 종료 이유”를 확인해야 합니다.
2) 5분 내 1차 진단 체크리스트 (명령어 중심)
아래 순서대로 보면 원인 범위를 빠르게 좁힐 수 있습니다.
2.1 상태/이벤트 확인
kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>
State: Waiting/Reason: CrashLoopBackOff여부Last State: Terminated의Reason,Exit Code,Started/Finished- Events에
Back-off restarting failed container,Killing,OOMKilled,Unhealthy(probe 실패),FailedMount등이 있는지
2.2 이전 로그 확인 (가장 중요)
kubectl logs -n <ns> <pod> -c <container>
kubectl logs -n <ns> <pod> -c <container> --previous
- 재시작이 반복되면 현재 로그보다
--previous가 더 결정적입니다.
2.3 종료 코드로 원인 힌트 얻기
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
137: SIGKILL(대개 OOMKilled 또는 노드가 강제 종료)143: SIGTERM(정상 종료 요청 후 종료)1: 앱 내부 에러(설정/의존성/마이그레이션 실패 등)
2.4 리소스 사용량 확인 (metrics-server 필요)
kubectl top pod -n <ns> <pod>
kubectl top node
- 메모리가 limit 근처에서 치솟는지
- 노드 전체가 메모리 압박인지
2.5 (EKS 등) 노드 디스크/압박 이슈도 함께 확인
CrashLoopBackOff는 앱 문제처럼 보이지만, 노드가 불안정하면 연쇄적으로 발생합니다. 특히 디스크 압박은 이미지 풀/로그 쓰기 실패로 재시작을 유발할 수 있습니다.
관련해서는 EKS 노드 디스크 부족 Evicted 폭주 해결 가이드도 함께 점검하면 좋습니다.
3) CrashLoopBackOff 원인별 해결
CrashLoopBackOff는 “원인”이 아니라 “증상”입니다. 아래에서 가장 흔한 원인을 우선순위로 정리합니다.
3.1 애플리케이션이 즉시 종료됨 (설정/환경변수/시크릿)
대표 증상
kubectl logs --previous에Missing ENV,Cannot load config,Invalid credentials등Exit Code: 1이 흔함
해결
- ConfigMap/Secret 키 누락, 오타, 타입 오류 확인
envFrom사용 시 의도치 않은 키 덮어쓰기 주의
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: myapp:1.2.3
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: databaseUrl
팁: 컨테이너가 너무 빨리 죽어 exec가 불가능하면, 동일 이미지로 디버그 Pod를 띄워 환경을 재현하세요.
kubectl run -n <ns> debug --rm -it \
--image=myapp:1.2.3 --command -- sh
3.2 프로브(liveness/readiness/startup) 설정이 공격적임
대표 증상
- Events에
Unhealthy/Liveness probe failed반복 - 앱은 실제로 정상 부팅 중인데 kubelet이 죽여버림
해결 전략
- startupProbe로 “부팅 시간”을 분리
- readiness는 “트래픽 받을 준비”만 판단
- liveness는 “회복 불가능한 상태”만 판단
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 24 # 최대 120초 부팅 허용
운영 팁: readiness 실패는 재시작을 유발하지 않지만, liveness 실패는 재시작을 유발합니다. “외부 의존성(DB, Redis) 장애”를 liveness로 판단하면 장애 시 재시작 폭풍이 발생할 수 있습니다.
3.3 이미지/엔트리포인트/권한 문제
대표 증상
exec format error(아키텍처 불일치: arm64 이미지 vs amd64 노드)permission denied(실행 권한, 읽기 전용 FS)CrashLoopBackOff+ 로그가 거의 없음
해결
- 이미지 플랫폼 확인:
docker buildx사용 시--platform명시 - ENTRYPOINT/CMD 확인
- non-root 실행 시 파일 권한/디렉토리 쓰기 권한 부여
# 예: 실행 권한 부여
RUN chmod +x /app/start.sh
ENTRYPOINT ["/app/start.sh"]
K8s 보안 컨텍스트도 함께 점검합니다.
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
readOnlyRootFilesystem를 켠 상태에서 /tmp에 쓰는 라이브러리가 있으면 즉시 크래시할 수 있으니 emptyDir 마운트로 우회합니다.
3.4 의존성 실패(DB 연결, 마이그레이션, 외부 API)로 앱이 종료
대표 증상
- 로그에
connection refused,timeout,too many connections등 - 재시작 반복으로 더 악화(재시작마다 커넥션 폭증)
해결
- 앱을 “의존성 불가 시 즉시 종료”가 아니라 “재시도/서킷브레이커/지수 백오프”로 설계
- readiness에서 의존성 체크, liveness는 프로세스 생존 위주
DB 커넥션 고갈이 원인이라면 애플리케이션 레벨에서도 개선 여지가 큽니다. Java/Spring 계열이라면 Spring Boot 3 가상스레드로 DB 커넥션 고갈 막기 같은 접근도 참고할 만합니다.
3.5 노드/클러스터 레벨 문제(디스크, CNI, DNS)
대표 증상
- 특정 노드에 스케줄된 Pod만 반복 크래시
- 이미지 풀 실패, 볼륨 마운트 실패, DNS 해석 실패로 앱 종료
해결
- 노드 드레인 후 교체로 빠른 복구
kubectl cordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
- CoreDNS, CNI, 노드 디스크/메모리 압박 확인
4) OOMKilled 원인별 해결
OOMKilled는 비교적 명확합니다. 메모리 limit을 넘었거나, 노드가 극단적 메모리 압박을 겪는 상황입니다. 다만 “왜 메모리가 늘었는지”는 앱/런타임/트래픽/캐시/버그 등으로 다양합니다.
4.1 requests/limits 설정이 현실과 불일치
대표 증상
Reason: OOMKilled,Exit Code: 137- 트래픽 피크에만 주기적으로 발생
해결
- 현재 사용량 기반으로 limit 상향
- HPA와 함께 수평 확장으로 피크 분산
- requests는 스케줄링 안정성을 위해 너무 낮게 잡지 않기
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
메모리 limit만 올리면 “일단은” 해결되지만, 노드 전체 메모리를 잠식해 다른 워크로드를 압박할 수 있습니다. HPA가 함께 필요할 수 있고, HPA가 동작하지 않는 환경이라면 EKS에서 HPA가 안 늘어날 때 Metrics 오류 해결처럼 메트릭 파이프라인부터 점검해야 합니다.
4.2 JVM/Node.js/Go 런타임 메모리 상한 미설정
컨테이너 환경에서는 런타임이 cgroup limit을 “완벽히” 반영하지 못하거나, 기본 상한이 너무 커서 limit에 부딪히는 경우가 있습니다.
JVM 예시
- Java 10+는 cgroup 인식을 개선했지만, 여전히 명시적 상한이 안전합니다.
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError"
Node.js 예시
- V8 old space 기본값이 워크로드에 비해 커서 OOM이 날 수 있습니다.
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
4.3 메모리 릭(누수) 또는 캐시 무제한 증가
대표 증상
- 시간이 갈수록 메모리가 단조 증가
- 재시작하면 잠깐 정상, 다시 증가
해결
- 애플리케이션 레벨에서 힙 덤프/프로파일링
- 캐시(예: in-memory LRU) 상한 설정
- 요청 바디/응답 바디를 통째로 메모리에 올리는 코드 제거
운영에서 빠른 판별법은 “트래픽과 무관하게 증가하는가?”입니다. 무관하면 누수 가능성이 큽니다.
4.4 버퍼/큐 적체(백프레셔 부재)
메시지 소비자, 배치, 스트리밍 처리에서 흔합니다.
- 다운스트림이 느려지면 큐가 메모리에 쌓임
- 재시작은 오히려 리밸런싱/재처리로 부하를 키울 수 있음
해결
- 큐 길이/동시성 제한
- 백프레셔 적용(예: reactive stream)
- 워커 수 늘리기 전에 처리량 병목부터 제거
4.5 노드 메모리 압박으로 인한 간접 OOM
컨테이너 limit을 넘지 않았는데도 OOMKilled처럼 보이거나, 노드에서 여러 Pod가 연쇄적으로 죽는 경우가 있습니다.
점검
kubectl describe node <node>
# Conditions: MemoryPressure 확인
해결
- 노드 스케일 아웃/인스턴스 타입 상향
- 과도한 requests/limits로 노드 과밀 배치 방지
- 우선순위/중요 워크로드에
priorityClass적용
5) 재발 방지: 관측/설계/배포 설정
5.1 종료 원인 자동 수집(이벤트/Exit Code)
- Pod 이벤트를 로그로 수집(예: event-exporter)
lastState.terminated.reason/exitCode를 대시보드화
5.2 프로브와 종료 훅을 “재시작 친화적”으로
- SIGTERM 처리: graceful shutdown
- preStop 훅으로 커넥션 드레인
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 30
5.3 리소스 정책의 표준화
- LimitRange로 최소/최대 가드레일
- VPA(Vertical Pod Autoscaler)로 권장치 산출(조직 정책에 따라)
5.4 운영자 관점의 빠른 분기표
Reason=OOMKilled→ 메모리/limit/런타임 상한/누수 트랙Unhealthy이벤트 다수 → probe 트랙Exit Code=1+ 설정 관련 로그 → Config/Secret/의존성 트랙- 노드 특정/다발성 → 노드 리소스/디스크/CNI/DNS 트랙
6) 실전 예제: “OOMKilled → CrashLoopBackOff” 연쇄를 끊는 패치
상황: Node.js API가 피크 타임에 메모리 급증 → OOMKilled(137) → 재시작 반복(CrashLoopBackOff)
- Node 메모리 상한 설정
- limit 상향(임시)
- readiness 강화(부팅 직후 과부하 방지)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: my-api:2.0.0
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
readinessProbe:
httpGet:
path: /readyz
port: 3000
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
이 패치는 “메모리 폭발” 자체를 완전히 해결하진 못해도, 죽었다 살아나는 루프를 끊고(가용성 확보) 근본 원인(누수/캐시/요청 패턴)을 분석할 시간을 벌어줍니다.
마무리
CrashLoopBackOff와 OOMKilled는 Kubernetes 운영에서 가장 흔하지만, 동시에 가장 “빨리 진단할 수 있는” 신호이기도 합니다. 핵심은 상태값 자체에 매달리기보다:
describe의 Last State / Eventslogs --previous의 직전 크래시 로그top의 리소스 추세
이 3가지를 기준으로 원인을 분기하고, 프로브/리소스/런타임 상한/노드 상태를 순서대로 정리하는 것입니다.
장애가 Pod 내부 문제가 아니라 노드/클러스터(예: 디스크 압박)에서 시작되는 경우도 많으니, 필요하면 EKS 노드 디스크 부족 Evicted 폭주 해결 가이드와 같이 노드 레벨 신호도 함께 점검해 “재시작 루프”를 구조적으로 줄여보세요.