스프링 부트 앱을 k8s에 올렸는데, 앱 자체는 멀쩡한데 파드가 뜬 지 20~30초쯤 되면 자꾸 재시작됩니다. RESTARTS 카운트가 계속 올라가요. 로그를 보면 애플리케이션은 정상적으로 부팅 중인데 그 도중에 SIGTERM을 받고 죽습니다.
NAME READY STATUS RESTARTS AGE
api-7d9f8-xk2l 0/1 Running 4 3mlivenessProbe는 이렇게 걸어놨습니다.
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3앱 부팅이 좀 무거워서 완전히 뜨는 데 40초 정도 걸리는데, 이게 관련이 있을까요? CrashLoopBackOff는 아니고 그냥 Running 상태에서 재시작만 반복됩니다.
댓글 4
initialDelaySeconds를 크게 늘리는 걸로 때우고 싶겠지만 그건 안티패턴입니다. 그 값을 60으로 잡으면 정상 운영 중에 앱이 진짜 hang 됐을 때도 60초를 기다려야 감지되거든요. 부팅 느린 것과 런타임 헬스체크는 목적이 다른데 하나의 노브로 둘 다 맞추려니 안 되는 거예요.
정답은 startupProbe입니다. startupProbe가 성공할 때까지 kubelet은 liveness랑 readiness를 아예 시작조차 안 해요. 그래서 느린 부팅은 startupProbe한테 넉넉하게 맡기고, liveness는 짧고 공격적으로 유지할 수 있습니다.
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
failureThreshold: 30
periodSeconds: 5
이러면 부팅에 최대 150초(30 × 5)까지 봐주고, 그동안 liveness는 잠자코 있습니다. 부팅 끝나고 startupProbe 통과한 뒤부터 liveness가 원래대로 5초 간격으로 감시 시작해요. 재시작 루프 바로 멈출 겁니다.
하나만 더 여쭤볼게요. liveness랑 readiness를 지금처럼 둘 다 /actuator/health 통짜로 써도 되나요, 아니면 분리하는 게 나은가요?
liveness는 '이 프로세스가 살아있나, 아니면 재시작해야 하나'를 묻습니다. readiness는 '지금 트래픽 받을 준비가 됐나'를 물어요. 예를 들어 DB 연결이 잠깐 끊기면 이건 재시작한다고 해결되는 문제가 아니거든요. 앱은 살아있으니 liveness는 통과해야 하고, 대신 readiness만 실패시켜서 트래픽을 잠깐 빼는 게 맞습니다.
그런데 /actuator/health 통짜로 쓰면 스프링 헬스는 DB 상태까지 다 물려 있어서, DB가 깜빡하는 순간 liveness까지 fail 나고 멀쩡한 파드가 불필요하게 재시작됩니다. DB 잠깐 끊긴 걸로 전체 파드가 재시작 도미노 타는 거예요.
스프링 부트는 health.liveness 와 health.readiness 그룹을 따로 지원합니다. management.endpoint.health.probes.enabled=true 켜면 /actuator/health/liveness 랑 /actuator/health/readiness 가 각각 생겨요. liveness 프로브는 앞엣걸, readiness 프로브는 뒤엣걸 물게 하세요. liveness 그룹엔 DB 같은 외부 의존성 체크를 넣지 말고 정말 프로세스 생존만 보게 하는 게 핵심입니다.