Published on

Go gRPC context deadline exceeded 9가지 원인

Authors
Binance registration banner

서버와 클라이언트 모두 정상처럼 보이는데 gRPC 호출이 context deadline exceeded로 끝나면, 원인은 “느림” 하나가 아니라 데드라인이 어디에서 걸렸는지(클라이언트/서버/프록시/네트워크)부터 분해해야 합니다. 특히 Go gRPC는 context.Context 기반으로 타임아웃이 전파되기 때문에, 작은 설정 실수나 리소스 병목이 곧바로 데드라인 초과로 관측됩니다.

이 글은 Go gRPC에서 context deadline exceeded가 발생하는 대표 원인 9가지를 증상 → 확인 방법 → 해결 형태로 정리합니다. (환경이 Kubernetes/EKS라면 DNS와 네트워크 계층을 반드시 함께 보세요. 예: EKS에서 Pod DNS만 느릴 때 ndots·search 튜닝, EKS STS 엔드포인트 타임아웃 - VPC·NAT·DNS 해결)

먼저: 이 에러는 어디서 발생했나?

context deadline exceeded는 보통 클라이언트가 정한 deadline 내에 RPC가 완료되지 못했을 때 발생합니다. 하지만 “서버가 느림”만이 전부가 아닙니다. 다음을 먼저 분리하세요.

  • 클라이언트 측: Dial 단계에서 timeout? RPC 처리 단계에서 timeout?
  • 서버 측: 서버 로그에 요청이 도착했는가? 도착했는데 처리 중 끊겼는가?
  • 중간 프록시/LB: idle timeout/connection draining/HTTP2 설정 문제?

최소 재현/관측 코드(클라이언트)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

start := time.Now()
resp, err := client.SomeRPC(ctx, &pb.Request{})
elapsed := time.Since(start)

st, _ := status.FromError(err)
log.Printf("elapsed=%s err=%v code=%v message=%q", elapsed, err, st.Code(), st.Message())
_ = resp
  • code=DeadlineExceeded면 대개 클라이언트 컨텍스트 deadline 초과입니다.
  • code=Unavailable 또는 Internal 등으로 바뀌면 네트워크/프록시/서버 오류일 가능성이 큽니다.

원인 1) Dial(연결) 단계가 느리거나 막힘: DNS/TCP/TLS

증상

  • 첫 호출에서만 특히 느림(콜드 스타트처럼 보임)
  • 서버 로그에는 요청이 아예 안 찍힘
  • grpc.DialContext 자체가 오래 걸리거나 실패

확인 방법

  • Dial에 별도 timeout을 걸어 “연결 단계”와 “RPC 단계”를 분리합니다.
// 연결 단계 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

conn, err := grpc.DialContext(
    ctx,
    target,
    grpc.WithTransportCredentials(creds),
    grpc.WithBlock(), // Dial이 완료될 때까지 블록
)
  • WithBlock() 없이 Dial하면 연결이 백그라운드에서 진행되어, 첫 RPC에서 timeout처럼 보일 수 있습니다.

해결

  • DNS가 느리면 ndots/search 튜닝 및 CoreDNS 성능을 점검하세요: EKS에서 Pod DNS만 느릴 때 ndots·search 튜닝
  • TLS 핸드셰이크가 무거우면 커넥션 재사용(keepalive), 인증서 체인/OCSP 확인
  • DialContext + WithBlock로 연결 실패를 조기에 감지

원인 2) deadline을 너무 짧게 잡았거나 “중첩 타임아웃”이 있음

증상

  • 특정 환경(부하/피크)에서만 간헐적으로 발생
  • 서버 처리시간 p95/p99가 타임아웃보다 큼

확인 방법

  • 클라이언트 timeout과 서버 내부 timeout(예: DB, 외부 API) 관계를 확인합니다.
  • 서버에서 ctx.Deadline()을 로깅하여 실제 남은 시간을 봅니다.
func (s *Server) SomeRPC(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    if dl, ok := ctx.Deadline(); ok {
        log.Printf("deadline in %s", time.Until(dl))
    }
    // ...
}

해결

  • SLO 기반으로 timeout을 설계: p99 + 네트워크 여유 + 재시도 여유
  • 체인 호출이 있다면 “전체 요청 예산”을 먼저 정하고, 하위 호출에 분배

원인 3) 서버 핸들러가 ctx 취소를 무시하고 블로킹됨

Go에서 흔한 실수는 ctx를 받았는데도, 실제 I/O에 ctx를 전달하지 않아 취소 불가능한 블로킹이 생기는 것입니다.

증상

  • 클라이언트는 deadline exceeded
  • 서버는 계속 작업하다가 뒤늦게 완료하거나 goroutine 누수

확인 방법

  • DB/HTTP/Redis 호출이 Context를 받는 API인지 확인
  • goroutine dump에서 특정 함수가 오래 대기하는지 확인

해결

  • 모든 외부 호출에 ctx를 전달
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
  • 채널/락 대기에도 ctx를 반영(가능하면 select로 탈출)
select {
case v := <-ch:
    _ = v
case <-ctx.Done():
    return nil, status.Error(codes.DeadlineExceeded, ctx.Err().Error())
}

원인 4) 서버/클라이언트 리소스 병목: CPU, GC, 스레드, 커넥션 풀

증상

  • 특정 Pod/노드에서만 유독 timeout
  • p99 latency 급증, GC pause 증가
  • 동시 요청 수가 늘면 급격히 악화

확인 방법

  • 서버의 goroutine 수, GC, CPU 사용률, run queue 확인
  • 외부 의존성(특히 DB) 커넥션 풀 고갈 여부 확인

DB 풀 고갈은 gRPC 타임아웃의 대표 원인입니다. (Java 예시지만 패턴은 동일: Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단)

해결

  • 서버 동시성 제한(세마포어) + 큐잉/백프레셔
  • DB/캐시 커넥션 풀 크기와 타임아웃 조정
  • 핫패스에서 할당 줄이기, 큰 응답 스트리밍화

원인 5) 메시지/페이로드가 커서 전송 시간이 deadline을 초과

gRPC는 기본 max message size 제한이 있고, 큰 메시지는 직렬화/네트워크 전송/수신 버퍼까지 비용이 큽니다.

증상

  • 큰 요청/응답에서만 timeout
  • 서버는 처리 자체는 빠른데 응답 전송 중 지연

확인 방법

  • payload 크기 로그
  • grpc.max_receive_message_length / MaxCallRecvMsgSize 설정 확인

해결

  • 큰 응답은 pagination 또는 server streaming으로 분할
  • 필요한 필드만 보내도록 proto 설계(필드 마스킹)

원인 6) gRPC Keepalive/Idle timeout 불일치로 커넥션이 끊김

프록시/LB/서버가 HTTP/2 커넥션을 idle로 판단해 끊어버리면, 다음 RPC가 재연결/재시도 중에 deadline을 소진할 수 있습니다.

증상

  • 유휴 후 첫 요청이 자주 timeout
  • 간헐적으로만 발생

확인 방법

  • LB idle timeout, 서버 keepalive 정책, 클라이언트 keepalive ping 주기 비교

해결(예시)

ka := keepalive.ClientParameters{
    Time:                30 * time.Second,
    Timeout:             10 * time.Second,
    PermitWithoutStream: true,
}
conn, err := grpc.Dial(target,
    grpc.WithTransportCredentials(creds),
    grpc.WithKeepaliveParams(ka),
)
  • 단, 과도한 keepalive는 인프라 비용/차단(특히 L7 프록시 정책) 리스크가 있으니 최소화

원인 7) 로드밸런서/인그레스가 HTTP/2(gRPC)를 제대로 처리하지 못함

특히 Kubernetes Ingress/ALB/Nginx/Envoy 설정이 gRPC(HTTP/2)와 충돌하면, 요청이 지연되거나 특정 조건에서만 실패합니다.

증상

  • Pod 직접 호출은 정상인데, Ingress/LB 경유 시 timeout
  • 특정 경로/호스트에서만 발생

확인 방법

  • LB 액세스 로그/타겟 그룹 헬스/프록시 타임아웃 설정 확인
  • gRPC는 HTTP/2 기반이므로, 프록시가 HTTP/1.1로 다운그레이드하는지 확인

해결

  • Ingress에 gRPC 백엔드 프로토콜/HTTP2 설정 명시
  • idle timeout, upstream timeout, max connection 설정 점검

ALB/Ingress 계층에서의 5xx/지연 패턴은 아래 글의 진단 프레임을 그대로 적용할 수 있습니다: EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지


원인 8) 재시도/백오프가 deadline 예산을 소모(클라이언트 인터셉터/서비스 메시)

gRPC 재시도는 “성공률”을 올리지만, 잘못 설계하면 최종적으로 deadline exceeded만 늘립니다. 예: 2초 deadline인데 내부적으로 3번 재시도하면, 각 시도는 짧게 끝나도 총합이 2초를 넘기 쉽습니다.

증상

  • 서버는 짧게 처리했는데도 클라이언트는 timeout
  • 동일 요청이 서버에 여러 번 도착(중복 처리)

확인 방법

  • 클라이언트 인터셉터/서비스 메시(Envoy/Istio)의 retry policy 확인
  • 서버 로그에서 request-id로 중복 호출 추적

해결

  • 전체 deadline을 “예산”으로 보고, (시도 횟수 × per-try timeout) 합이 예산을 넘지 않게 설계
  • 멱등성 없는 RPC는 재시도 제한(또는 request-id 기반 중복 방지)

원인 9) 서버 종료/스케일링/드레이닝 중 연결이 끊겨 지연 발생

Kubernetes에서 Pod가 종료되는 동안(rolling update, scale-in) 커넥션 드레이닝이 제대로 되지 않으면, 클라이언트는 재연결/재시도하다 deadline을 소진합니다.

증상

  • 배포/스케일링 이벤트 직후 timeout 급증
  • 특정 Pod로 라우팅될 때만 문제

확인 방법

  • Pod 이벤트에서 SIGTERM 이후 처리 시간, readiness 전환 시점 확인
  • terminationGracePeriodSeconds와 preStop hook 확인

해결

  • SIGTERM 시 readiness를 먼저 내려 트래픽 차단 후 드레이닝
  • gRPC 서버 graceful stop 사용
// 종료 시그널 처리
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)

go func() {
    <-sigCh
    // 신규 커넥션/요청을 줄이고
    grpcServer.GracefulStop()
}()

Pod 종료/드레이닝 디버깅은 아래 글의 체크리스트가 매우 유용합니다: Kubernetes Pod가 Terminating에 멈출 때 - finalizer·grace·SIGTERM 실전 디버깅


실전 트러블슈팅 순서(10분 컷 체크리스트)

  1. 클라이언트에서 Dial과 RPC 타임아웃 분리 (DialContext + WithBlock)
  2. 서버 로그로 요청 도착 여부 확인(도착 안 하면 네트워크/DNS/LB)
  3. ctx.Deadline() 로깅으로 서버가 받은 남은 예산 확인
  4. 핸들러 내부에서 ctx 미전달 I/O(HTTP/DB/Redis) 점검
  5. p95/p99와 비교해 deadline 과소 여부 확인
  6. LB/Ingress의 HTTP/2, idle timeout, upstream timeout 확인
  7. 재시도 정책이 deadline 예산을 초과하지 않는지 확인
  8. 배포/스케일 이벤트와 상관관계 확인(드레이닝/GracefulStop)
  9. 큰 메시지/스트리밍 필요 여부 점검

마무리

context deadline exceeded는 “서버가 느리다”의 동의어가 아니라, 타임아웃 예산이 어디에서 어떻게 소진되었는지를 알려주는 신호입니다. Dial 단계, DNS/LB, 서버의 ctx 전파, 리소스 병목, 재시도 예산, 종료 드레이닝까지 9가지 축으로 나누어 보면 대부분의 케이스는 빠르게 좁혀집니다.

원하시면 사용 중인 환경(예: EKS + ALB Ingress, Istio/Envoy 여부, unary/streaming, 타임아웃 값)을 알려주시면 위 9가지 중 어디부터 보는 게 가장 빠른지 우선순위를 잡아드릴게요.