AmberAx

Kubernetes에서 gRPC 로드밸런싱 문제와 해결 방법

· 4 min read
Kubernetes에서 gRPC 로드밸런싱 문제와 해결 방법

gRPC는 빠르고 효율적인 통신을 제공하기 때문에 마이크로서비스 아키텍처에서 널리 사용되고 있습니다.

그러나 Kubernetes에 gRPC 서버를 배포하면 로드밸런싱이 기대대로 작동하지 않는 문제가 발생하는 경우가 많습니다. 이 글에서는 그 원인을 분석하고 실질적인 해결 방법을 제안합니다.


gRPC와 Kubernetes의 기본 동작 방식 #

gRPC는 HTTP/2를 기반으로 작동하며, 클라이언트와 서버 간 Persistent Connection(장기 연결)을 유지합니다. 이를 통해 높은 성능과 효율적인 리소스 사용이 가능하지만, 연결 관리 방식이 Kubernetes의 기본 로드밸런싱 동작과 충돌을 일으킬 수 있습니다.

Kubernetes는 일반적으로 서비스 객체(Service)를 통해 Pod 간 트래픽을 분산시킵니다. 클라이언트는 서비스의 ClusterIP를 통해 요청을 보내고, Kubernetes는 이를 기반으로 Pod로 요청을 분산시킵니다. 이 방식은 HTTP/1.1 같은 단기 연결에서는 잘 작동하지만, HTTP/2와 gRPC에서는 문제가 발생합니다.


Kubernetes에서의 로드밸런싱 문제 #

gRPC의 로드밸런싱 문제가 발생하는 주된 이유는 다음과 같습니다.

HTTP/2의 연결 유지 특성

gRPC는 HTTP/2의 특성을 활용하여 클라이언트와 서버 간 하나의 TCP 연결을 유지합니다. 이 연결이 유지되는 동안 새로운 요청은 기존 연결을 통해 전송되기 때문에 Kubernetes의 로드밸런싱은 초기 연결 시점에만 적용됩니다. 결과적으로, 하나의 Pod에 요청이 집중되는 상황이 발생합니다.

Kubernetes의 TCP 연결 기반 로드밸런싱

Kubernetes의 kube-proxy는 TCP 연결 수준에서 라운드로빈 분산을 수행합니다. 하지만 HTTP/2와 gRPC는 기존 연결을 재사용하기 때문에 새로운 TCP 연결이 생성되지 않으며, 기본 로드밸런싱이 제대로 작동하지 않습니다.

DNS 기반 로드밸런싱의 한계

gRPC 클라이언트가 DNS를 통해 서비스 엔드포인트를 조회하더라도 대부분의 구현체는 하나의 Pod에 연결을 집중시키는 경향이 있습니다. 이는 DNS 라운드로빈만으로는 트래픽 분산이 어렵다는 것을 의미합니다.


해결 방법 #

gRPC와 Kubernetes의 로드밸런싱 문제를 해결하기 위해 사용할 수 있는 몇 가지 접근 방안을 소개합니다.

Kuberesolver

가장 간단한 방법으로 Round Robin을 적용할 수 있습니다. 구현이 쉬운 만큼 업데이트가 다소 늦어 Kubernetes의 최신 버전을 따라가지 못할 수도 있습니다.

// Import the module
import "github.com/sercand/kuberesolver/v5"

// Register kuberesolver to grpc before calling grpc.Dial
kuberesolver.RegisterInCluster()

// it is same as
resolver.Register(kuberesolver.NewBuilder(nil /*custom kubernetes client*/ , "kubernetes"))

// if schema is 'kubernetes' then grpc will use kuberesolver to resolve addresses
cc, err := grpc.Dial("kubernetes:///service.namespace:portname", opts...)

클라이언트 로드밸런싱 설정

gRPC는 자체적으로 다양한 로드밸런싱 전략을 지원합니다. 클라이언트 측에서 round_robin 정책을 명시적으로 설정하면 트래픽을 분산할 수 있습니다.

예제: Go gRPC 클라이언트

import "google.golang.org/grpc"
import "google.golang.org/grpc/balancer/roundrobin"

conn, err := grpc.Dial(
    "your-service:50051",
    grpc.WithInsecure(),
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)

주의 사항

클라이언트 로드밸런싱을 사용하려면 Kubernetes의 서비스 유형을 Headless Service로 설정하여 Pod IP 목록을 직접 반환해야 합니다.

Envoy나 Linkerd와 같은 프록시 사용

Envoy와 Linkerd 같은 서비스 메시는 gRPC와 Kubernetes 간의 로드밸런싱 문제를 효과적으로 해결할 수 있는 도구입니다. 이들은 HTTP/2 연결을 관리하고, 요청을 Pod 간에 분산시킬 수 있습니다.

Envoy 설정 예제

Envoy의 설정 파일을 통해 라운드로빈 로드밸런싱 정책을 구현할 수 있습니다.

load_balancing_policy:
  round_robin: {}

프록시를 추가하는 것은 설정의 복잡성을 증가시킬 수 있지만, 큰 규모의 클러스터에서 특히 유용합니다.

Headless Service 사용

Kubernetes의 Headless Service를 사용하면 DNS 조회 시 서비스의 ClusterIP 대신 각 Pod의 IP가 반환됩니다. 이를 통해 클라이언트가 Pod에 직접 연결할 수 있으며, 라운드로빈 로드밸런싱 정책이 더욱 효과적으로 작동합니다.

Headless Service 예제

apiVersion: v1
kind: Service
metadata:
  name: my-grpc-service
spec:
  clusterIP: None
  selector:
    app: my-grpc-app
  ports:
    - protocol: TCP
      port: 50051
      targetPort: 50051

클라이언트 연결 재설정

클라이언트에서 연결을 주기적으로 재설정하거나 새로운 연결을 강제 생성함으로써 라운드로빈 분산을 유도할 수 있습니다. 이 방법은 연결 유지가 필수가 아닌 경우에 적합합니다.


각 방법의 장단점 비교 #

방법 장점 단점
클라이언트 로드밸런싱 구현이 간단하고 클라이언트에서 제어 가능 클라이언트에 추가 설정 필요
프록시 사용 다양한 기능 지원, 복잡한 트래픽 관리 가능 설정이 복잡하고 리소스 사용 증가
Headless Service Pod 직접 연결로 간단한 설정 가능 클라이언트가 Pod 목록 관리 필요
연결 재설정 추가적인 도구 없이 문제 해결 가능 연결 유지가 필수적인 경우에는 적합하지 않음

마무리 #

Kubernetes에서 gRPC를 사용할 때 발생하는 로드밸런싱 문제는 HTTP/2의 특성과 Kubernetes의 기본 동작 방식 간의 차이에서 비롯됩니다. 이를 해결하려면 클라이언트 로드밸런싱 설정, 프록시 사용, 또는 Kubernetes의 설정을 조정하는 등 다양한 전략을 적용할 수 있습니다.

여러분의 운영 환경에 가장 적합한 해결 방법을 선택하여 안정적이고 효율적인 gRPC 서비스 환경을 구축해 보세요!

Did you find this post helpful?
Share it with others!