Published on

K8s CrashLoopBackOff 진단 - OOMKilled·Probe

Authors

CrashLoopBackOff는 쿠버네티스가 컨테이너를 반복 재시작하면서 백오프(backoff) 지연을 늘리는 상태입니다. 즉, CrashLoopBackOff 자체가 원인이 아니라 컨테이너가 왜 종료되는지가 핵심입니다. 현장에서 가장 흔한 축은 크게 두 가지입니다.

  • 메모리 부족으로 강제 종료되는 OOMKilled
  • 프로브(livenessProbe, readinessProbe, startupProbe) 설정/동작 문제로 재시작 루프

이 글은 kubectl 명령과 이벤트/로그/메트릭을 이용해, “무엇부터 확인해야 가장 빨리 좁혀지는지”를 중심으로 정리합니다.

1) 먼저 증상 분류: 종료 코드와 이벤트로 1분 컷

CrashLoopBackOff 진단은 아래 3가지만 먼저 보면 방향이 거의 결정됩니다.

  1. 마지막 종료 사유(Reason)와 종료 코드(Exit Code)
  2. Pod 이벤트(Events)에 찍힌 프로브 실패/리소스 압박 메시지
  3. 직전 로그(--previous)에 남은 스택트레이스/에러

1-1. 가장 먼저 보는 명령

kubectl get pod -n <ns>
kubectl describe pod -n <ns> <pod>

describe 결과에서 특히 아래를 찾습니다.

  • Last State: TerminatedReason, Exit Code
  • State: WaitingReason: CrashLoopBackOff
  • 하단 Events 섹션의 Killing, Unhealthy, Back-off restarting failed container

1-2. 직전 크래시 로그 확인

재시작이 반복되면 현재 컨테이너 로그는 이미 새 프로세스일 수 있습니다. 직전 인스턴스 로그는 --previous로 봅니다.

kubectl logs -n <ns> <pod> -c <container> --previous
  • 애플리케이션 예외(예: 설정 누락, DB 연결 실패, 마이그레이션 실패)가 원인이라면 여기서 바로 드러납니다.
  • 반대로 로그가 거의 없고 갑자기 끊기면 OOMKilled 또는 시그널 종료 가능성이 큽니다.

2) OOMKilled 진단: “limit에 맞춰 죽었는지”부터 확인

OOMKilled는 리눅스 커널 OOM killer가 컨테이너 프로세스를 강제 종료한 상황입니다. 쿠버네티스에서는 대개 memory limit을 초과했을 때 발생합니다.

2-1. OOMKilled 여부 확인

kubectl describe pod -n <ns> <pod>

아래와 비슷한 패턴이 보이면 거의 확정입니다.

  • Last State: Terminated / Reason: OOMKilled
  • 이벤트에 Killing container ... 와 함께 메모리 관련 메시지

또는 JSONPath로 종료 사유만 빠르게 뽑을 수도 있습니다.

kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason} {.status.containerStatuses[0].lastState.terminated.exitCode}'

2-2. requests/limits 설정이 현실적인지 점검

리소스 설정은 requests(스케줄링 기준)와 limits(강제 상한)로 나뉩니다.

kubectl get pod -n <ns> <pod> -o jsonpath='{.spec.containers[0].resources}'

자주 터지는 패턴은 다음과 같습니다.

  • limits.memory가 너무 낮음(예: JVM, Node.js, Python이 기본 런타임 오버헤드만으로도 버거움)
  • requests.memory가 너무 낮아 노드에 과밀 배치되어 실제 사용 중 경쟁이 심해짐
  • HPA는 CPU 기준인데 메모리로 죽음(스케일이 늘지 않아 OOM 반복)

2-3. 실제 메모리 사용량 확인(메트릭)

Metrics Server가 있다면:

kubectl top pod -n <ns>
kubectl top pod -n <ns> <pod> --containers
  • limits.memory 대비 피크가 얼마나 치는지 확인합니다.
  • top은 순간값이므로, 프로메테우스/그라파나가 있다면 max_over_time 같은 피크 지표를 보는 게 더 정확합니다.

2-4. 런타임별 흔한 원인과 대응

JVM

  • 힙을 limit보다 크게 잡으면 거의 확정적으로 OOMKilled
  • 컨테이너 환경에서는 -Xmx를 명시하거나, JVM 컨테이너 인식 옵션을 확인합니다.

예시(개념):

JAVA_TOOL_OPTIONS='-Xms256m -Xmx768m'

Node.js

  • 기본 old space가 커지거나 트래픽 급증 시 메모리 스파이크
  • --max-old-space-size로 상한을 컨테이너 limit보다 낮게 맞춥니다.
node --max-old-space-size=768 server.js

Python

  • 워커 수 과다(예: gunicorn workers)로 메모리 선형 증가
  • 캐시/대용량 객체 누수

2-5. 해결 체크리스트

  • limits.memory를 올리기 전에, 프로세스 메모리 상한을 런타임 옵션으로 먼저 통제
  • 피크 메모리 기반으로 requestslimits를 재산정
  • 메모리 기반 HPA 또는 VPA(정책 허용 시) 고려
  • 대량 캐시/버퍼는 외부(예: Redis)로 분리

3) Probe 실패 진단: “죽어서 재시작” vs “살아있는데 쿠버가 죽임”

프로브 실패는 애플리케이션이 실제로는 실행 중인데도, 쿠버네티스가 비정상으로 판단해 재시작시키는 전형적인 CrashLoopBackOff 원인입니다.

  • livenessProbe 실패: kubelet이 컨테이너를 죽이고 재시작
  • readinessProbe 실패: 트래픽에서 제외(재시작은 보통 안 함)
  • startupProbe 실패: 초기 기동이 느릴 때 liveness 오판을 막기 위한 안전장치

3-1. 이벤트에서 먼저 확인

kubectl describe pod -n <ns> <pod>

Events에 아래가 보이면 프로브 이슈입니다.

  • Unhealthy / Liveness probe failed
  • Readiness probe failed
  • Startup probe failed

3-2. 흔한 실패 원인 6가지

  1. 앱이 바인딩한 포트가 다름(컨테이너는 8080인데 프로브는 80)
  2. 앱이 0.0.0.0이 아니라 127.0.0.1 또는 특정 인터페이스에만 바인딩
  3. 초기화가 느린데 initialDelaySeconds가 너무 짧음
  4. DB/외부 의존성 준비 전에는 200을 못 주는데 liveness로 때려버림
  5. HTTP 경로가 리다이렉트/인증 필요로 401 또는 302 발생
  6. 타임아웃(timeoutSeconds)이 너무 짧아 간헐 실패로 누적 재시작

3-3. 권장 패턴: startupProbe로 초기 구간 보호

기동 시간이 들쭉날쭉한 서비스(마이그레이션, 캐시 워밍, 모델 로딩 등)는 startupProbe를 두고, 그 이후에만 liveness를 적용하는 방식이 안전합니다.

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  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
  timeoutSeconds: 2
  failureThreshold: 30
  • startupProbe가 성공하기 전까지는 liveness가 평가되지 않아, “기동 중인데 죽이는” 상황을 크게 줄입니다.

3-4. 프로브 엔드포인트 설계 팁

  • liveness는 프로세스가 살아있는지(치명적 데드락/이벤트루프 정지 등)만 판단
  • readiness는 트래픽을 받을 준비가 되었는지(DB 연결, 필수 의존성) 판단
  • 인증이 필요한 경로를 프로브로 쓰지 않기
  • 가능하면 가벼운 체크(외부 네트워크 호출 최소화)

4) CrashLoopBackOff인데 OOM도 Probe도 아니면

아래 케이스가 꽤 자주 남습니다.

4-1. Exit Code로 분기

  • Exit Code 1: 앱 예외 종료(설정, 마이그레이션, 권한, 연결 실패)
  • Exit Code 137: 대개 OOMKilled 또는 강제 SIGKILL
  • Exit Code 143: SIGTERM으로 정상 종료(종료 훅/프리셧다운에서 문제 가능)

종료 코드는 describe pod 또는 아래처럼 확인합니다.

kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'

4-2. ConfigMap/Secret 누락, 잘못된 엔트리포인트

  • 환경변수 누락으로 앱이 즉시 종료
  • 커맨드/아규먼트 오타로 프로세스가 바로 끝남
kubectl get deploy -n <ns> <deploy> -o yaml
kubectl get cm -n <ns>
kubectl get secret -n <ns>

4-3. 노드/클러스터 이슈(리소스 압박, 디스크)

노드가 MemoryPressureDiskPressure 상태면 정상 Pod도 연쇄적으로 불안정해질 수 있습니다.

kubectl describe node <node>

EKS 환경에서는 네트워크 문제로 Pod가 정상 스케줄링/통신을 못하는 경우도 있어, 증상이 CrashLoop과 섞여 보일 때가 있습니다. 특히 Pod가 뜨기 전 단계에서 막히는 이슈라면 아래 글도 함께 확인해두면 진단 범위를 줄일 수 있습니다.

5) 재현과 격리: 디버그용으로 프로브를 잠깐 끄는 방법

원인 파악을 위해 “일단 컨테이너를 살려두고” 내부를 확인해야 할 때가 있습니다. 이때 liveness가 계속 죽이면 조사 자체가 불가능합니다.

  • 운영 환경에서는 신중히(장애 확산 방지)
  • 임시로 livenessProbe를 제거하거나 failureThreshold를 크게 늘려 관찰

예: liveness를 제거한 임시 패치

kubectl patch deploy -n <ns> <deploy> --type='json' -p='[
  {"op":"remove","path":"/spec/template/spec/containers/0/livenessProbe"}
]'

그 후 exec로 들어가 프로세스 상태/포트 리슨 여부를 확인합니다.

kubectl exec -n <ns> -it <pod> -c <container> -- sh

6) 실전 트러블슈팅 순서(요약)

  1. kubectl describe podReason, Exit Code, Events 확인
  2. kubectl logs --previous로 직전 크래시 로그 확인
  3. OOMKilledlimits.memory와 런타임 메모리 상한을 함께 조정
  4. Unhealthy 이벤트면 프로브 경로/포트/타임아웃/초기 지연을 재설계하고 startupProbe 도입
  5. 그 외에는 설정 누락, 엔트리포인트 오류, 노드 압박 상태를 점검

7) 함께 보면 좋은 EKS 운영 이슈

CrashLoopBackOff 자체는 애플리케이션/프로브/리소스 문제인 경우가 많지만, EKS에서는 Ingress 계층에서의 타임아웃/헬스체크 실패가 “앱이 죽은 것처럼” 보이게 만드는 경우도 있습니다. 특히 readiness가 계속 실패하거나, 외부 요청이 408로 끊기며 재시도 폭주로 메모리가 급증하는 패턴이라면 아래 글이 도움이 됩니다.


CrashLoopBackOff는 “재시작이 반복된다”는 사실만 알려줄 뿐, 원인은 OOMKilled처럼 명확한 경우도 있고 프로브 설계처럼 구조적인 경우도 있습니다. 위 순서대로 종료 사유이벤트, 직전 로그를 먼저 확보하면, 불필요한 추측 없이 빠르게 원인을 좁히고 재발 방지까지 연결할 수 있습니다.