Probe vs Health Check 불일치 디버깅
📅 작성일: 2026-04-07 | ⏱️ 읽는 시간: 약 20분
📌 기준 환경: EKS 1.32+, AWS Load Balancer Controller v2.9+, Ingress-NGINX v1.11+
1. 개요
Kubernetes Probe와 Load Balancer/Ingress Controller의 Health Check는 독립적으로 실행되며, 서로 다른 메커니즘과 타이밍을 가집니다. 이로 인한 불일치는 다음과 같은 장애를 유발합니다:
- 503 Service Unavailable: Probe는 성공하지만 ALB Health Check 실패
- 502 Bad Gateway: Graceful Shutdown 시퀀스 불일치로 종료 중인 Pod로 트래픽 전송
- 일시적 장애: Rolling Update 중 새 Pod가 준비되기 전에 트래픽 수신
- 504 Gateway Timeout: Ingress 타임아웃과 백엔드 응답 시간 불일치
본 문서는 K8s Probe와 ALB/NLB/Ingress Health Check의 메커니즘 차이를 명확히 하고, 빈발하는 불일치 패턴별 진단 방법과 권장 설정을 제공합니다.
- Probe 기초: Pod 헬스체크 & 라이프사이클 — Probe 설정 상세
- 네트워킹 디버깅: 네트워킹 문제 해결 — Service/DNS 이슈 (추후 작성 예정)
- 고가용성: EKS 고가용성 아키텍처 가이드 — PDB, Graceful Shutdown
2. 메커니즘 비교: Probe vs Health Check
2.1 Kubernetes Probe (kubelet 실행)
Kubernetes Probe는 kubelet이 각 노드에서 독립적으로 실행하는 헬스 체크입니다.
| Probe 유형 | 실행 주체 | 체크 대상 | 실패 시 동작 |
|---|---|---|---|
| readinessProbe | kubelet | 컨테이너 | Service Endpoints에서 제거 (Pod는 살아있음) |
| livenessProbe | kubelet | 컨테이너 | 컨테이너 재시작 (SIGTERM → SIGKILL) |
| startupProbe | kubelet | 컨테이너 | 초기화 완료 전 다른 Probe 비활성화, 실패 시 재시작 |
핵심 특징:
- Pod 내부에서 실행: kubelet이 컨테이너에 직접 접근
- Service Endpoint 제어: readinessProbe 실패 →
kubectl get endpoints목록에서 제거 - 빠른 체크: 기본 1초 timeout, 10초 간격
2.2 AWS Load Balancer Health Check
AWS Load Balancer Controller(LBC)가 관리하는 ALB/NLB Health Check는 AWS 인프라 레벨에서 독립적으로 실행됩니다.
| Health Check 유형 | 실행 주체 | 체크 대상 | 실패 시 동작 |
|---|---|---|---|
| ALB Target Group HC | ALB | HTTP(S) endpoint | Target Group에서 deregister (Pod 상태와 무관) |
| NLB Target Group HC | NLB | TCP or HTTP | Target Group에서 deregister |
핵심 특징:
- 외부에서 실행: ALB/NLB가 Pod IP로 HTTP/TCP 요청
- 독립적 설정: K8s Probe와 별도로 interval, timeout, threshold 설정
- 느린 체크: 기본 5초 timeout, 15-30초 간격
2.3 Ingress-NGINX Health Check
Ingress-NGINX Controller는 nginx upstream 레벨에서 헬스 체크를 수행합니다.
| Health Check 유형 | 실행 주체 | 체크 대상 | 실패 시 동작 |
|---|---|---|---|
| upstream health | nginx process | HTTP backend | proxy_next_upstream 동작 (다른 upstream으로 재시도) |
핵심 특징:
- nginx process 내부: L7 프록시 레벨 체크
- timeout 설정:
proxy-read-timeout,proxy-send-timeout(기본 60초) - 암묵적 체크: 별도 health check endpoint 없이 실제 요청 결과로 판단
3. 타이밍 비교표
다음 표는 각 Health Check의 기본 타이밍과 체크 주체, 실패 시 동작을 비교합니다.
| 설정 | K8s Probe | ALB Health Check | NLB Health Check | Ingress-NGINX |
|---|---|---|---|---|
| 기본 interval | 10s | 15s | 30s | - (실제 트래픽) |
| 기본 timeout | 1s | 5s | 6s | 60s (proxy_read_timeout) |
| 실패 threshold | 3 | 2 (unhealthy) | 3 | - |
| 체크 주체 | kubelet | ALB | NLB | nginx process |
| 실패 시 동작 | Endpoints 제거 | TG deregister | TG deregister | upstream 제거 후 재시도 |
| 체크 경로 | /healthz 등 | / 또는 커스텀 | TCP 또는 HTTP | 실제 요청 경로 |
| 설정 위치 | Pod spec | Service annotation | Service annotation | Ingress annotation |
타이밍 불일치의 핵심:
- ALB는 K8s보다 느리게 체크: 15초 간격 vs 10초 간격
- ALB timeout이 더 김: 5초 vs 1초 → Probe는 통과하지만 ALB는 실패 가능
- 체크 경로 불일치: readinessProbe
/healthz≠ ALB Health Check/
4. 빈발 불일치 패턴
패턴 1: Probe 성공 + ALB Health Check 실패 → 503
증상:
kubectl get pods→ Pod는Running,Ready 1/1kubectl get endpoints→ Endpoints에 Pod IP 존재- 실제 요청 →
503 Service Unavailable
근본 원인:
-
Health Check 경로 불일치 (가장 흔함)
- readinessProbe:
GET /healthz→ 200 OK - ALB Target Group HC:
GET /→ 404 Not Found - 결과: K8s는 Ready 판정, ALB는 Unhealthy 판정
- readinessProbe:
-
타임아웃 불일치
- readinessProbe timeout 1초 → 앱이 800ms에 응답
- ALB HC timeout 5초 내에 앱이 응답 못함 (예: DB 쿼리 지연)
-
Security Group 설정 오류
- ALB → Pod CIDR 트래픽 차단
- kubelet은 노드 내부에서 체크 (통과), ALB는 외부에서 체크 (실패)
진단 플로우:
해결책:
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
# ALB Health Check 경로를 readinessProbe와 통일
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "15"
alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "5"
alb.ingress.kubernetes.io/healthy-threshold-count: "2"
alb.ingress.kubernetes.io/unhealthy-threshold-count: "2"
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz # ALB HC 경로와 일치
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
패턴 2: Graceful Shutdown 시 502 Bad Gateway
증상:
- Pod 종료 중에
502 Bad Gateway발생 - 일부 요청만 실패 (간헐적)
근본 원인: Pod 종료 시퀀스와 ALB deregistration 타이밍 불일치로 종료 중인 Pod로 트래픽 전송
Pod 종료 시퀀스:
kubectl delete pod또는 Rolling Update 시작- Pod status →
Terminating - 동시에 두 가지 동작:
- kubelet:
preStophook 실행 →SIGTERM전송 - kube-proxy: Endpoints에서 Pod 제거 (iptables 규칙 업데이트)
- kubelet:
terminationGracePeriodSeconds(기본 30초) 대기SIGKILL로 강제 종료
ALB deregistration 시퀀스:
- ALB가 Target Group에서 Pod 제거 요청 수신
deregistration_delay(기본 300초) 동안 대기- 대기 중에 도 기존 연결은 유지 (connection draining)
- 300초 후 Target 완전 제거
문제 상황:
시간축:
T+0s Pod Terminating, preStop 실행 (없으면 즉시 SIGTERM)
T+0s ALB deregistration 시작 (하지만 300초 대기)
T+0s SIGTERM 전송 → 앱이 즉시 종료 시작
T+1s 앱 프로세스 종료
T+1s~ ALB가 아직 connection draining 중 → 502 발생
T+30s terminationGracePeriodSeconds 도달 → SIGKILL
T+300s ALB deregistration 완료
권장 설정 공식:
terminationGracePeriodSeconds > deregistration_delay + preStop_duration + app_shutdown_time
예시: deregistration_delay=15s, preStop=10s, app_shutdown=5s
→ terminationGracePeriodSeconds=40s 이상
진단 플로우:
해결책:
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
# ALB deregistration delay 단축 (기본 300초 → 15초)
alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=15
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
terminationGracePeriodSeconds: 40 # preStop + deregistration + shutdown
containers:
- name: app
image: my-app:1.0
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# 1. ALB가 deregistration을 감지할 시간 확보
sleep 15
# 2. 애플리케이션에 종료 신호 (선택)
# curl -X POST localhost:8080/shutdown
# 애플리케이션은 SIGTERM을 받아 graceful shutdown 수행
언어별 SIGTERM 핸들러 예시 (Node.js):
// server.js
const express = require('express');
const app = express();
const server = app.listen(8080);
// 진행 중인 요청 추적
let isShuttingDown = false;
app.use((req, res, next) => {
if (isShuttingDown) {
res.setHeader('Connection', 'close');
return res.status(503).send('Server is shutting down');
}
next();
});
// SIGTERM 핸들러
process.on('SIGTERM', () => {
console.log('SIGTERM received, starting graceful shutdown');
isShuttingDown = true;
server.close(() => {
console.log('All connections closed, exiting');
process.exit(0);
});
// 강제 종료 타임아웃 (25초 후)
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 25000);
});
패턴 3: Rolling Update 시 일시적 503
증상:
kubectl rollout status중 간헐적 503- 새 Pod는
Running,Ready, 하지만 일부 요청 실패
근본 원인: ALB Health Check가 통과하기 전에 K8s가 Pod를 "Ready" 상태로 판정하여 트래픽 전송
타이밍 불일치:
T+0s 새 Pod 시작
T+10s readinessProbe 성공 (첫 체크 10초 후)
T+10s K8s Endpoints에 Pod 추가 → ALB에 Target 등록 요청
T+10s K8s가 구 Pod로 트래픽 전송 중지
T+15s ALB 첫 Health Check 실행
T+30s ALB Health Check 2회 성공 (healthy threshold=2)
T+30s ALB가 새 Pod로 트래픽 전송 시작
문제: T+10s ~ T+30s 구간에서 새 Pod 준비 전 트래픽 → 503
진단 플로우:
해결책:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 한 번에 1개씩만 종료
maxSurge: 1 # 한 번에 1개씩만 추가
# 핵심: ALB Health Check 통과 대기
minReadySeconds: 30 # ALB HC interval(15s) × threshold(2) = 30s
template:
spec:
containers:
- name: app
image: my-app:2.0
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 2 # 엄격하게 체크
successThreshold: 1
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 2 # 최소 50% 유지
selector:
matchLabels:
app: my-app
패턴 4: NLB + externalTrafficPolicy: Local
증상:
- NLB 사용 시 일부 요청 타임아웃
externalTrafficPolicy: Local설정 시 Health Check 실패
근본 원인:
NLB는 모든 노드에 트래픽 전송하지만, externalTrafficPolicy: Local은 Pod가 있는 노드만 응답
동작 방식:
| externalTrafficPolicy | Client IP 보존 | Health Check | 트래픽 분배 |
|---|---|---|---|
| Cluster (기본) | ❌ (SNAT) | 모든 노드 healthy | 균등 분배 → 노드 간 hop 발생 |
| Local | ✅ | Pod 있는 노드만 healthy | 불균등 분배 (Pod 수에 비례) |
문제 상황:
노드 1: Pod A, Pod B → NLB HC 성공 → 트래픽 수신
노드 2: Pod 없음 → NLB HC 실패 → TG에서 제거
노드 3: Pod C → NLB HC 성공 → 트래픽 수신
문제: 노드 1이 2배 트래픽 수신 (불균등)
진단 및 해결:
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
# NLB Health Check 설정
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "http"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/healthz"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-interval: "10"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-timeout: "6"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold: "2"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-unhealthy-threshold: "2"
spec:
type: LoadBalancer
# Client IP 보존 vs 균등 분배 선택
externalTrafficPolicy: Local # Client IP 필요 시
# externalTrafficPolicy: Cluster # 균등 분배 필요 시
ports:
- port: 80
targetPort: 8080
권장 사항:
- Client IP 필요:
Local+ 충분한 Pod 수 (노드당 최소 1개) - 균등 분배 우선:
Cluster+ X-Forwarded-For 헤더로 Client IP 추출
패턴 5: Ingress-NGINX upstream timeout
증상:
504 Gateway Timeout발생- 파일 업로드, 배치 API 실패
413 Request Entity Too Large(파일 크기 초과)
근본 원인:
Ingress-NGINX의 proxy-read-timeout (기본 60초)가 백엔드 처리 시간보다 짧음
진단 및 해결:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
# Timeout 설정 (초 단위)
nginx.ingress.kubernetes.io/proxy-read-timeout: "300" # 백엔드 응답 대기
nginx.ingress.kubernetes.io/proxy-send-timeout: "300" # 백엔드로 전송 대기
nginx.ingress.kubernetes.io/proxy-connect-timeout: "10" # 백엔드 연결 대기
# 파일 업로드 크기 제한 (기본 1m)
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
# 버퍼 설정 (대용량 응답)
nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
배치 API 전용 Ingress 분리:
# 일반 API (짧은 timeout)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
---
# 배치 API (긴 timeout)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: batch-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # 30분
nginx.ingress.kubernetes.io/proxy-body-size: "1g"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /batch
pathType: Prefix
backend:
service:
name: batch-service
port:
number: 80