10.4 흔한 장애 패턴
Kubernetes 장애의 대부분은 처음 보는 문제가 아니라 몇 가지 패턴의 반복이다. 패턴마다 전형적인 증상이 있고, 확률 높은 원인 순서가 있고, 각 가설을 확정하거나 기각하는 명령이 있다. 이 흐름을 절차로 익혀 두면 장애 대응에서 추측이 차지하는 비중이 줄어든다. 모든 패턴에 공통인 첫 동작은 같다. kubectl describe로 Events 섹션을 읽는 것이다.
Pending: 스케줄되지 못한 Pod
증상은 kubectl get pods에서 STATUS가 Pending에 머물고 노드가 할당되지 않는 것이다. 컨테이너는 시작조차 하지 않았으므로 로그도 없다. Pending은 스케줄러가 조건을 만족하는 노드를 찾지 못했다는 뜻이고, 스케줄러는 그 이유를 FailedScheduling 이벤트에 그대로 적어 준다.
kubectl describe pod mypodEvents 섹션에 이런 메시지가 남는다.
Warning FailedScheduling 0/6 nodes are available: 3 Insufficient cpu,
2 node(s) had untolerated taint {dedicated: db}, 1 node(s) didn't match
Pod's node affinity/selector.메시지가 원인 후보를 노드 수로 분해해 주므로, 남은 일은 항목별 확인이다. 자원 부족이면 노드의 Allocatable 대비 requests 합계를 확인한다. 스케줄러가 보는 것은 실제 사용량이 아니라 requests 예약량이라는 점에 주의한다. 실사용이 한가한 클러스터라도 requests가 가득 차면 Pending이 난다.
kubectl describe nodes | grep -A 8 "Allocated resources"taint가 원인이면 describe node의 Taints 항목과 Pod의 tolerations를 대조한다. 배경은 Part VIII. 스케줄링과 자원 관리에서 다뤘다. PVC가 원인이면 이벤트에 “pod has unbound immediate PersistentVolumeClaims"가 남는다. kubectl get pvc로 PVC가 Pending인지 확인하고, StorageClass 오타나 프로비저너 부재를 의심한다. 다만 WaitForFirstConsumer 모드의 PVC는 Pod가 스케줄될 때까지 Pending이 정상이므로 그 자체는 이상이 아니다. 바인딩 동작의 상세는 Part VI. 스토리지 참고.
ImagePullBackOff: 이미지를 받지 못한다
증상은 STATUS가 ErrImagePull과 ImagePullBackOff를 오가는 것이다. BackOff는 실패한 pull의 재시도 간격을 늘려 가며 대기하는 상태다. 역시 describe의 Events가 시작점인데, 이 패턴은 에러 문구로 원인이 갈린다.
“not found"나 “manifest unknown"이면 이미지 이름 또는 태그가 레지스트리에 없는 것이다. 오타이거나, CI가 그 태그를 아직 push하지 않았거나, 태그가 삭제된 경우다. “unauthorized"나 “pull access denied"면 인증 문제다. 프라이빗 레지스트리인데 imagePullSecrets가 없거나, Secret은 있는데 Pod나 ServiceAccount에 연결되지 않았거나, 자격증명이 만료된 경우다.
# Pod가 실제로 요구하는 이미지 문자열 확인 (오타 검증)
kubectl get pod mypod -o jsonpath='{.spec.containers[*].image}'
# pull secret 연결 확인
kubectl get pod mypod -o jsonpath='{.spec.imagePullSecrets}'
kubectl get sa default -o jsonpath='{.imagePullSecrets}'확정은 재현으로 한다. 노드에 접근 가능하면 crictl pull <이미지>를 노드에서 실행해 같은 에러가 재현되는지 확인하고, 로컬에서는 docker pull이나 crane manifest로 태그 존재를 검증한다. 레지스트리의 rate limit도 같은 증상을 만들므로, 여러 노드에서 동시다발로 발생하면 그쪽을 의심한다.
CrashLoopBackOff와 OOMKilled: 반복해서 죽는다
증상은 STATUS가 CrashLoopBackOff이고 RESTARTS가 계속 오르는 것이다. 컨테이너는 시작에 성공했지만 곧 종료되고, kubelet이 10초, 20초, 40초로 두 배씩 늘어나는 대기(최대 5분) 후 재시작을 반복한다.
첫 명령은 kubectl logs -p다. 현재 인스턴스는 방금 시작해 아무 로그가 없기 쉽고, 죽은 이유는 직전 인스턴스의 마지막 로그에 있다.
kubectl logs mypod -p --tail=50로그로 원인이 보이지 않으면 종료 코드를 읽는다.
kubectl describe pod mypod
# Last State: Terminated
# Reason: Error (또는 OOMKilled, Completed)
# Exit Code: 1 (또는 137 등)가설 순서는 이렇다. 첫째, 앱 자체의 크래시. 로그에 스택트레이스가 남고 Exit Code가 1 같은 앱 정의 값이다. 둘째, 설정 오류. 필수 환경변수 누락, 참조하는 ConfigMap이나 Secret 부재, 볼륨 마운트 실패가 여기 속하고 보통 시작 직후 즉시 종료된다. describe로 env와 volume이 참조하는 대상이 실제로 존재하는지 확인한다. 셋째, liveness probe 오탐. 앱은 정상인데 probe가 너무 이르거나 엄격해서 kubelet이 종료시키는 경우다. Events에 “Liveness probe failed"가 반복되는데 앱 로그에는 이상이 없으면 이쪽이다. initialDelaySeconds를 늘리거나 startup probe를 도입한다.
Exit Code 137은 SIGKILL(128+9)로 죽었다는 뜻이다. Reason이 OOMKilled면 컨테이너의 메모리 사용이 limits에 도달해 커널 OOM killer가 프로세스를 종료한 것이다. 이때 로그는 아무 단서 없이 중간에 끊긴 듯 끝나는 것이 특징이라, 크래시 로그를 아무리 읽어도 원인이 없다면 종료 코드부터 다시 본다.
kubectl top pod mypod --containers # 현재 워킹셋과 limits 비교대응은 limits 재산정이다. 시작 직후 죽으면 초기 메모리 요구량이 limits보다 큰 것이고, 수 시간 뒤에 죽으면 누수나 캐시 증가를 의심한다. limits만 올리는 대응은 누수의 발현 주기를 늘릴 뿐이므로, 반복되면 힙 프로파일링으로 넘어간다.
무증상 장애: readiness 실패와 NotReady 노드
가장 발견이 늦는 장애는 아무것도 죽지 않는 장애다. readiness probe가 실패한 Pod는 재시작되지 않는다. Running 상태 그대로 Service의 endpoints에서 조용히 빠질 뿐이다. Pod 목록은 멀쩡한데 서비스 응답이 없거나 일부 요청만 실패한다면 이 패턴을 의심한다.
kubectl get endpointslice -l kubernetes.io/service-name=my-service
kubectl get pods -o wide # READY 컬럼이 0/1인 Pod 찾기
kubectl describe pod mypod # "Readiness probe failed" 이벤트 확인endpoints가 비어 있으면 Service 셀렉터 불일치이거나 모든 Pod의 readiness 실패다. READY 0/1인 Pod의 describe에서 probe 실패 메시지를 읽고, probe가 검사하는 경로를 kubectl exec나 port-forward로 직접 호출해 앱의 실제 상태와 대조한다. 의존 백엔드가 죽어 readiness가 연쇄 실패하는 경우 재시작으로는 아무것도 해결되지 않는다는 점이 CrashLoop 계열과 다르다.
노드 NotReady는 kubelet이 API server에 정상 상태를 보고하지 못하는 상황이다.
kubectl get nodes
kubectl describe node worker-1 # Conditions와 Events 확인Conditions에서 Ready가 Unknown이면 kubelet과의 통신 두절(kubelet 정지, 네트워크 단절, 노드 전원)이고, False면 kubelet이 살아서 문제를 보고하는 상태다. MemoryPressure나 DiskPressure가 함께 True면 자원 고갈 쪽이다. 확정은 노드에서 한다. journalctl -u kubelet으로 kubelet 로그를, systemctl status containerd로 런타임 상태를 확인한다. NotReady가 지속되면 기본 5분(not-ready toleration 300초) 후 그 노드의 Pod들이 eviction되어 다른 노드로 재배치되므로, 수습이 그 안에 끝날지가 대응 강도를 정한다.
정리
패턴별 첫 명령만 요약하면 다음과 같다. 어느 경우든 describe의 Events를 먼저 읽는 것은 공통이다.
| 증상 | 먼저 실행 | 주로 보는 것 |
|---|---|---|
| Pending | kubectl describe pod | FailedScheduling 메시지 |
| ImagePullBackOff | kubectl describe pod | pull 에러 문구(not found/unauthorized) |
| CrashLoopBackOff | kubectl logs -p | 직전 인스턴스 로그, Exit Code |
| OOMKilled | kubectl describe pod | Last State의 Reason, limits |
| 응답 없음(무재시작) | kubectl get endpointslice | endpoints 유무, READY 컬럼 |
| NotReady | kubectl describe node | Conditions, kubelet 로그 |