TT Lab
블로그 시작하기
배우기 러닝패스 코스

gRPC 서비스를 직접 세우고 운영한다

쿠버네티스 위의 gRPC: 헬스 체크와 부하 분산

TT Lab 에서 이어서 보기

한 줄 요약

쿠버네티스의 기본 Service 는 연결을 맺는 순간에만 파드를 고르고, gRPC 는 연결 하나에 호출을 몰아 보냅니다. 그래서 호출을 나누려면 클라이언트가 파드 주소를 모두 알고(헤드리스 서비스) 호출마다 고르거나(round_robin), 서버가 연결에 수명을 주어야 합니다. 트래픽에서 빼는 일은 grpc.health.v1 과 kubelet 의 gRPC 프로브가 맡습니다.

왜 이게 필요했나

HTTP/1.1 클라이언트는 연결을 짧게 쓰고 자주 새로 맺습니다. kube-proxy 가 연결마다 파드를 무작위로 고르니 호출이 저절로 고르게 퍼집니다. gRPC 클라이언트는 HTTP/2 연결 하나를 오래 열어 두고 그 위에 호출을 겹쳐 보냅니다. 파드를 셋으로 늘려도 이미 맺은 연결은 처음 고른 파드에 묶여 있어서, 대시보드에는 파드 하나만 CPU 를 쓰는 그림이 나오고 자동 확장은 늘린 파드를 놀립니다.

헬스 체크도 사정이 다릅니다. 예전에는 gRPC 서버에 HTTP 헬스 엔드포인트를 따로 열거나 grpc_health_probe 바이너리를 이미지에 넣어 exec 프로브로 불렀습니다. 쿠버네티스 1.27 부터 kubelet 이 gRPC 헬스 프로토콜을 직접 부르는 grpc 프로브가 정식 기능입니다.

어떻게 동작하나

gRPC 채널 안에는 리졸버와 부하 분산 정책이 있습니다. 리졸버는 대상 이름을 주소 목록으로 바꾸고, 정책은 그 주소마다 연결(하위 채널)을 어떻게 쓸지 정합니다. 기본 정책 pick_first 는 주소가 여럿이어도 연결 하나만 씁니다. round_robin 은 모든 주소에 연결하고 호출마다 돌아가며 고릅니다. 실습 이미지에서 주소 셋에 60호출을 보내 보면 pick_first 는 60·0·0, round_robin 은 21·20·19 였습니다(실측).

ClusterIP   whoami:50051           DNS → 가상 IP 하나 → kube-proxy 가 연결마다 파드 하나   (호출은 그 파드로만)
헤드리스     dns:///whoami-headless  DNS → Ready 인 파드 IP 전부 → round_robin 이 호출마다 고름

정책은 서비스 설정(service config)으로 고릅니다. 채널 옵션 grpc.service_config 에 JSON 을 넣으며, 그 안에 loadBalancingConfig·methodConfig(재시도·기한)·healthCheckConfig 를 둡니다.

헬스 프로토콜 grpc.health.v1.Health 는 Check 와 Watch 두 메서드입니다. 서비스 이름 "" 는 서버 전체를, 서비스 이름은 그 서비스를 묻습니다. 둘을 나누면 「프로세스는 살아 있지만 이 서비스는 트래픽을 받지 않는다」 를 표현할 수 있습니다. kubelet 의 grpc 프로브는 port 와 service 를 받아 Check 를 부르고 SERVING 이 아니면 실패로 셉니다. 서버가 모르는 서비스 이름을 적으면 NOT_FOUND 가 돌아와 파드가 영영 Ready 가 되지 않습니다. 포트는 이름이 아니라 숫자로 적습니다.

readiness 가 실패하면 Endpoints 에서 빠지고 헤드리스 DNS 도 그 주소를 빼지만, 이미 연결해 둔 클라이언트는 연결이 끊기기 전까지 주소를 다시 묻지 않습니다. 그 틈을 메우는 것이 클라이언트 헬스 체크입니다. healthCheckConfig 에 서비스 이름을 주면 채널이 하위 채널마다 Health/Watch 를 열고, NOT_SERVING 을 받은 서버를 고르지 않습니다. 실습 이미지에서 Watch 가 NOT_SERVING 을 보낸 서버는 다음 60호출에서 하나도 받지 않았고, 설정이 없는 round_robin 은 그 서버에도 20호출을 보냈습니다(실측).

ClusterIP 를 쓰는 클라이언트를 모두 고칠 수 없다면 서버 쪽에서 grpc.max_connection_age_ms 로 연결에 수명을 줍니다. 수명이 다하면 서버가 GOAWAY 를 보내고, 클라이언트는 다시 연결하며 kube-proxy 가 파드를 다시 고릅니다. grace 를 함께 주면 진행 중인 호출은 끝까지 갑니다. 실측에서 연결을 0.5초마다 닫아도 40호출 중 실패는 없었습니다.

현장에서 만나는 모습

배포 직후 새 파드는 놀고 옛 파드만 뜨거운 장면, 파드 하나만 OOM 으로 죽는 장면이 이 쏠림의 흔한 얼굴입니다. liveness 에 무거운 의존성(DB 연결)을 걸어 DB 가 잠깐 흔들릴 때 모든 파드가 함께 재시작되는 사고도 흔합니다. liveness 는 프로세스만, readiness 는 트래픽을 받을 준비를 묻게 나눕니다.

헤드리스와 클라이언트 정책 대신 호출 단위로 나눠 주는 L7 프록시(Envoy·서비스 메시)를 앞에 두는 방법도 있고, 프록시 없이 라이브러리가 xDS 로 설정을 받는 방법도 있습니다. 원리와 실측은 블로그 글 쿠버네티스에서의 gRPC: 한 파드로 쏠리는 이유와 헬스 체크 에 있고, 그 글이 「미실측」 으로 남긴 쏠림 재현을 다음 실습이 직접 합니다.

다음 실습에서 할 것

k3s VM 에서 헬스 서비스와 Drain 이 있는 서버를 만들어 gRPC 프로브를 단 디플로이먼트로 올립니다. 파드 하나를 Drain 해 트래픽에서만 빠지는 것을 보고, ClusterIP 로 붙은 클라이언트가 한 파드에 묶이는 것을 잰 뒤, 헤드리스 서비스와 round_robin, 클라이언트 헬스 체크, 서버의 연결 수명으로 차례로 풉니다.