본문 바로가기
IT/k8s

[k8s] 쿠버네티스 Pod 트러블슈팅: CrashLoopBackOff, OOMKilled 완벽 해결

by 수누다 2026. 5. 4.

쿠버네티스 Pod가 계속 죽는다면? 당신만 그런 게 아닙니다

쿠버네티스를 처음 운영하다 보면 꼭 한 번씩 마주치는 상황이 있어요. 열심히 작성한 배포 파일을 kubectl apply로 올렸는데, Pod 상태가 CrashLoopBackOff거나 OOMKilled로 떠 있는 겁니다. 처음엔 진짜 당황스럽죠. 로그를 봐도 뭔가 무한루프처럼 에러가 쌓이고, 뭘 고쳐야 할지 막막하고요.

저도 입사 초반에 이거 때문에 밤새 삽질한 기억이 있습니다. 당시엔 kubectl describe pod 명령어조차 제대로 활용 못 했거든요. 지금은 쿠버네티스 Pod 트러블슈팅이 거의 반사적으로 손가락이 움직일 정도가 됐지만, 처음엔 정말 막막했어요. 이번 글에서는 제가 직접 겪고 해결한 경험을 바탕으로, CrashLoopBackOffOOMKilled 두 가지 대표적인 Pod 오류를 어떻게 진단하고 해결하는지 단계별로 풀어드릴게요.

▲ 쿠버네티스 Pod의 주요 상태 전환 흐름. Pending → Running → CrashLoopBackOff / OOMKilled 경로를 시각화한 다이어그램

CrashLoopBackOff와 OOMKilled, 이게 대체 뭔가요?

CrashLoopBackOff — "계속 죽었다 살아났다 반복"

쉽게 말해서, 컨테이너가 시작됐다가 바로 죽고, 쿠버네티스가 다시 살리고, 또 죽고를 반복하는 상태예요. 쿠버네티스는 기본적으로 컨테이너가 죽으면 재시작(restart)을 시도하는데, 계속 실패하면 재시작 간격을 점점 늘리면서 BackOff 상태로 전환됩니다. 10초, 20초, 40초... 이런 식으로요.

원인은 정말 다양합니다:

  • 애플리케이션 자체 버그 (예외 처리 안 된 panic, exit code 1)
  • 잘못된 환경 변수(Environment Variable) 설정
  • ConfigMap이나 Secret 마운트 실패
  • Liveness Probe(생존 확인 프로브) 설정 오류
  • 의존 서비스(DB, 외부 API 등)에 연결 못 하는 경우

OOMKilled — "메모리를 너무 많이 먹어서 강제 종료"

OOM은 Out Of Memory의 약자입니다. 컨테이너가 설정된 메모리 Limit(제한)을 초과하면, 리눅스 커널의 OOM Killer가 해당 프로세스를 강제로 죽여버려요. kubectl describe pod로 확인하면 OOMKilled라고 딱 찍혀 있고, exit code는 137이에요.

이건 크게 두 가지 경우인데요:

  • 메모리 Limit이 너무 낮게 설정된 경우: 실제 앱이 필요한 메모리보다 Limit이 작아서 죽는 거예요
  • 메모리 누수(Memory Leak)가 있는 경우: Limit은 적절한데 앱이 메모리를 계속 잡아먹고 반환 안 하는 경우
오류 유형 주요 원인 Exit Code 확인 명령어
CrashLoopBackOff 앱 오류, 설정 문제, Probe 실패 1, 2 등 다양 kubectl logs, kubectl describe
OOMKilled 메모리 Limit 초과, 메모리 누수 137 kubectl describe, metrics-server

1단계: 상황 파악 — 일단 뭐가 문제인지 보자

쿠버네티스 Pod 트러블슈팅의 첫 번째 단계는 항상 현재 상태 파악이에요. 저는 이 순서대로 확인합니다.

Pod 상태 전체 확인

# 네임스페이스 전체 Pod 상태 확인
kubectl get pods -n <네임스페이스> -o wide

# 모든 네임스페이스에서 문제 있는 Pod만 필터링
kubectl get pods -A | grep -v Running | grep -v Completed

여기서 RESTARTS 컬럼 숫자가 높으면 CrashLoopBackOff 의심이에요. 한 자리면 괜찮은데, 두세 자리 넘어가면 심각한 거거든요.

Pod 상세 이벤트 확인 — 이게 핵심입니다

kubectl describe pod  -n <네임스페이스>

출력 맨 아래 Events: 섹션을 꼭 보세요. 여기에 실제로 무슨 일이 있었는지 타임라인이 찍혀 있어요. OOMKilled라면 이런 식으로 보입니다:

Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Warning  BackOff    2m (x5 over 5m)   kubelet            Back-off restarting failed container
  Normal   Pulled     6m                kubelet            Successfully pulled image
  Normal   Started    6m                kubelet            Started container app
  Warning  OOMKilling 5m                kubelet            Memory limit reached, killing container

로그 확인 — 현재 로그와 이전 컨테이너 로그

# 현재 컨테이너 로그
kubectl logs  -n <네임스페이스>

# 이전에 죽은 컨테이너 로그 (이게 더 중요할 때가 많아요!)
kubectl logs  -n <네임스페이스> --previous

# 실시간 로그 스트리밍
kubectl logs -f  -n <네임스페이스>

# 멀티 컨테이너 Pod인 경우 컨테이너 지정
kubectl logs  -c  -n <네임스페이스> --previous

💡 : --previous 플래그를 꼭 써보세요. 현재 로그에는 아무것도 없어도, 이전에 죽을 때의 로그가 여기 남아 있거든요. 저도 처음엔 이걸 몰라서 한참 헤맸습니다 ㅎㅎ

▲ kubectl describe pod 명령어 실행 결과. Events 섹션에서 OOMKilled 및 CrashLoopBackOff 원인을 확인하는 화면

2단계: CrashLoopBackOff 원인별 해결법

케이스 1: 애플리케이션 자체 오류

로그에서 스택 트레이스(stack trace)나 panic, exception 같은 키워드가 보인다면 앱 코드 문제예요. 이건 개발팀에 공유해야 하는 케이스고, 인프라 엔지니어 입장에서 할 수 있는 건 명확한 로그를 전달하는 거예요.

# 마지막 100줄 로그 확인
kubectl logs  --previous --tail=100 -n <네임스페이스>

케이스 2: Liveness Probe 설정 오류

이게 은근히 많은 케이스더라고요. Liveness Probe(생존 확인 프로브)가 너무 엄격하게 설정되어 있으면, 앱이 정상인데도 쿠버네티스가 죽었다고 판단해서 재시작시켜 버립니다.

# 잘못된 설정 예시 — 너무 빠른 초기 딜레이
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 3   # 앱 시작에 10초 걸리는데 3초만 기다림
  periodSeconds: 5
  failureThreshold: 1      # 1번만 실패해도 재시작

---

# 올바른 설정 예시
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30  # 앱 시작 시간보다 여유 있게
  periodSeconds: 10
  failureThreshold: 3      # 3번 연속 실패해야 재시작
  timeoutSeconds: 5        # 응답 타임아웃도 여유 있게

⚠️ 주의: initialDelaySeconds는 컨테이너 시작 후 처음 Probe를 시도하기 전 대기 시간이에요. 앱이 완전히 뜨는 데 걸리는 시간보다 넉넉하게 잡아야 합니다. 저는 보통 실제 시작 시간의 1.5~2배로 잡아요.

케이스 3: ConfigMap / Secret 마운트 실패

# ConfigMap 존재 여부 확인
kubectl get configmap -n <네임스페이스>

# Secret 존재 여부 확인
kubectl get secret -n <네임스페이스>

# describe에서 Volume 마운트 실패 메시지 확인
kubectl describe pod  -n <네임스페이스> | grep -A5 "Volumes"

describe 출력에서 이런 메시지가 보이면 ConfigMap이나 Secret이 없는 겁니다:

Warning  FailedMount  10s   kubelet  MountVolume.SetUp failed for volume "config" :
configmap "my-app-config" not found

케이스 4: 환경 변수 누락

# 실행 중인 Pod의 환경 변수 확인
kubectl exec  -n <네임스페이스> -- env | sort

# Pod가 죽어서 exec가 안 되는 경우 — 임시 디버그 Pod 실행
kubectl run debug-pod --image=busybox -it --rm -- /bin/sh

3단계: OOMKilled 해결법

현재 메모리 사용량 확인

먼저 실제로 얼마나 쓰고 있는지 봐야죠. metrics-server가 설치되어 있다면:

# Pod 리소스 사용량 확인
kubectl top pod  -n <네임스페이스>

# 컨테이너별 상세 확인
kubectl top pod  -n <네임스페이스> --containers

현재 설정된 Limit 확인:

kubectl get pod  -n <네임스페이스> -o jsonpath='{.spec.containers[*].resources}'

메모리 Limit 조정

실제 사용량이 Limit에 근접하거나 초과한다면 Limit을 올려야 해요. 이건 Deployment를 수정해야 합니다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:latest
        resources:
          requests:
            memory: "256Mi"   # 스케줄링 기준 (최소 보장량)
            cpu: "250m"
          limits:
            memory: "512Mi"   # 최대 허용량 (이 이상 쓰면 OOMKilled)
            cpu: "500m"

💡 Request와 Limit의 차이:

  • requests: 쿠버네티스가 Pod를 노드에 배치할 때 기준이 되는 최소 보장량이에요. 이 공간이 있는 노드에만 배치됩니다.
  • limits: 컨테이너가 절대 넘을 수 없는 상한선이에요. 메모리 Limit을 넘으면 OOMKilled, CPU Limit을 넘으면 쓰로틀링(throttling)이 발생해요.

메모리 누수 의심 케이스

Limit을 올렸는데도 계속 OOMKilled가 난다면 메모리 누수를 의심해야 해요. 이 경우엔 시간에 따른 메모리 증가 패턴을 봐야 합니다. Prometheus + Grafana 조합이 있다면 메모리 사용량 그래프를 확인하세요. 선형으로 계속 올라간다면 누수 가능성이 높습니다.

# 메모리 사용량 변화를 5초 간격으로 모니터링
watch -n 5 kubectl top pod  -n <네임스페이스>

누수가 확인되면 개발팀에 공유하고, 임시 방편으로는 Deployment에 주기적 재시작을 설정하는 방법도 있어요 (권장하진 않지만요):

# CronJob으로 주기적 재시작 — 근본 해결책은 아닙니다!
apiVersion: batch/v1
kind: CronJob
metadata:
  name: restart-my-app
spec:
  schedule: "0 4 * * *"  # 매일 새벽 4시
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: kubectl
            image: bitnami/kubectl
            command:
            - kubectl
            - rollout
            - restart
            - deployment/my-app
          restartPolicy: OnFailure

▲ Grafana 대시보드에서 Pod 메모리 사용량 추이를 모니터링하는 화면. 메모리 누수 시 선형 증가 패턴이 확인됨

4단계: 고급 디버깅 — 그래도 모르겠을 때

임시 디버그 컨테이너 (kubectl debug)

쿠버네티스 1.23 이상이라면 kubectl debug를 활용할 수 있어요. 이미지에 쉘이 없거나, distroless 이미지를 쓰는 경우에도 쿠버네티스 Pod를 디버깅할 수 있습니다.

# 실행 중인 Pod에 디버그 컨테이너 추가
kubectl debug -it  -n <네임스페이스> \
  --image=busybox \
  --target=

# Pod를 복사해서 디버그 버전으로 실행
kubectl debug  -n <네임스페이스> \
  -it \
  --copy-to=debug-pod \
  --image=ubuntu \
  -- bash

Pod가 계속 죽어서 exec가 안 될 때

이건 진짜 난감한 상황인데요. 이럴 때 쓰는 트릭이 있어요. 컨테이너 커맨드를 sleep infinity로 덮어써서 앱이 실행되지 않은 상태에서 내부를 들여다보는 겁니다:

# 임시로 커맨드를 sleep으로 덮어쓰기
kubectl debug  -n <네임스페이스> \
  --copy-to=debug-pod \
  --image= \
  -- sleep infinity

# 그 다음 exec로 들어가서 환경 확인
kubectl exec -it debug-pod -n <네임스페이스> -- bash

네트워크 연결 문제 확인

DB나 외부 서비스 연결 실패로 CrashLoopBackOff가 나는 경우도 많아요:

# 네임스페이스 내 Service 확인
kubectl get svc -n <네임스페이스>

# DNS 해석 확인 (임시 Pod 사용)
kubectl run dns-test --image=busybox -it --rm -n <네임스페이스> \
  -- nslookup my-db-service

# TCP 연결 확인
kubectl run tcp-test --image=busybox -it --rm -n <네임스페이스> \
  -- nc -zv my-db-service 5432

검증 — 제대로 고쳐졌는지 확인하기

수정 후에는 꼭 아래 순서로 확인해요:

  1. Pod 상태가 Running으로 안정적으로 유지되는지 확인
  2. RESTARTS 카운트가 더 이상 올라가지 않는지 확인
  3. 로그에 정상 동작 메시지가 찍히는지 확인
  4. Readiness Probe(준비 확인 프로브)가 통과하는지 확인
# 실시간으로 Pod 상태 변화 모니터링
kubectl get pods -n <네임스페이스> -w

# Rollout 상태 확인
kubectl rollout status deployment/ -n <네임스페이스>

# 최근 이벤트 확인
kubectl get events -n <네임스페이스> --sort-by='.lastTimestamp' | tail -20

🎉 kubectl get pods에서 STATUS가 Running이고 READY가 1/1이면서 RESTARTS가 안 올라가면 성공입니다!

쿠버네티스 Pod 트러블슈팅 체크리스트 정리

확인 항목 CrashLoopBackOff OOMKilled 명령어
Pod 상태 확인 kubectl get pods
이벤트 로그 확인 kubectl describe pod
이전 컨테이너 로그 kubectl logs --previous
Probe 설정 확인 - kubectl get pod -o yaml
메모리 사용량 확인 - kubectl top pod
리소스 Limit 조정 - kubectl edit deployment
ConfigMap/Secret 확인 - kubectl get cm,secret
네트워크 연결 확인 - kubectl run (임시 Pod)

▲ CrashLoopBackOff와 OOMKilled 트러블슈팅 플로우차트. 증상별 진단 경로와 해결 방법을 한눈에 정리한 인포그래픽

마무리 — 결국 로그가 답이더라고요

여러 해 동안 수많은 인프라 사고를 겪어오면서 느낀 건, 쿠버네티스 Pod 트러블슈팅의 핵심은 결국 로그와 이벤트를 제대로 읽는 것이에요. 화려한 도구가 없어도, kubectl describekubectl logs --previous 두 개만 잘 써도 대부분의 문제는 해결됩니다.

CrashLoopBackOff는 원인이 다양하니까 로그를 꼼꼼히 보고 하나씩 제거해 나가는 방식이 좋아요. OOMKilled는 일단 메모리 사용량 측정부터 시작해서 Limit 조정이냐, 누수 수정이냐를 판단하면 되고요.

정리하면:

  • CrashLoopBackOff: kubectl logs --previous로 죽기 직전 로그 확인 → Probe 설정, 환경 변수, 마운트 순서로 체크
  • OOMKilled: kubectl top pod로 실제 사용량 확인 → Limit 상향 조정 또는 누수 수정
  • ✅ 막막할 땐 kubectl debug로 임시 컨테이너 붙여서 내부 확인

다음 글에서는 Pending 상태에서 Pod가 뜨지 않는 경우 (노드 리소스 부족, Taints/Tolerations 문제 등)를 다뤄볼 예정입니다. Pod가 아예 시작조차 안 된다면 그쪽 글을 참고해 주세요!

혹시 이 글에서 다루지 않은 케이스로 고생하고 계신 분 있으시면 댓글로 남겨주세요. 같이 고민해 볼게요 😊