- Published on
gRPC 마이크로서비스 503·데드라인 초과 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 간 통신을 gRPC로 바꾸면 “HTTP/2라 빠르다”, “IDL이 있으니 안전하다” 같은 장점이 먼저 보입니다. 하지만 운영에서는 에러가 훨씬 ‘압축된 형태’로 나타납니다. 대표가 UNAVAILABLE(503)와 DEADLINE_EXCEEDED입니다. 둘 다 “요청이 실패했다”는 결과는 같지만, 원인 레이어가 완전히 다릅니다.
이 글은 **마이크로서비스 환경(특히 Kubernetes/EKS, Envoy/ALB/NLB, 서비스 메시)**에서 gRPC 503·데드라인 초과를 재현 → 분리 → 계측 → 수정 순서로 디버깅하는 방법을 정리합니다.
1) 에러 코드 해석: 503은 HTTP가 아니라 gRPC 상태다
먼저 용어를 정리해야 합니다.
- gRPC의
UNAVAILABLE는 종종 HTTP status 503으로 매핑되어 보이지만, 실제 의미는 “현재 서버에 도달할 수 없거나(연결 실패), 연결이 중간에 끊겼거나, 로드밸런서/프록시가 업스트림을 못 찾는다”에 가깝습니다. DEADLINE_EXCEEDED는 “서버가 늦게 응답했다” 뿐 아니라, 클라이언트/프록시가 타임아웃을 먼저 선언했을 수도 있습니다.
실무에서 가장 중요한 질문은 두 가지입니다.
- 요청이 서버까지 도달했나? (서버 로그/메트릭/트레이스에 흔적이 있는가)
- 도달했다면 어디서 시간이 소비됐나? (큐잉/DB/외부 API/GC/스레드 풀)
이 두 질문만 끝까지 밀어붙이면 대부분 해결됩니다.
2) 증상 분류: 503 vs DEADLINE_EXCEEDED 빠른 분기표
A. UNAVAILABLE(503)가 많은 경우
- DNS/Service discovery 문제 (CoreDNS, headless service, endpoint stale)
- L4/L7 로드밸런서가 gRPC를 제대로 못 받음 (HTTP/2, h2c, idle timeout)
- 프록시(Envoy/Nginx) 업스트림 연결 실패
- 서버 프로세스 크래시/재시작, readiness 실패로 엔드포인트가 빠짐
- 커넥션/FD 고갈(서버가 accept 못 함)
B. DEADLINE_EXCEEDED가 많은 경우
- 서버 처리 지연(스레드 풀 고갈, 락 경합, DB 커넥션 대기)
- 클라이언트 deadline이 너무 짧음
- 프록시/Ingress에서 더 짧은 timeout을 강제
- 재시도 정책이 오히려 꼬여서 tail latency 폭발
이 분류가 중요한 이유는, 503은 네트워크/인프라/연결 수립 쪽을 먼저 보고, deadline 초과는 서버 내부 병목을 먼저 보는 게 시간 절약이기 때문입니다.
3) “서버에 도달했는지” 확인하는 최소 계측
3.1 서버 접근 로그/인터셉터로 요청 흔적 남기기
gRPC 서버는 HTTP access log가 기본으로 없기 때문에, 인터셉터로 최소한의 메타데이터를 남겨야 합니다.
Go 서버 예시(Unary Interceptor)
grpc.NewServer(
grpc.UnaryInterceptor(func(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
md, _ := metadata.FromIncomingContext(ctx)
resp, err := handler(ctx, req)
st, _ := status.FromError(err)
log.Printf(
"method=%s code=%s dur=%s peer=%v xrid=%v",
info.FullMethod,
st.Code(),
time.Since(start),
peerFromCtx(ctx),
first(md.Get("x-request-id")),
)
return resp, err
}),
)
- 로그에 찍히지 않으면: 서버까지 요청이 안 온 것(프록시/네트워크/디스커버리)일 확률이 큽니다.
- 로그는 찍히는데 deadline: 서버 내부 병목 또는 다운스트림 호출 타임아웃을 의심합니다.
3.2 클라이언트에서 deadline/재시도/호출 시간 로깅
클라이언트는 “내가 몇 초를 줬는지”를 반드시 남겨야 합니다.
ctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
defer cancel()
start := time.Now()
resp, err := c.GetUser(ctx, &pb.GetUserRequest{Id: "42"})
log.Printf("GetUser dur=%s err=%v", time.Since(start), err)
운영에서 자주 생기는 실수: 서버는 2초 안에 응답할 수 있는데, 클라이언트가 300ms deadline을 걸어놓고 “서버 느리다”로 결론내는 경우입니다.
4) gRPC 503(UNAVAILABLE) 디버깅: 연결 레이어부터
4.1 로드밸런서/Ingress의 HTTP/2, idle timeout 확인
gRPC는 HTTP/2 장기 연결을 전제로 합니다. 따라서 Ingress/ALB/NLB/프록시의 idle timeout이 짧으면 유휴 커넥션이 끊기고 다음 요청에서 UNAVAILABLE가 튀는 패턴이 나옵니다(특히 트래픽이 듬성듬성한 배치/관리 API).
- ALB Ingress를 쓴다면 idle timeout(기본 60초)과 keepalive 정책을 반드시 확인하세요. gRPC 자체 문제처럼 보이지만 사실은 L7 타임아웃인 경우가 많습니다.
문맥상 함께 보면 좋은 글: EKS ALB Ingress 504(60초) idle_timeout 해결
4.2 kube-proxy/IPVS/conntrack 이슈로 간헐적 연결 실패
노드 네트워킹 계층에서 연결이 끊기면 애플리케이션은 보통 UNAVAILABLE로만 봅니다.
체크 포인트:
- 특정 노드에서만 발생하는가? (Pod가 어느 노드에 뜨면 터지는지)
kubectl get endpoints가 정상인데도 연결이 실패하는가?- conntrack 테이블이 포화되는가? (
nf_conntrack_max, drop 증가)
EKS에서 kube-proxy 모드(IPVS/iptables) 변경 이후 통신 장애가 난 사례는 재현도 어렵고 증상이 비슷합니다. 관련 경험 글: EKS kube-proxy를 IPVS로 바꾼 뒤 통신 장애 복구
4.3 서버의 파일 디스크립터(FD)/소켓 고갈
서버가 커넥션을 더 못 받으면, 클라이언트는 연결 실패 → UNAVAILABLE로 관측합니다.
리눅스에서 다음을 확인하세요.
- 프로세스 FD 사용량
ulimit -n/ systemdLimitNOFILE- TIME_WAIT 폭증(특히 L4 LB 앞에서 짧은 커넥션을 반복하는 구조)
FD 고갈은 gRPC에서도 흔합니다. 특히 스트리밍을 많이 쓰거나, 프록시/사이드카가 붙으면 소켓 수가 급격히 늘 수 있습니다. 참고: 리눅스 Too many open files 해결 - ulimit·systemd·Nginx
5) DEADLINE_EXCEEDED 디버깅: “서버가 늦은가, 기다림이 짧은가”
5.1 서버 처리 시간 vs 큐잉 시간 분리
서버가 느린 게 아니라 서버가 일할 기회를 못 얻는 경우가 많습니다.
- 스레드/워커 풀 고갈
- 동시성 제한(세마포어)로 대기
- CPU throttling (K8s limits)
- GC stop-the-world
가장 빠른 방법은 **서버에서 ‘핸들러 진입 시각’과 ‘응답 시각’**을 찍고, 추가로 큐잉이 있다면 큐 대기 시간을 분리해서 기록하는 것입니다.
5.2 DB 커넥션 대기(HikariCP/풀 고갈)로 deadline 초과
Java/Spring 기반 gRPC 서버에서 deadline 초과의 상위 원인은 DB 커넥션 풀 고갈입니다.
- 애플리케이션은 “DB 쿼리 시간이 길다”로 보이지만
- 실제론 “커넥션을 얻기까지 기다리다” 타임아웃이 나는 경우
Spring Boot라면 HikariCP 메트릭(Active/Idle/Pending)과 함께, 커넥션 획득 시간(최소 로그/메트릭)을 꼭 보세요.
관련 글: Spring Boot 3에서 HikariCP 커넥션 고갈 원인 9가지
5.3 클라이언트 deadline/프록시 timeout 정렬
운영에서 권장하는 규칙:
- 클라이언트 deadline < 프록시 timeout < 서버 내부 타임아웃 같은 식으로 뒤죽박죽이면 디버깅이 지옥이 됩니다.
- 보통은 서버 내부 다운스트림 타임아웃 < 클라이언트 deadline < 프록시 idle timeout처럼, “안쪽이 먼저 포기하고 바깥은 그 결과를 받는” 구조가 관측 가능성을 높입니다.
예시(권장 패턴):
- 서버가 DB에 400ms 타임아웃
- 서버 전체 처리 deadline 800ms
- 클라이언트 deadline 1s
- Ingress idle timeout 60s(혹은 더)
이렇게 하면 DB가 느릴 때 서버가 먼저 실패를 결정하고, 클라이언트는 DEADLINE_EXCEEDED가 아니라 더 구체적인 에러로 받을 여지가 생깁니다.
6) 재시도(retry)와 데드라인의 함정: tail latency를 폭발시키지 말 것
gRPC는 재시도 정책을 잘못 넣으면 장애를 증폭시킵니다.
- 서버가 느려져서 deadline이 가까워짐
- 클라이언트가 재시도
- 서버 부하가 더 증가
- 더 많은 요청이 deadline 초과
권장 체크:
- 재시도는 멱등(idempotent) 요청에만
- 재시도 횟수 제한 + 지수 백오프 + jitter
- “서버가 처리 중인데 클라이언트가 포기”하는 중복 실행을 줄이기 위해, 가능하면 요청 ID 기반 중복 방지 고려
Envoy를 쓴다면 x-envoy-attempt-count 같은 헤더/메타데이터로 재시도 횟수를 추적해 “한 요청이 몇 번이나 날아갔는지”를 관측하세요.
7) 현장에서 바로 쓰는 디버깅 플레이북(체크리스트)
7.1 10분 내 1차 결론 내기
- 서버 로그(인터셉터)에 요청이 찍히나?
- No → 네트워크/LB/DNS/엔드포인트/readiness
- Yes → 서버 내부 병목/다운스트림
- 에러가 503(UNAVAILABLE)인가, DEADLINE_EXCEEDED인가?
- 특정 노드/특정 AZ/특정 버전에서만 발생하나? (배포 직후면 더더욱)
- 클라이언트 deadline 값이 무엇인가? (코드/설정)
7.2 Kubernetes/EKS에서 추가로 보는 것
kubectl describe pod에서 readiness probe 실패/재시작kubectl get endpoints가 비어있거나 수가 흔들림- 노드 리소스 압박(CPU throttling, 메모리)
- conntrack/네트워크 드롭
7.3 서버 리소스/커넥션 관측
- FD 사용량(Too many open files 전조)
- gRPC keepalive 설정(서버/클라이언트)
- 스레드 풀/이벤트 루프 백로그
- DB 풀 pending
8) 예방: 운영 친화적인 gRPC 설정 템플릿
아래는 “장기 연결이 중간 장비에서 끊겨도 빨리 감지하고, 무한 대기 없이 실패를 관측 가능하게 만드는” 쪽에 초점을 둔 예시입니다.
Go client keepalive + timeout 예시
ka := keepalive.ClientParameters{
Time: 30 * time.Second, // ping 주기
Timeout: 10 * time.Second, // ping ack 기다림
PermitWithoutStream: true,
}
conn, err := grpc.NewClient(
target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithKeepaliveParams(ka),
)
if err != nil { panic(err) }
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err = pb.NewUserClient(conn).GetUser(ctx, &pb.GetUserRequest{Id: "42"})
주의: keepalive는 인프라(LB, 방화벽) 정책과 충돌할 수 있으니, 무작정 짧게 잡지 말고 “유휴 커넥션이 끊기는 주기”를 측정한 뒤 조정하세요.
9) 마무리: 503과 deadline은 ‘현상’이고, 답은 레이어 분리에 있다
gRPC에서 503과 deadline 초과는 에러 메시지가 짧아 더 막막하게 느껴지지만, 역으로 말하면 레이어를 분리해서 증거를 모으면 해결이 빠릅니다.
- 503(UNAVAILABLE): “연결이 왜 안 되는가” → LB/프록시/네트워크/FD/엔드포인트
- DEADLINE_EXCEEDED: “왜 늦는가” → 서버 큐잉/스레드풀/DB 풀/다운스트림 타임아웃 정렬
다음 액션으로는 (1) 서버 인터셉터로 최소 로그를 남기고, (2) 클라이언트 deadline/재시도 정책을 명시화하고, (3) LB/Ingress idle timeout과 keepalive를 정렬하는 것부터 시작하면 대부분의 케이스가 정리됩니다.