Published on

K8s CrashLoopBackOff - OOMKilled·Probe 실패 진단

Authors

CrashLoopBackOff는 쿠버네티스가 컨테이너를 반복 재시작하다가 백오프(backoff)를 걸어 재시작 간격을 늘리는 상태입니다. 중요한 점은 CrashLoopBackOff 자체가 원인이 아니라 증상이라는 것입니다. 원인은 크게 두 갈래로 나뉩니다.

  • 프로세스가 죽는다: 앱 크래시, 설정 오류, 의존성 실패, OOMKilled
  • 프로세스는 살아있지만 쿠버네티스가 죽인다: livenessProbe 실패, startupProbe 실패 등

이 글에서는 현장에서 가장 흔한 두 축인 OOMKilledProbe 실패를 빠르게 구분하고, 재발 방지까지 연결되는 진단 루틴을 제공합니다.

1) 먼저 “무슨 이유로 재시작됐는지”부터 확정

1-1. describe로 이벤트와 종료 사유 확인

가장 먼저 확인할 것은 Pod 이벤트와 컨테이너의 마지막 종료 사유입니다.

kubectl -n <namespace> describe pod <pod-name>
  • Last State: Terminated 아래 ReasonOOMKilled인지
  • Exit Code137(OOM에서 흔함), 1(일반 에러), 0(정상 종료 후 재시작 정책 때문에 반복)인지
  • Events에 Unhealthy(probe 실패), Back-off restarting failed container가 찍히는지

종종 kubectl get pod만 보고 추측하는데, describeEvents가 거의 정답을 줍니다.

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

CrashLoop에서는 현재 컨테이너가 이미 다시 뜬 상태일 수 있으니, 반드시 이전 로그를 봅니다.

kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
  • 앱이 직접 죽는 경우: 스택트레이스, 설정/환경변수 누락, 포트 바인딩 실패, DB 연결 실패 등이 로그에 남는 편
  • OOMKilled: 로그가 깔끔하게 끊기거나, 메모리 사용량이 급증하는 지표만 남고 종료 메시지가 없을 수 있음
  • Probe 실패: 앱 로그상으로는 잘 돌아가는데 재시작되는 경우가 많음

1-3. JSONPath로 종료 이유만 빠르게 뽑기

여러 Pod를 빠르게 훑고 싶다면 아래처럼 종료 이유를 추출합니다.

kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'

2) OOMKilled 진단: “limit에 맞춰 죽었는지, 진짜 누수인지”

OOMKilled는 컨테이너가 설정된 메모리 limit을 초과했을 때 커널 OOM에 의해 종료되는 전형적인 패턴입니다. 이때 쿠버네티스는 Reason: OOMKilled, Exit Code: 137로 표시합니다.

2-1. requests/limits 설정부터 확인

메모리 requestlimit이 어떻게 잡혀 있는지 확인합니다.

kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.containers[0].resources}{"\n"}'
  • limit이 너무 낮으면 정상 트래픽에서도 쉽게 OOM
  • request가 너무 낮으면 노드에서 과밀 배치되어 실제 가용 메모리가 부족해지고, 간접적으로 OOM이 잦아질 수 있음

2-2. 실제 사용량 확인(가능한 경우)

metrics-server가 있다면 아래로 현재 사용량을 봅니다.

kubectl -n <namespace> top pod <pod-name>

다만 CrashLoop 상황에서는 Pod가 오래 못 살아 top에 잘 안 잡힐 수 있습니다. 이 경우에는 다음을 병행합니다.

  • APM/프로메테우스 메트릭(컨테이너 메모리 working set)
  • 애플리케이션 자체 메모리 로그(힙 사용량 주기적 출력)

2-3. OOMKilled인데 limit을 올리기 전에 확인할 것

무조건 limit을 올리면 “증상 완화”는 되지만 원인이 남습니다. 다음 패턴을 먼저 분류하세요.

(1) 캐시/버퍼가 트래픽과 함께 선형으로 증가

  • 인메모리 캐시(예: LRU 미설정)
  • 요청 바디/응답을 통째로 메모리에 올리는 구현
  • 대용량 파일 처리 시 스트리밍이 아닌 readAll 스타일

(2) 동시성 증가로 태스크/고루틴/스레드가 누적

  • 백그라운드 워커가 실패 재시도로 쌓임
  • 큐 컨슈머가 ack 실패로 재처리 루프

이 유형은 언어/런타임별로 진단법이 다르지만, “동시성 누수”는 OOM의 상위 원인입니다. Rust 서비스라면 태스크 누수로 RAM이 폭증하는 케이스가 특히 자주 보입니다. 관련해서는 Rust Tokio task 누수로 RAM 폭증 진단법도 함께 참고하면 좋습니다.

(3) Node.js 계열: 힙 상한과 컨테이너 메모리 불일치

Node.js는 기본 힙 상한이 컨테이너 limit과 다르게 동작할 수 있습니다. 컨테이너 limit이 512Mi인데 V8 힙이 그 근처까지 커지면 네이티브 메모리까지 합쳐 OOM이 날 수 있습니다. 이때는 NODE_OPTIONS로 힙 상한을 명시적으로 제한하는 접근이 흔합니다.

env:
  - name: NODE_OPTIONS
    value: "--max-old-space-size=256"

단, 이는 임시 처방이므로 실제 메모리 사용 패턴(캐시, 버퍼, 동시성)을 함께 줄여야 합니다.

2-4. limit 조정의 원칙

  • 먼저 원인(누수/버퍼링/동시성)을 줄이고
  • 그 다음 피크 트래픽을 감안해 limit을 올리며
  • request는 스케줄링 안정성을 위해 현실적으로 설정합니다

예시(단순 예):

resources:
  requests:
    cpu: "200m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

3) Probe 실패 진단: “앱은 살아있는데 왜 죽이나”

Probe 실패는 앱 프로세스가 죽지 않아도 쿠버네티스가 컨테이너를 재시작시키는 대표 원인입니다.

  • startupProbe 실패: 기동이 느린 앱이 준비되기도 전에 죽음
  • livenessProbe 실패: 일시적 지연인데도 “죽었다”고 판단해 재시작
  • readinessProbe 실패: 재시작은 안 하지만 트래픽이 안 들어오며 장애처럼 보일 수 있음

3-1. 이벤트에서 probe 실패 메시지 확인

describe의 Events에 보통 아래 같은 로그가 남습니다.

  • Readiness probe failed: ...
  • Liveness probe failed: ...
  • Startup probe failed: ...

이 메시지의 핵심은 “어떤 엔드포인트를 어떤 방식으로 체크했는지”입니다.

3-2. probe 설정값 점검 체크리스트

다음 항목을 실제 YAML에서 확인합니다.

kubectl -n <namespace> get deploy <deploy-name> -o yaml
  • httpGet 경로가 맞는가(리다이렉트, 인증 필요, 404)
  • port가 컨테이너가 리슨하는 포트와 일치하는가
  • timeoutSeconds가 너무 짧지 않은가(기본 1초는 생각보다 짧음)
  • periodSecondsfailureThreshold 조합이 너무 공격적이지 않은가
  • 기동이 느리면 startupProbe를 별도로 두었는가

3-3. 가장 흔한 실수: liveness로 “의존성”을 체크

예를 들어 liveness가 DB 연결까지 확인하도록 구현되어 있으면, DB가 잠깐 느려져도 컨테이너를 재시작해 오히려 장애를 증폭시킵니다.

권장 패턴은 다음처럼 나누는 것입니다.

  • liveness: 프로세스가 살아있고 이벤트 루프가 멈추지 않았는지 같은 최소 조건
  • readiness: 외부 의존성(DB, 캐시, 다운스트림)까지 포함해 “트래픽을 받을 준비”가 됐는지

3-4. startupProbe로 “느린 기동”을 분리

Java/Spring, 대형 Node.js 번들, 마이그레이션 수행 등으로 초기 기동이 길면 startupProbe가 사실상 필수입니다.

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 60
  periodSeconds: 2
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3
  • startupProbe가 성공하기 전까지는 livenessProbe가 무시됩니다
  • 따라서 “기동 중에는 죽이지 말고, 떠오른 다음에만 엄격히 감시”가 가능합니다

3-5. probe는 성공인데도 재시작된다면

이 경우는 대개 아래 범주입니다.

  • 프로세스가 자체적으로 종료(예: 설정 오류로 즉시 exit)
  • SIGTERM 처리 미흡으로 종료가 빨라짐
  • preStop 훅이나 종료 시 정리 로직에서 크래시

직전 로그(--previous)와 함께 종료 코드, termination message를 확인하세요.

4) “로그도 exec도 안 된다”면 노드/네트워크 계층을 의심

CrashLoop 상황에서 kubectl logskubectl exec가 간헐적으로 실패하면, 단순히 Pod 문제를 넘어 kubelet 접근 문제일 수 있습니다. 특히 관리형 쿠버네티스(EKS)에서 보안그룹, 프록시, RBAC, 10250 포트 경로 문제로 이런 증상이 나옵니다.

이 경우는 애플리케이션 원인 분석과 별개로, 운영 경로를 먼저 복구해야 합니다. 관련 진단은 EKS에서 kubectl exec·logs가 안 될 때 진단법에 정리해 두었습니다.

5) 재현 가능한 트러블슈팅 루틴(현장용)

아래 순서대로 하면 “추측”을 줄이고 빠르게 수렴합니다.

5-1. 상태와 원인 확정

kubectl -n <namespace> get pod <pod-name> -o wide
kubectl -n <namespace> describe pod <pod-name>
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
  • OOMKilled인지
  • Unhealthy 이벤트(Probe 실패)인지
  • 앱 로그가 남는지

5-2. OOMKilled면

  1. 리소스 설정 확인
  2. 메모리 사용량(가능하면 top/메트릭) 확인
  3. 캐시/버퍼/동시성/런타임 힙 상한 점검
  4. 필요 시 limit 상향, HPA/큐 기반 스케일링 병행

5-3. Probe 실패면

  1. 이벤트의 실패 메시지로 실패 타입 확정
  2. probe 경로/포트/타임아웃/임계치 재검토
  3. 기동이 느리면 startupProbe 도입
  4. liveness는 최소 조건, readiness는 의존성 포함으로 분리

6) 운영에서 자주 쓰는 “안전한 임시 조치”

장애 대응 중에는 원인 분석과 별개로 서비스 복구가 필요합니다. 다만 임시 조치는 “원인을 가리는 방향”이 될 수 있어 기록을 남기고 되돌릴 계획이 있어야 합니다.

  • OOMKilled: 일시적으로 memory limit 상향, 트래픽 우회, 동시성 제한(워커 수 감소)
  • Probe 실패: timeoutSeconds 상향, failureThreshold 상향, startupProbe 추가
  • 즉시 크래시: 잘못된 환경변수/시크릿/ConfigMap 롤백, 직전 이미지 태그 롤백

7) 마무리: CrashLoopBackOff는 “분류”가 절반

CrashLoopBackOff를 보면 먼저 OOMKilled인지 Probe 실패인지부터 갈라야 합니다. 전자는 리소스와 메모리 사용 패턴(누수/버퍼링/동시성)을, 후자는 probe 설계(특히 liveness의 역할)와 타이밍 파라미터를 집중 점검하면 해결 속도가 급격히 빨라집니다.

진단 과정에서 노드나 kubelet 경로 문제로 관측 자체가 막히는 경우도 있으니, kubectl logs/exec가 불안정하다면 인프라 계층부터 복구한 뒤 애플리케이션 원인 분석을 진행하세요.