Published on

EKS에서 gRPC 14 UNAVAILABLE 간헐 해결법

Authors

서버/클라이언트 로그에 rpc error: code = Unavailable desc = ...가 간헐적으로만 찍히면 가장 난감한 점은 “항상”이 아니라 “가끔”이라는 사실입니다. EKS에서는 이 증상이 네트워크 자체 장애라기보다 로드밸런서/Ingress의 연결 정책, Pod 종료/스케일링 시 드레이닝, DNS(CoreDNS) 순간 실패, 노드/Pod 리소스 압박으로 인한 지연 같은 주변 요소에서 많이 발생합니다.

이 글에서는 gRPC status code 14 UNAVAILABLE를 EKS에서 실전적으로 줄이는 방법을, 재현 포인트 → 관측 지표 → 원인별 처방 순서로 정리합니다.

gRPC 14 UNAVAILABLE이 의미하는 것(현상 분류)

UNAVAILABLE은 “서버에 도달하지 못했거나(transport error) 연결이 중간에 끊겼거나, 서버가 준비되지 않았다”에 가깝습니다. EKS에서 자주 만나는 패턴은 다음과 같습니다.

  • RST/FIN 또는 idle timeout으로 HTTP/2 스트림이 끊김 → 클라이언트에서 UNAVAILABLE
  • Pod가 Terminating 중인데 트래픽이 계속 유입 → 처리 중 연결 끊김
  • CoreDNS 간헐 오류로 서비스 이름 해석 실패 → dial 실패로 UNAVAILABLE
  • HPA/롤링 업데이트 중 엔드포인트 변동 + keepalive 정책 미스매치 → 연결 재설정
  • 노드/Pod CPU throttling, DiskPressure 등으로 서버가 응답 못하고 타임아웃 → 결과적으로 UNAVAILABLE

핵심은 “gRPC 자체”보다 EKS 트래픽 경로(클라이언트 → Ingress/LB → Service → Pod) 어디에서 끊기는지 먼저 특정하는 것입니다.

먼저 확인할 관측 포인트(로그/메트릭/패킷)

1) 클라이언트 로그에서 desc 패턴 분류

언어별로 desc가 조금씩 다르지만 다음 키워드가 힌트입니다.

  • connection reset by peer / RST_STREAM → 중간 장비(LB/프록시)나 서버가 연결을 리셋
  • transport is closing → channel이 닫힘(keepalive/idle/서버 종료)
  • name resolver error / no such host → DNS(CoreDNS) 의심
  • deadline exceeded → 서버 지연(리소스/백엔드)

2) gRPC 서버(Pod) 이벤트/종료 로그

Pod가 내려가는 시점과 UNAVAILABLE 발생 시점을 맞춰보면 원인 좁히기가 매우 빨라집니다.

kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -n 50
kubectl describe pod -n <ns> <pod>
kubectl logs -n <ns> <pod> --previous

3) Service 엔드포인트 변동 확인

엔드포인트가 자주 바뀌거나 순간적으로 0이 되면(특히 스케일 인/롤링 시) 연결 실패가 튈 수 있습니다.

kubectl get endpointslice -n <ns> -l kubernetes.io/service-name=<svc> -w

4) CoreDNS 상태 점검

서비스 디스커버리 실패는 gRPC에서 매우 흔한 “간헐” 원인입니다. CoreDNS 로그에 SERVFAIL, NXDOMAIN이 튀는지 확인하세요.

kubectl -n kube-system logs deploy/coredns --tail=200
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide

CoreDNS 간헐 이슈 체크리스트는 아래 글도 같이 보면 진단 속도가 올라갑니다.

원인 1) ALB/NLB/Ingress의 HTTP/2, Idle timeout, 연결 재설정

EKS에서 gRPC를 Ingress로 노출할 때 가장 흔한 함정은 로드밸런서가 HTTP/2 연결을 오래 유지하지 못하거나, idle timeout 정책과 gRPC keepalive가 충돌하는 경우입니다. 이때 클라이언트는 “갑자기” UNAVAILABLE을 받습니다.

증상

  • 특정 시간(예: 60초/300초) 간격으로 뚝뚝 끊김
  • 장시간 idle 후 첫 요청에서 실패
  • ALB 액세스 로그/타겟 로그에 5xx/target reset 계열이 섞임

아래 글들이 같은 계열의 현상(간헐 5xx, reset, idle timeout loop)을 다룹니다.

해결 방향

  1. gRPC keepalive를 LB idle timeout보다 짧게(하지만 과도하게 짧지 않게)
  2. Ingress/ALB가 HTTP/2를 올바르게 처리하도록 설정(컨트롤러/annotation)
  3. 가능하면 gRPC는 NLB(TCP) + gRPC 서버에서 TLS/HTTP2 직접 처리가 안정적인 경우도 많음

예시: 클라이언트 keepalive(Go)

import (
  "time"
  "google.golang.org/grpc"
  "google.golang.org/grpc/keepalive"
)

ka := keepalive.ClientParameters{
  Time:                30 * time.Second, // ping interval
  Timeout:             10 * time.Second,
  PermitWithoutStream: true,
}

conn, err := grpc.Dial(
  target,
  grpc.WithInsecure(),
  grpc.WithKeepaliveParams(ka),
)

예시: 서버 keepalive(Go)

import (
  "time"
  "google.golang.org/grpc"
  "google.golang.org/grpc/keepalive"
)

srv := grpc.NewServer(
  grpc.KeepaliveParams(keepalive.ServerParameters{
    Time:    60 * time.Second,
    Timeout: 20 * time.Second,
  }),
  grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
    MinTime:             10 * time.Second,
    PermitWithoutStream: true,
  }),
)

> 포인트: LB가 idle timeout으로 끊기기 전에 “정상적인 ping/트래픽”으로 연결을 유지하거나, 반대로 keepalive가 너무 공격적이라 중간 장비에서 차단/리셋되지 않게 균형을 잡는 것입니다.

원인 2) Pod 종료(롤링 업데이트/HPA 스케일 인) 중 연결이 끊김

EKS에서 간헐 UNAVAILABLE의 2번째 큰 원인은 Pod가 Terminating인데도 연결이 계속 들어오는 상황입니다. 특히 gRPC는 장기 연결이 많아, 롤링 업데이트 때 기존 커넥션이 깔끔하게 드레인되지 않으면 “가끔” 터집니다.

체크

  • UNAVAILABLE 발생 시점에 Pod가 Terminating
  • kubectl describe podPreStop 미설정 또는 terminationGracePeriodSeconds가 짧음
  • readiness가 내려가기 전에 트래픽이 계속 유입

해결 1) readiness를 먼저 내리고, 충분히 기다린 뒤 종료

핵심은 (1) readiness false → (2) 드레이닝 시간 확보 → (3) SIGTERM 처리 후 종료 순서를 만들기입니다.

Kubernetes 배포 예시(PreStop + grace period)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpc-api
spec:
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: app
        image: yourrepo/grpc-api:1.0.0
        ports:
        - containerPort: 50051
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]
        readinessProbe:
          tcpSocket:
            port: 50051
          periodSeconds: 5
          failureThreshold: 1
  • readinessProbe가 실패하면 엔드포인트에서 빠져 새 연결 유입이 줄어듭니다.
  • preStop으로 약간의 시간을 벌어 기존 연결/요청을 마무리합니다.
  • terminationGracePeriodSeconds는 서버가 SIGTERM을 받고 “정리할 시간”입니다.

해결 2) gRPC 서버의 graceful shutdown 구현

Go 기준으로는 GracefulStop()을 사용하고, SIGTERM을 받아 새 요청을 막고 기존 스트림을 정리합니다.

ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)

go func() {
  <-ch
  // 새 커넥션/요청을 받지 않고 기존 RPC 마무리
  grpcServer.GracefulStop()
}()

해결 3) PDB/HPA/스케일 인 정책 재점검

스케일 인이 너무 공격적이면 연결이 자주 끊깁니다. 최소 레플리카, PDB, HPA 안정화 윈도우를 함께 보세요.

원인 3) CoreDNS 간헐 실패로 name resolution이 깨짐

gRPC 클라이언트가 dns:///service.namespace.svc.cluster.local 같은 타깃을 쓰는 경우, CoreDNS가 순간적으로 실패하면 dial 자체가 실패하고 UNAVAILABLE이 튈 수 있습니다.

해결 방향

  • CoreDNS 리소스(특히 CPU) 여유 확보, autoscaling
  • NodeLocal DNSCache 도입 검토
  • upstream resolver/네트워크 경로 점검

CoreDNS는 원인과 해결 옵션이 다양하므로 아래 글의 체크리스트를 권장합니다.

원인 4) kube-proxy/네트워크 모드 변경, conntrack 이슈

클러스터 네트워킹 계층 문제가 있으면 서비스 VIP/엔드포인트 NAT 경로에서 드롭이 발생해 UNAVAILABLE이 나올 수 있습니다. 특히 kube-proxy 모드(IPVS 전환 등)나 커널/conntrack 튜닝이 얽히면 “간헐”이 됩니다.

  • IPVS 전환 이후 장애를 겪었다면 아래 사례를 참고해 원복/튜닝 포인트를 확인하세요.

EKS kube-proxy를 IPVS로 바꾼 뒤 통신 장애 복구

원인 5) 노드/Pod 리소스 압박(CPU throttling, DiskPressure)로 응답 지연

서버가 죽지 않아도, GC/CPU throttling/IO wait로 이벤트 루프가 밀리거나 핸들러가 늦어지면 클라이언트는 deadline을 넘기고 재시도하며 결과적으로 UNAVAILABLE처럼 보이기도 합니다.

체크

  • Pod CPU limit이 너무 낮아 throttling 심함
  • 노드 DiskPressure로 eviction/IO 지연
  • p99 latency가 튀는 시점과 UNAVAILABLE가 겹침

DiskPressure/eviction 폭주는 특히 “간헐 오류”를 대량으로 만들 수 있습니다.

실전 처방 순서(가장 효과 큰 것부터)

1) Terminating 드레이닝부터 고치기

  • readinessProbe 확실히 설정
  • preStop + 충분한 grace period
  • 서버 graceful shutdown

간헐 UNAVAILABLE의 상당수가 여기서 사라집니다.

2) Ingress/LB idle timeout ↔ gRPC keepalive 정렬

  • keepalive를 “너무 잦지 않게, 너무 길지 않게”
  • 장시간 idle 후 실패가 있으면 idle timeout을 의심
  • ALB/NLB 경로에서 reset/5xx가 있는지 로그로 확인

3) DNS(CoreDNS) 흔들림 제거

  • CoreDNS 리소스/스케일/캐시 전략 재점검
  • 서비스 디스커버리 실패 로그가 있다면 우선순위 높임

4) 네트워크 계층(kube-proxy/conntrack)과 리소스 압박 점검

  • 최근 네트워크 모드 변경 여부 확인
  • 노드 상태(NotReady, DiskPressure, 네트워크 드롭) 확인

재현/검증용: grpcurl로 장기 연결/연속 호출 테스트

간헐 이슈는 “재현 가능한 부하/패턴”을 만드는 게 중요합니다.

# 1) 짧은 간격으로 반복 호출
for i in $(seq 1 2000); do
  grpcurl -plaintext -max-time 2 \
    -d '{"ping":"pong"}' \
    <svc>.<ns>.svc.cluster.local:50051 your.pkg.Service/HealthCheck \
    || echo "fail at $i"
  sleep 0.2
done
  • 롤링 업데이트 중에도 돌려서 실패율이 튀는지 확인
  • Ingress 경유/ClusterIP 직결을 각각 테스트해 “끊기는 지점”을 분리

마무리: UNAVAILABLE는 ‘원인’이 아니라 ‘경로 문제의 결과’

EKS에서 gRPC 14 UNAVAILABLE가 간헐로 발생하면, 가장 먼저 Pod 종료 드레이닝LB idle timeout/keepalive 정렬을 의심하는 것이 비용 대비 효과가 큽니다. 그 다음이 CoreDNS 간헐 실패, 마지막이 클러스터 네트워크/리소스 압박입니다.

운영 중인 트래픽 경로(직접 Service로 붙는지, ALB/NLB/Ingress를 타는지), 배포 전략(롤링/HPA), 그리고 keepalive/deadline 설정을 함께 맞추면 “가끔 끊기는 gRPC”는 대부분 안정화할 수 있습니다.