목차
- 쿠버네티스 Pod가 계속 죽는다면? 당신만 그런 게 아닙니다
- CrashLoopBackOff와 OOMKilled, 이게 대체 뭔가요?
- CrashLoopBackOff — "계속 죽었다 살아났다 반복"
- OOMKilled — "메모리를 너무 많이 먹어서 강제 종료"
- 1단계: 상황 파악 — 일단 뭐가 문제인지 보자
- Pod 상태 전체 확인
- Pod 상세 이벤트 확인 — 이게 핵심입니다
- 로그 확인 — 현재 로그와 이전 컨테이너 로그
- 2단계: CrashLoopBackOff 원인별 해결법
- 케이스 1: 애플리케이션 자체 오류
- 케이스 2: Liveness Probe 설정 오류
- 케이스 3: ConfigMap / Secret 마운트 실패
- 케이스 4: 환경 변수 누락
- 3단계: OOMKilled 해결법
- 현재 메모리 사용량 확인
- 메모리 Limit 조정
- 메모리 누수 의심 케이스
- 4단계: 고급 디버깅 — 그래도 모르겠을 때
- 임시 디버그 컨테이너 (kubectl debug)
- Pod가 계속 죽어서 exec가 안 될 때
- 네트워크 연결 문제 확인
- 검증 — 제대로 고쳐졌는지 확인하기
- 쿠버네티스 Pod 트러블슈팅 체크리스트 정리
- 마무리 — 결국 로그가 답이더라고요
쿠버네티스 Pod가 계속 죽는다면? 당신만 그런 게 아닙니다
쿠버네티스를 처음 운영하다 보면 꼭 한 번씩 마주치는 상황이 있어요. 열심히 작성한 배포 파일을 kubectl apply로 올렸는데, Pod 상태가 CrashLoopBackOff거나 OOMKilled로 떠 있는 겁니다. 처음엔 진짜 당황스럽죠. 로그를 봐도 뭔가 무한루프처럼 에러가 쌓이고, 뭘 고쳐야 할지 막막하고요.
저도 입사 초반에 이거 때문에 밤새 삽질한 기억이 있습니다. 당시엔 kubectl describe pod 명령어조차 제대로 활용 못 했거든요. 지금은 쿠버네티스 Pod 트러블슈팅이 거의 반사적으로 손가락이 움직일 정도가 됐지만, 처음엔 정말 막막했어요. 이번 글에서는 제가 직접 겪고 해결한 경험을 바탕으로, CrashLoopBackOff와 OOMKilled 두 가지 대표적인 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
검증 — 제대로 고쳐졌는지 확인하기
수정 후에는 꼭 아래 순서로 확인해요:
- Pod 상태가
Running으로 안정적으로 유지되는지 확인 - RESTARTS 카운트가 더 이상 올라가지 않는지 확인
- 로그에 정상 동작 메시지가 찍히는지 확인
- 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 describe와 kubectl logs --previous 두 개만 잘 써도 대부분의 문제는 해결됩니다.
CrashLoopBackOff는 원인이 다양하니까 로그를 꼼꼼히 보고 하나씩 제거해 나가는 방식이 좋아요. OOMKilled는 일단 메모리 사용량 측정부터 시작해서 Limit 조정이냐, 누수 수정이냐를 판단하면 되고요.
정리하면:
- ✅ CrashLoopBackOff:
kubectl logs --previous로 죽기 직전 로그 확인 → Probe 설정, 환경 변수, 마운트 순서로 체크 - ✅ OOMKilled:
kubectl top pod로 실제 사용량 확인 → Limit 상향 조정 또는 누수 수정 - ✅ 막막할 땐
kubectl debug로 임시 컨테이너 붙여서 내부 확인
다음 글에서는 Pending 상태에서 Pod가 뜨지 않는 경우 (노드 리소스 부족, Taints/Tolerations 문제 등)를 다뤄볼 예정입니다. Pod가 아예 시작조차 안 된다면 그쪽 글을 참고해 주세요!
혹시 이 글에서 다루지 않은 케이스로 고생하고 계신 분 있으시면 댓글로 남겨주세요. 같이 고민해 볼게요 😊
'IT > k8s' 카테고리의 다른 글
| [k8s] Kubernetes Longhorn 스토리지: 설치부터 운영까지 완벽 가이드 (0) | 2026.05.09 |
|---|---|
| [쿠버네티스] Nginx vs Traefik Ingress Controller 선택 가이드: 13년차 엔지니어의 경험담 (0) | 2026.05.08 |
| [k8s] Istio 서비스 메시 완벽 가이드: 설치부터 트래픽 관리, 보안까지 (0) | 2026.05.04 |
| [k8s] ArgoCD 프로덕션 환경 GitOps 베스트 프랙티스: 안정적인 배포와 보안 강화 (0) | 2026.04.30 |
| [k8s] 쿠버네티스 영구 스토리지: Longhorn vs Rook Ceph 비교 및 선택 가이드 (0) | 2026.04.27 |
| [k8s] Helm Chart 베스트 프랙티스: 프로덕션 배포 및 관리 전략 (0) | 2026.04.27 |