- Published on
EKS에서 gRPC 14 UNAVAILABLE 간헐 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/클라이언트 로그에 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)을 다룹니다.
해결 방향
- gRPC keepalive를 LB idle timeout보다 짧게(하지만 과도하게 짧지 않게)
- Ingress/ALB가 HTTP/2를 올바르게 처리하도록 설정(컨트롤러/annotation)
- 가능하면 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 pod에PreStop미설정 또는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”는 대부분 안정화할 수 있습니다.