- Published on
EKS에서 gRPC DEADLINE_EXCEEDED 폭증 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데도 gRPC DEADLINE_EXCEEDED가 갑자기 폭증하는 순간이 있습니다. 특히 EKS에서는 오토스케일링/노드 교체, 코어DNS/네트워크 경로, L7/L4 로드밸런서(Envoy/NLB/ALB), Pod 리소스 압박, 커넥션 드레이닝 미흡이 겹치면 “지연이 살짝 늘어난 것”이 “타임아웃 폭발”로 증폭됩니다.
이 글은 EKS에서 DEADLINE_EXCEEDED가 폭증했을 때,
- 빠르게 범위를 좁히는 관측 포인트,
- 흔한 원인별 증상 패턴,
- 즉시 완화(Stop the bleeding),
- 근본 해결(재발 방지) 를 실전 체크리스트 형태로 정리합니다.
1) DEADLINE_EXCEEDED의 의미를 먼저 분해하기
gRPC의 DEADLINE_EXCEEDED는 “서버가 에러를 반환했다”라기보다 클라이언트가 설정한 deadline 내에 응답을 못 받았다는 뜻입니다. 즉 원인은 크게 네 갈래입니다.
- 서버 처리시간 증가: CPU throttling, GC pause, 락 경합, DB/외부 호출 지연
- 네트워크/프록시 경로 지연: DNS 지연, 패킷 드랍/재전송, 프록시 큐잉
- 연결/스트림 관리 문제: keepalive/idle timeout, 커넥션 재사용 실패, 드레이닝 미흡
- 클라이언트 deadline 설계 문제: 너무 짧은 deadline, 재시도 폭주로 인한 self-amplification
EKS에서는 2~3번이 특히 자주 원인이 됩니다. “서버 CPU는 낮은데 타임아웃이 난다”가 대표적 신호입니다.
2) 폭증 시 가장 먼저 확인할 6가지 관측 포인트
2.1 gRPC 상태코드/지연 히스토그램을 분리
DEADLINE_EXCEEDED만 보면 원인이 안 보입니다. 최소한 다음을 분리해 봅니다.
p50/p90/p99latency (클라이언트 관측 vs 서버 관측)UNAVAILABLE동반 여부(연결 실패/리셋)- 재시도 횟수 증가 여부
- 특정 메서드만 폭증하는지(핫 메서드)
Prometheus를 쓴다면(예: grpc-go, grpc-java) 대개 아래와 같은 메트릭이 있습니다.
# 클라이언트 관측 p99
histogram_quantile(0.99,
sum by (le, grpc_method) (rate(grpc_client_handling_seconds_bucket[5m]))
)
# DEADLINE_EXCEEDED 비율
sum by (grpc_method) (rate(grpc_client_handled_total{grpc_code="DEADLINE_EXCEEDED"}[5m]))
/
sum by (grpc_method) (rate(grpc_client_handled_total[5m]))
클라이언트 p99만 튀고 서버 p99는 안정적이면 네트워크/프록시/드레이닝 쪽을 먼저 의심합니다.
2.2 Pod/Node 리소스: CPU throttling과 네트워크 drop
EKS에서 “CPU 사용률은 낮은데 느리다”는 상황은 CPU throttling일 수 있습니다. requests/limits가 타이트하면 CFS throttling으로 tail latency가 튑니다.
container_cpu_cfs_throttled_periods_totalcontainer_cpu_usage_seconds_total
# throttling 비율(대략)
rate(container_cpu_cfs_throttled_periods_total[5m])
/
rate(container_cpu_cfs_periods_total[5m])
또한 노드 네트워크 드랍/재전송은 gRPC에 치명적입니다. 노드 레벨에서 ethtool -S, ss -s, netstat -s도 확인합니다.
2.3 CoreDNS 지연/오류
서비스 디스커버리가 흔들리면 연결 재시도와 함께 deadline이 쉽게 소진됩니다.
- CoreDNS
SERVFAIL,timeout,no such host증가 ndots/search domain로 인한 쓸데없는 질의 폭증
2.4 로드밸런서/프록시 타임아웃과 드레이닝
Envoy/Istio, Nginx, ALB/NLB를 경유한다면 다음이 자주 원인입니다.
- idle timeout이 짧아 커넥션이 중간에 끊김
- Pod 종료 시 드레이닝 없이 커넥션이 강제 종료
- L7에서 헤더/프레임 처리 지연
2.5 오토스케일링 이벤트(노드 교체/스케일 인)
DEADLINE_EXCEEDED 폭증이 노드 스케일 인/교체 타이밍과 맞물리면, “정상 트래픽 + 재시도”가 겹쳐 장애처럼 보일 수 있습니다.
Karpenter를 사용 중이라면 노드가 제때 늘지 않거나(과부하 지속), 반대로 너무 공격적으로 교체되며 커넥션이 흔들릴 수 있습니다. 관련 점검은 아래 글도 함께 참고하세요.
2.6 로그 비용 폭증(부수 증상)
타임아웃/재시도 폭주가 시작되면 애플리케이션/프록시 로그가 급증해 CloudWatch Logs 비용이 같이 튀는 경우가 많습니다. 장애 대응 중 비용도 함께 막아야 합니다.
3) 흔한 원인별 “증상 패턴”과 해결책
3.1 Pod 종료/노드 교체 시 커넥션 드레이닝 미흡
증상
- 배포/스케일 인 직후
DEADLINE_EXCEEDED와UNAVAILABLE가 동시에 증가 - 특정 노드/Pod로 트래픽이 몰린 뒤 갑자기 타임아웃
- 서버 로그에는 처리 시작 로그가 없거나, 중간에 끊긴 흔적
원인
- Kubernetes는 Pod에 SIGTERM을 보내고
terminationGracePeriodSeconds동안 종료를 기다립니다. - 하지만 서비스 엔드포인트에서 제거되기 전에 또는 프록시/클라이언트가 커넥션을 재사용하는 동안 Pod가 죽으면 in-flight RPC가 깨집니다.
해결
- preStop + 충분한 grace period로 커넥션 드레이닝 시간을 확보
- readiness를 먼저 내려서 새 요청 유입을 차단
- 서버는
GracefulStop()(grpc-go) 등으로 in-flight를 정리
# deployment 예시
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
grpc-go 서버라면:
// SIGTERM 수신 시 graceful stop
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-ch
grpcServer.GracefulStop() // in-flight RPC 종료 대기
}()
Pod가 Terminating에 오래 걸리거나 finalizer로 막히면 드레이닝이 꼬여 장애가 길어질 수 있습니다. 아래 글의 디버깅 체크도 유용합니다.
3.2 클라이언트 deadline이 지나치게 짧거나, 재시도가 폭주
증상
- 서버 p99는 200ms인데 클라이언트는 1s deadline으로
DEADLINE_EXCEEDED가 발생 - 에러가 늘자마자 QPS가 더 증가(재시도 증폭)
해결
- deadline은 “정상 p99 + 버퍼 + 네트워크 변동”을 기준으로 잡습니다.
- 재시도는 지터 포함 exponential backoff, 최대 재시도 횟수 제한, 서킷 브레이커가 필요합니다.
grpc-go 클라이언트 예시(간단한 deadline + per-RPC timeout):
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := client.SomeRPC(ctx, req)
재시도를 직접 구현한다면(개념 예시):
for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
resp, err := client.SomeRPC(ctx, req)
cancel()
if err == nil {
return resp, nil
}
time.Sleep(backoffWithJitter(i))
}
return nil, err
핵심은 “타임아웃이 났으니 즉시 재시도”가 아니라, 시스템을 더 느리게 만드는 재시도 폭주를 막는 것입니다.
3.3 CPU throttling / GC pause로 인한 tail latency 상승
증상
- 평균 CPU는 낮은데 p99가 튐
- 특정 시간대(트래픽 버스트)에서만 급격히 악화
해결
limits를 너무 낮게 잡지 않습니다(특히 Go/Java).- 가능하면 CPU limit을 제거하거나(클러스터 정책에 따라)
requests를 올려 스케줄링 품질을 확보합니다. - JVM은 GC 로그/heap sizing을 재점검합니다.
Kubernetes 리소스 예시:
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1024Mi"
3.4 CoreDNS/네임해결 이슈로 연결 지연
증상
- 애플리케이션 로그에
no such host,i/o timeout이 간헐적으로 등장 - 재시작/스케일 아웃 시 새 Pod에서만 더 심함(초기 DNS warm-up)
해결
- CoreDNS 리소스/replica 확장
ndots과도 설정으로 불필요한 질의가 늘지 않는지 확인- gRPC 채널을 매 요청마다 새로 만들지 말고(안티패턴) 커넥션 재사용
클라이언트 안티패턴 예:
// 매 요청마다 Dial -> DNS/handshake 비용 폭발
conn, _ := grpc.Dial(target, grpc.WithInsecure())
defer conn.Close()
client := pb.NewServiceClient(conn)
개선:
// 프로세스 시작 시 1회 Dial 후 재사용
var (
conn *grpc.ClientConn
client pb.ServiceClient
)
func initClient() {
c, err := grpc.Dial(target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil { panic(err) }
conn = c
client = pb.NewServiceClient(conn)
}
3.5 로드밸런서/프록시 idle timeout, keepalive 불일치
증상
- 일정 시간 유휴 후 첫 요청이 자주 타임아웃
RST_STREAM,connection reset by peer동반
해결
- 프록시/로드밸런서의 idle timeout을 gRPC 사용 패턴에 맞게 조정
- gRPC keepalive를 과도하게 공격적으로 두면 오히려 중간 장비에서 차단될 수 있으니,
- 클라이언트 keepalive 주기
- 서버 enforcement policy
- LB idle timeout 을 서로 일관되게 맞춥니다.
grpc-go keepalive 예시:
ka := keepalive.ClientParameters{
Time: 30 * time.Second, // ping 주기
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}
conn, err := grpc.Dial(target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithKeepaliveParams(ka),
)
4) “지금 당장” 폭증을 멈추는 응급 처치(Stop the bleeding)
- 클라이언트 deadline을 임시로 상향(예: 1s → 2~3s)하고 에러율을 즉시 낮춰 재시도 폭주를 완화
- 재시도 제한/서킷 브레이커를 즉시 적용(가능하면 feature flag)
- HPA/Karpenter로 용량을 빠르게 확보(CPU throttling/큐잉이 원인일 때 효과)
- 배포/노드 교체가 원인 같으면 스케일 인/디스러션을 일시 중단(PDB, Karpenter disruption 설정)
- 로그가 폭주하면 샘플링/레벨 조정으로 CloudWatch 비용과 I/O 병목을 같이 차단
5) 근본 해결을 위한 체크리스트(재발 방지)
5.1 SLO 기반 deadline 설계
- 메서드별 정상 p99를 기준으로 deadline을 다르게 설정
- “서버 처리 + 네트워크 + 큐잉” 버퍼를 포함
5.2 드레이닝 표준화
- 모든 gRPC 서버에 graceful shutdown 패턴 적용
preStop,terminationGracePeriodSeconds, readiness 연동을 템플릿화- PDB로 동시에 죽는 Pod 수 제한
5.3 리소스/스케일링 안정화
- CPU throttling을 메트릭으로 상시 감시
- HPA는 CPU만 보지 말고 RPS, in-flight, 큐 길이 같은 신호를 추가
- 노드 스케일링(Karpenter/Cluster Autoscaler) 이벤트와 에러율 상관관계를 대시보드로 고정
5.4 네트워크/프록시 타임아웃 정합성
- client keepalive / server enforcement / LB idle timeout을 문서화
- 서비스 메시를 쓰면 per-route timeout, outlier detection, connection pool 설정을 점검
6) 실전 디버깅 플로우(30분 안에 범인 찾기)
- 폭증 시점을 기준으로 배포/스케일링/노드 교체 이벤트 확인
DEADLINE_EXCEEDED가 특정 메서드/특정 클라이언트에서만 발생하는지 분리- 클라이언트 p99 vs 서버 p99 비교(네트워크/프록시 vs 서버 처리)
- throttling/GC/DB 지연 같은 “서버 내부 지연”을 배제 또는 확정
- 드레이닝/idle timeout/keepalive 불일치 확인
- 재시도 정책으로 인한 증폭 여부 확인(에러 직후 QPS 상승하면 거의 확정)
마무리
EKS에서 gRPC DEADLINE_EXCEEDED 폭증은 대개 “단일 버그”가 아니라 타임아웃 설계 + 드레이닝 + 스케일링 + 네트워크 경로가 동시에 흔들리며 발생합니다.
가장 효과적인 접근은 (1) 클라이언트/서버 관측을 분리해 지연의 위치를 찾고, (2) 드레이닝과 재시도 정책으로 증폭을 막고, (3) 리소스/타임아웃 정합성을 표준화해 재발을 방지하는 것입니다.
원하시면 사용 중인 조합(Envoy/Istio 여부, NLB/ALB, 언어(grpc-go/grpc-java), HPA/Karpenter 설정, 현재 deadline 값)을 기준으로 원인 후보를 우선순위로 정렬한 맞춤 점검표도 만들어 드리겠습니다.