- Published on
K8s CrashLoopBackOff와 livenessProbe 503 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff가 뜨면서 이벤트를 보면 Liveness probe failed: HTTP probe failed with statuscode: 503 같은 메시지가 반복되는 경우가 있습니다. 이 상황은 단순히 "앱이 죽는다"가 아니라, 쿠버네티스가 컨테이너를 "살아있지 않다"고 판단해 재시작을 강제하면서 장애를 증폭시키는 패턴입니다.
핵심은 두 가지를 분리하는 것입니다.
- 503이 "정상적인 워밍업 중"에 잠깐 발생하는가
- 503이 "진짜 비정상 상태"(의존성 장애, 데드락, 리소스 고갈 등)에서 지속되는가
이 글은 위 두 케이스를 빠르게 구분하고, livenessProbe와 readinessProbe 설계를 바로잡아 CrashLoopBackOff를 멈추는 실전 가이드입니다.
먼저 결론: liveness는 "프로세스 생존"만, readiness는 "트래픽 수용"만
livenessProbe는 "컨테이너를 재시작해야 할 정도로" 망가졌는지 판단해야 합니다. 반면 readinessProbe는 "지금 트래픽을 받을 준비"가 됐는지 판단합니다.
그런데 많은 서비스가 /health 하나로 둘 다 처리하면서 문제가 생깁니다.
- DB 연결이 아직 안 됨
->/health가 503 반환->liveness 실패->재시작->영원히 기동 못 함 - 외부 API 장애
->헬스가 503->재시작 반복->장애 전파
따라서 다음을 권장합니다.
- liveness: 프로세스가 살아 있고 이벤트 루프가 돌고 있는지, 내부적으로 치명적 상태인지
- readiness: DB, 캐시, 큐 등 의존성이 준비됐는지(트래픽을 받을 수 있는지)
증상 확인: CrashLoopBackOff가 "왜" 발생하는지 3분 진단
아래 순서대로 보면 원인이 빠르게 좁혀집니다.
1) 이벤트에서 실패한 probe와 타이밍 확인
kubectl describe pod -n myns mypod
Events 섹션에서 다음을 확인합니다.
Liveness probe failed인지Readiness probe failed인지- 실패 status code가 503인지, timeout인지
- 재시작 주기(예: 30초마다 죽는지)
2) 컨테이너 종료 사유와 종료 코드 확인
kubectl get pod -n myns mypod -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{" "}{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
OOMKilled면 probe보다 리소스가 먼저 원인일 수 있습니다.- exit code 137은 OOM/강제 종료 가능성이 큽니다.
3) 재시작 직전 로그 확인
CrashLoopBackOff에서는 현재 로그가 비어 있을 수 있으니 --previous가 중요합니다.
kubectl logs -n myns mypod -c app --previous
여기서 "서버가 아직 바인딩 전"인지, "DB 연결 실패"인지, "마이그레이션 중"인지가 보이면 probe 설계 문제일 확률이 높습니다.
왜 503이 liveness에서 치명적인가
HTTP probe는 단순합니다. kubelet이 Pod IP로 접속해서 HTTP 상태 코드를 봅니다.
- 200-399
->성공 - 그 외(503 포함)
->실패
즉 애플리케이션이 "준비 안 됨"을 표현하려고 503을 반환했더라도, livenessProbe에 걸려 있으면 kubelet은 "죽었다"고 판단하고 재시작합니다.
또한 503이 아니라도 다음 케이스가 자주 섞입니다.
- 서버 바인딩 전에 probe가 먼저 들어와 connection refused
- 워밍업 시간이 긴데
initialDelaySeconds가 너무 짧음 - GC, JIT, 콜드 스타트로 일시적으로 응답이 느려
timeoutSeconds초과
가장 흔한 원인 5가지와 해결 체크리스트
1) liveness에 readiness 성격을 섞어둔 경우
대표적으로 /health에서 DB ping을 하고, 실패 시 503을 반환하는 구현입니다.
해결:
- liveness용 엔드포인트는 DB를 보지 않게 분리
- readiness는 DB/캐시 등 의존성을 확인
예시(권장):
/live->프로세스/스레드/이벤트루프 체크, 내부 치명 상태만 실패/ready->DB/Redis/외부 의존성 준비 여부
2) 기동 시간이 긴데 initialDelaySeconds가 너무 짧은 경우
특히 Spring Boot, Node.js 대형 번들, Python 모델 로딩, 마이그레이션 수행 시 흔합니다.
해결:
startupProbe를 추가해 "기동 완료 전"에는 liveness를 무시하게 만들기- 또는
initialDelaySeconds를 늘리기(하지만 startupProbe가 더 안전)
3) HTTP 503을 "정상적 워밍업" 표현으로 쓰는 경우
워밍업 중 503은 readiness에서는 좋지만, liveness에서는 위험합니다.
해결:
- 워밍업 중에는 readiness만 실패하도록 설계
- liveness는 200을 유지하되, 진짜 치명 상태에서만 실패
4) 리소스 부족으로 응답이 느려져 timeout이 나는 경우
CPU throttling, 메모리 압박, GC 폭주가 있으면 probe가 timeout으로 실패합니다.
해결:
timeoutSeconds를 현실적으로 조정resources.requests를 올려 스케줄링 품질 개선- 필요 시 HPA, JVM 옵션, Node.js 메모리 옵션 조정
5) 서비스가 0.0.0.0이 아닌 localhost에만 바인딩된 경우
컨테이너 내부에서는 떠 있지만 Pod IP로 접근이 안 되어서 실패합니다.
해결:
- 서버 바인딩을
0.0.0.0로 - 프레임워크별 기본값 점검
정석 설정: startupProbe + readinessProbe + livenessProbe
아래는 "기동이 느릴 수 있는" 웹 앱의 안전한 프로브 템플릿입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: app
image: myapp:1.0.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /live
port: 8080
failureThreshold: 60
periodSeconds: 2
timeoutSeconds: 1
livenessProbe:
httpGet:
path: /live
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
포인트:
startupProbe가 성공하기 전까지 liveness가 평가되지 않으므로, 콜드 스타트가 길어도 CrashLoopBackOff로 가지 않습니다./live는 "DB 없어도" 200을 줄 수 있어야 합니다./ready는 DB 연결, 마이그레이션 완료, 캐시 준비 등을 확인해 트래픽을 안전하게 차단합니다.
애플리케이션 엔드포인트 설계 예시
여기서는 "liveness는 가볍게, readiness는 엄격하게"를 코드로 보여줍니다.
Node.js Express 예시
import express from "express";
const app = express();
let ready = false;
let fatal = false;
async function init() {
try {
// 예: DB 연결, 캐시 워밍업 등
// 실패해도 바로 fatal로 두지 말고, 재시도 정책을 두는 편이 낫습니다.
await connectDbWithRetry();
ready = true;
} catch (e) {
fatal = true; // 정말 복구 불가일 때만
}
}
app.get("/live", (req, res) => {
if (fatal) return res.status(500).send("fatal");
return res.status(200).send("ok");
});
app.get("/ready", async (req, res) => {
if (!ready) return res.status(503).send("not ready");
// 필요 시 DB ping, 의존성 체크
return res.status(200).send("ready");
});
app.listen(8080, "0.0.0.0", () => {
init();
});
여기서 503은 /ready에만 사용합니다. /live에서 503을 반환하면 kubelet이 재시작을 걸 가능성이 커집니다.
Spring Boot Actuator를 쓰는 경우(개념)
Spring Boot는 Actuator로 liveness/readiness를 분리할 수 있습니다. 운영에서는 보통 다음을 조합합니다.
- liveness:
livenessState - readiness:
readinessState
그리고 ingress나 service는 readiness를 기준으로 트래픽을 분배합니다.
주의: 본문에 경로를 적을 때 부등호가 들어가는 문법은 피하고, 실제 설정은 사용하는 Spring Boot 버전의 Actuator 가이드를 따르세요.
503이 진짜로 "앱 내부"에서 오는지 확인하는 방법
때로는 앱이 아니라 사이드카/프록시(예: Envoy, Nginx)에서 503을 만들기도 합니다. 다음처럼 Pod 내부에서 직접 호출해 분리합니다.
임시 디버그 컨테이너로 내부 호출
kubectl debug -n myns -it mypod --image=curlimages/curl --target=app -- sh
디버그 셸에서:
curl -v http://127.0.0.1:8080/live
curl -v http://127.0.0.1:8080/ready
127.0.0.1로는 200인데 probe는 실패한다->바인딩/포트/네트워크 정책 의심- 둘 다 503
->앱 로직 또는 의존성 문제
네트워크 자체가 의심되면, EKS 환경에서 Pod 간 통신 문제를 빠르게 진단하는 글도 함께 참고할 만합니다: EKS Pod간 통신만 실패? MTU·PMTUD 10분 진단
CrashLoopBackOff를 멈추기 위한 "응급 처치" 순서
장애 상황에서 즉시 재시작 폭풍을 멈추고 원인 분석 시간을 벌어야 할 때가 있습니다.
- liveness를 일시적으로 완화하거나 제거(가장 빠름)
- 단, 제거하면 진짜로 죽은 프로세스도 살아있는 것으로 간주될 수 있으니, 임시 조치로만 사용하세요.
startupProbe추가
- 재배포가 가능하다면 가장 추천되는 응급 조치입니다.
- readiness를 강화해 트래픽을 차단
- 장애가 외부 의존성(DB 등)이라면, 재시작보다 "트래픽 차단"이 더 안전합니다.
- 리소스 상향
- OOM 또는 CPU throttling이 의심되면
requests부터 올려 재현성을 낮춥니다.
스토리지 마운트 실패가 기동을 막아 probe 실패로 이어지는 케이스도 종종 있습니다. PV 관련 이벤트가 보이면 이 글이 빠른 우회로가 됩니다: EKS PV MountFailed - wrong fs type 해결법
운영 팁: 프로브 튜닝 기준(실무에서 자주 쓰는 값)
서비스 성격에 따라 다르지만, 기준점을 잡는 데 도움이 되는 룰을 정리합니다.
timeoutSeconds: p99 응답 시간보다 여유 있게(보통 1~3초). GC가 큰 런타임이면 더 크게.periodSeconds: readiness는 3~5초, liveness는 10초 정도가 무난failureThreshold: liveness는 3 이상 권장(순간적인 hiccup에 재시작 방지)startupProbe.failureThreshold * periodSeconds: 최악의 콜드 스타트 시간보다 크게
추가로, 앱이 외부 API에 크게 의존해서 가끔 5xx가 나는 구조라면 "헬스 체크는 외부 의존성에 끌려가면 안 된다"는 원칙을 지키는 것이 중요합니다. 외부 API 장애의 전형적인 패턴은 502/503이 섞여 나오며, 원인 분리 접근이 비슷합니다: OpenAI Responses API 502 Bad Gateway 원인과 해결
최종 점검: 재발 방지 체크리스트
배포 후 아래를 확인하면 같은 유형의 CrashLoopBackOff를 상당 부분 예방할 수 있습니다.
/live는 DB/Redis 등 외부 의존성 없이도 200을 주는가/ready는 트래픽 수용 가능 상태에서만 200을 주는가startupProbe로 콜드 스타트를 보호하는가kubectl describe pod이벤트에서 probe 실패가 사라졌는가- 재시작 횟수가 0으로 안정화됐는가
- 리소스 그래프에서 CPU throttling, OOM 흔적이 없는가
livenessProbe 503으로 인한 CrashLoopBackOff는 대부분 "프로브의 역할 분리"와 "startupProbe 도입"으로 해결됩니다. 재시작은 문제를 고치는 도구가 아니라, 잘못 쓰면 장애를 키우는 도구라는 점을 기준으로 설정을 다시 설계해 보세요.