gRPC 헬스체크, HTTP만으론 부족한 이유
HTTP /health가 200을 반환해도, 같은 프로세스 안의 gRPC 서버는 죽어 있을 수 있습니다. 표준 gRPC 헬스체크를 함께 등록해야 하는 이유와 붙이는 방법을 정리합니다.
헬스체크란
헬스체크는 오케스트레이터(쿠버네티스, ECS 등)가 각 서버가 살아 있는지 주기적으로 확인하는 절차입니다. "정상"이라고 답하면 트래픽을 보내고, "비정상"이면 트래픽을 끊거나 재시작합니다.
비유하면 건강검진입니다. 문을 두드려 "계세요?"에 대답만 하는 것과, 실제로 진료해서 "일할 수 있는 상태인가"를 보는 것은 다릅니다. 헬스체크는 후자여야 합니다.
왜 HTTP만으론 부족한가
HTTP 헬스체크만으로는 같은 프로세스 안의 gRPC 서버가 죽었는지 알 수 없기 때문입니다. 많은 서비스가 한 프로세스에서 HTTP 서버와 gRPC 서버를 함께 띄웁니다. HTTP는 외부 요청용, gRPC는 서비스 간 통신용입니다.
이때 헬스체크를 HTTP /health로만 하면 문제가 생깁니다.

gRPC 서버가 죽었는데 HTTP /health는 멀쩡히 200을 돌려줬습니다. 오케스트레이터는 "이 서버 정상"이라 판단하고 트래픽을 계속 보냈고, 그쪽으로 간 gRPC 요청은 전부 실패했습니다. 정상 서버와 섞여 있어 실패율이 어중간했고, "죽었다"가 안 보여 오래 갔습니다. (회고: 모노레포 편)
원인은 단순합니다. 두 서버를 띄웠는데 헬스의 답이 하나뿐이었습니다. HTTP가 살아 있다고 gRPC가 살아 있는 게 아닙니다.
표준 gRPC 헬스체크 붙이기
gRPC 헬스체크는 세 단계로 붙입니다. ① 서버에 표준 Health 서비스를 등록하고, ② 오케스트레이터가 gRPC 상태를 직접 확인하게 하고, ③ 종료 시 graceful shutdown으로 트래픽을 빼냅니다.
gRPC에는 표준 헬스체크 프로토콜(grpc.health.v1.Health)이 있습니다. 서버에 이 서비스를 등록하면, 오케스트레이터가 gRPC 쪽 상태도 직접 물어볼 수 있습니다.
1. 서버에 Health 서비스 등록
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
from grpc import aio
async def serve() -> None:
server = aio.server()
add_my_servicer_to_server(MyServicer(), server)
# 표준 헬스 서비스 등록
health_servicer = health.aio.HealthServicer()
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
# 서비스가 준비되면 SERVING 으로 표시
await health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
server.add_insecure_port("[::]:50051")
await server.start()
await server.wait_for_termination()
빈 문자열 "" 키는 "서버 전체"의 상태를 뜻합니다.
2. 오케스트레이터가 gRPC를 직접 확인하게
쿠버네티스는 gRPC probe를 기본 지원합니다.
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 10
기본 gRPC probe가 없는 환경에서는 grpc_health_probe 바이너리로 확인합니다.
# 이미지에 probe 설치
RUN curl -fL "https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.4.24/grpc_health_probe-linux-amd64" \
-o /usr/local/bin/grpc_health_probe && chmod +x /usr/local/bin/grpc_health_probe
# 상태 확인 (SERVING 이면 정상 종료코드 0)
grpc_health_probe -addr=localhost:50051
3. 종료할 때는 곱게 (graceful shutdown)

배포로 서버를 내릴 때, 처리 중이던 요청을 끊으면 에러가 튑니다. 종료 신호를 받으면 먼저 헬스를 NOT_SERVING으로 바꿔 새 트래픽을 끊고, 처리 중이던 요청(in-flight)을 마친 뒤 종료합니다.
import signal
async def shutdown(server, health_servicer):
# 1) "이제 안 받음", 오케스트레이터가 트래픽을 뺌
await health_servicer.set("", health_pb2.HealthCheckResponse.NOT_SERVING)
# 2) 잠깐 대기 후 in-flight 요청 drain 하며 종료
await server.stop(grace=10)
흔한 함정
- 헬스의 정의가 실제 처리 능력과 어긋남: "포트가 열렸나"가 아니라 "요청을 받을 수 있나"를 답해야 합니다. 의존하는 DB·다운스트림이 죽었으면
NOT_SERVING을 고려합니다. - 종료 시 NOT_SERVING 전환을 빼먹음: 그러면 배포마다 미세한 에러 스파이크가 납니다.
- HTTP만 보고 안심: 한 프로세스에 서버가 둘이면, 헬스체크도 둘 다 봐야 합니다.
Q&A
- HTTP만 쓰는 서비스도 gRPC 헬스체크가 필요한가요?
- 아니요. gRPC 서버가 없으면 HTTP
/health로 충분합니다. 문제는 둘을 한 프로세스에 함께 띄울 때입니다.
- 아니요. gRPC 서버가 없으면 HTTP
- liveness와 readiness를 나눠야 하나요?
- 권 합니다. liveness는 "재시작이 필요한가", readiness는 "지금 트래픽을 받아도 되나"입니다. 종료 직전 readiness만 끄면 트래픽을 깔끔히 뺄 수 있습니다.
참고자료
- gRPC Health Checking Protocol (
grpc.health.v1) grpc-ecosystem/grpc-health-probe- Kubernetes: Configure Liveness/Readiness/Startup Probes (gRPC)