다음을 통해 공유


gRPC 클라이언트 쪽 부하 분산

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

작성자: James Newton-King

클라이언트 쪽 부하 분산은 gRPC 클라이언트가 가용 서버 간에 부하를 최적으로 배포할 수 있게 하는 기능입니다. 이 문서에서는 .NET에서 확장성 있는 고성능 gRPC 앱을 만들기 위해 클라이언트 쪽 부하 분산을 구성하는 방법을 설명합니다.

클라이언트 쪽 부하 분산은 다음을 요구합니다.

gRPC 클라이언트 쪽 부하 분산 구성

클라이언트 쪽 부하 분산은 채널을 만들 때 구성됩니다. 부하 분산을 사용할 때는 다음과 같은 두 가지 요소를 고려해야 합니다.

  • 첫 번째는 채널의 주소를 확인하는 확인자입니다. 확인자는 외부 소스에서의 주소 가져오기를 지원합니다. 이 기능은 서비스 검색이라고도 합니다.
  • 두 번째는 연결을 만들고 gRPC 통화에서 사용할 주소를 선택하는 부하 분산 장치입니다.

해결 프로그램 및 부하 분산 장치의 기본 제공 구현이 포함되어 Grpc.Net.Client있습니다. 사용자 지정 확인자 및 부하 분산 장치를 작성하여 부하 분산을 확장할 수도 있습니다.

주소, 연결 및 기타 부하 분산 상태는 GrpcChannel 인스턴스에 저장됩니다. 부하 분산이 제대로 작동하려면 gRPC 호출을 할 때 채널을 다시 사용해야 합니다.

참고 항목

일부 부하 분산 구성은 DI(종속성 주입)를 사용합니다. DI를 사용하지 않는 앱은 인스턴스를 ServiceCollection 만들 수 있습니다.

앱에 ASP.NET Core 웹 사이트와 같은 DI 설정이 이미 있는 경우 형식을 기존 DI 인스턴스에 등록해야 합니다. GrpcChannelOptions.ServiceProvider는 DI에서 IServiceProvider를 가져와 구성합니다.

확인자 구성

확인자는 채널 생성에 사용한 주소를 이용하여 구성됩니다. 주소의 URI 스키마로 확인자를 지정합니다.

구성표 Type 설명
dns DnsResolverFactory DNS 주소 레코드의 호스트 이름을 쿼리하여 주소를 확인합니다.
static StaticResolverFactory 앱에서 지정한 주소를 확인합니다. 호출하는 주소를 앱에서 이미 알고 있는 경우에 권장됩니다.

채널은 확인자와 일치하는 URI를 직접 호출하지 않습니다. 대신 일치하는 확인자를 생성하여 주소 확인에 사용합니다.

예를 들어 GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })를 사용하는 경우

  • dns 스키마는 DnsResolverFactory에 매핑합니다. 채널에 대한 DNS 확인자의 새 인스턴스가 생성됩니다.
  • 확인자가 my-example-host를 대상으로 DNS 쿼리를 수행하고 127.0.0.100127.0.0.101이라는 두 가지 결과를 얻습니다.
  • 부하 분산 장치는 127.0.0.100:80127.0.0.101:80을 사용하여 연결을 만들고 gRPC 호출을 합니다.

DnsResolverFactory

DnsResolverFactory는 외부 소스에서 주소를 가져오도록 설계된 확인자를 만듭니다. DNS 확인은 주로 Kubernetes 헤드리스 서비스를 포함하는 Pod 인스턴스에 부하를 분산하는 용도로 사용합니다.

var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });

앞의 코드가 하는 역할은 다음과 같습니다.

  • dns:///my-example-host 주소를 사용하여 만든 채널을 구성합니다.
    • dns 스키마는 DnsResolverFactory에 매핑합니다.
    • my-example-host 은 확인할 호스트 이름입니다.
    • 주소에 포트가 지정되지 않으므로 gRPC 호출은 포트 80으로 전송됩니다. 이는 보안되지 않은 채널의 기본 포트입니다. 필요에 따라 호스트 이름 다음에 포트를 지정할 수 있습니다. 예를 들어 dns:///my-example-host:8080 포트 8080으로 보내도록 gRPC 호출을 구성합니다.
  • 분산 장치를 지정하지 마세요. 채널은 기본적으로 첫 번째 부하 분산 장치를 선택합니다.
  • gRPC 호출 SayHello을 시작합니다.
    • DNS 확인자는 호스트 이름인 my-example-host의 주소를 가져옵니다.
    • 첫 번째 부하 분산 장치를 선택하면 확인된 주소 중 하나에 연결합니다.
    • 채널이 성공적으로 연결된 첫 번째 주소에 호출이 전송됩니다.
DNS 주소 캐싱

부하를 분산할 때는 성능이 중요합니다. 주소를 캐싱하여 주소 확인의 대기 시간을 gRPC 호출에서 제거합니다. 첫 번째 gRPC 호출을 하면 확인자가 호출되며 후속 호출에서는 캐시를 사용합니다.

연결이 중단되면 주소가 자동으로 새로 고침됩니다. 새로 고침은 런타임 시 주소가 변경되는 상황에서는 대단히 중요합니다. 예를 들어 Kubernetes에서는 다시 시작한 Pod가 DNS 확인자를 트리거하여 새로 고침을 수행해 Pod의 새 주소를 얻습니다.

기본적으로 연결이 중단되면 DNS 확인자도 새로 고쳐집니다. DNS 확인자는 필요에 따라 주기적으로 새로 고칠 수도 있습니다. 그러면 새 Pod 인스턴스를 빠르게 검색하는 데 유용할 수 있습니다.

services.AddSingleton<ResolverFactory>(
    sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));

위의 코드는 새로 고침 간격으로 DnsResolverFactory를 만들고 종속성 주입에 등록합니다. 사용자 지정 구성 확인자 사용에 대한 자세한 내용은 사용자 지정 확인자 및 부하 분산 장치 구성을 참조하세요.

StaticResolverFactory

정적 확인자는 StaticResolverFactory에서 제공합니다. 이 확인자는

  • 외부 소스를 호출하지 않습니다. 대신 클라이언트 앱에서 주소를 구성합니다.
  • 호출하는 주소를 앱에서 이미 알고 있는 상황을 위해 설계되었습니다.
var factory = new StaticResolverFactory(addr => new[]
{
    new BalancerAddress("localhost", 80),
    new BalancerAddress("localhost", 81)
});

var services = new ServiceCollection();
services.AddSingleton<ResolverFactory>(factory);

var channel = GrpcChannel.ForAddress(
    "static:///my-example-host",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceProvider = services.BuildServiceProvider()
    });
var client = new Greet.GreeterClient(channel);

앞의 코드가 하는 역할은 다음과 같습니다.

  • StaticResolverFactory을 만듭니다. 이 팩터리는 localhost:80localhost:81이라는 두 가지 주소를 알고 있습니다.
  • 팩터리에 DI(종속성 주입)를 등록합니다.
  • 다음을 사용하여 만든 채널을 구성합니다.
    • static:///my-example-host 주소. static 스키마는 정적 확인자에 매핑합니다.
    • DI 서비스 공급자를 이용해 GrpcChannelOptions.ServiceProvider를 설정합니다.

이 예제에서는 DI를 위한 새 ServiceCollection을 만듭니다. 앱에 ASP.NET Core 웹 사이트 같은 DI 설정이 이미 있다고 가정하겠습니다. 이 경우 형식을 기존 DI 인스턴스에 등록해야 합니다. GrpcChannelOptions.ServiceProvider는 DI에서 IServiceProvider를 가져와 구성합니다.

부하 분산 장치 구성

부하 분산 장치는 ServiceConfig.LoadBalancingConfigs 컬렉션을 사용하여 service config에서 지정됩니다. 두 부하 분산 장치는 기본 제공되며 다음과 같은 부하 분산 장치 구성 이름에 매핑됩니다.

속성 형식 설명
pick_first PickFirstLoadBalancerFactory 연결될 때까지 주소에 연결을 시도합니다. gRPC 호출은 모두 처음 성공한 연결을 대상으로 수행됩니다.
round_robin RoundRobinLoadBalancerFactory 모든 주소에 연결을 시도합니다. gRPC 호출은 라운드 로빈 논리를 사용하여 모든 성공적인 연결에 배포됩니다.

service config는 서비스 구성의 약자이며 ServiceConfig 형식으로 표시됩니다. 채널은 부하 분산 장치가 구성된 service config를 몇 가지 방법으로 얻을 수 있습니다.

  • 앱은 GrpcChannelOptions.ServiceConfig를 이용해 채널을 생성한 경우 service config를 지정할 수 있습니다.
  • 확인자는 채널에 대한 service config를 확인할 수도 있습니다. 이 기능을 사용하면 호출자가 부하 분산을 수행하는 방법을 외부 소스에서 지정할 수 있습니다. 확인자의 service config 확인 지원 여부는 확인자 구현에 따라 달라집니다. GrpcChannelOptions.DisableResolverServiceConfig를 이용해 이 기능을 사용하지 않도록 설정하세요.
  • service config가 제공되지 않거나 service config에 부하 분산 장치가 구성되지 않았다면 채널은 기본값은 PickFirstLoadBalancerFactory로 설정됩니다.
var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
    });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });

앞의 코드가 하는 역할은 다음과 같습니다.

  • service config에서 RoundRobinLoadBalancerFactory를 지정합니다.
  • gRPC 호출 SayHello을 시작합니다.
    • DnsResolverFactory에서 호스트 이름 my-example-host의 주소를 가져오는 확인자를 만듭니다.
    • 라운드 로빈 부하 분산 장치는 모든 확인된 주소에 연결을 시도합니다.
    • gRPC 호출은 라운드 로빈 논리를 사용하여 균등하게 분산됩니다.

채널 자격 증명 구성

채널은 gRPC 호출이 전송 보안을 사용하여 수행되는지를 알고 있어야 합니다. httphttps는 더 이상 주소의 일부가 아니며, 이제 스키마는 확인자를 지정하므로 부하 분산을 사용할 때는 채널 옵션에서 Credentials를 구성해야 합니다.

  • ChannelCredentials.SecureSsl - gRPC 호출은 TLS(전송 계층 보안)로 보호됩니다. https 주소와 동일합니다.
  • ChannelCredentials.Insecure - gRPC 호출은 전송 보안을 사용하지 않습니다. http 주소와 동일합니다.
var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });

gRPC 클라이언트 팩터리에서 부하 분산 사용

부하 분산을 사용하도록 gRPC 클라이언트 팩터 리를 구성할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("dns:///my-example-host");
    })
    .ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

builder.Services.AddSingleton<ResolverFactory>(
    sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));

var app = builder.Build();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 부하 분산 주소를 사용하여 클라이언트를 구성합니다.
  • 채널 자격 증명을 지정합니다.
  • DI 형식을 앱에 등록합니다 IServiceCollection.

사용자 지정 확인자 및 부하 분산 장치 작성

클라이언트 쪽 부하 분산은 확장 가능합니다.

  • Resolver를 구현하여 사용자 지정 확인자를 만들고 새 데이터 원본에서 주소를 확인합니다.
  • LoadBalancer를 구현하여 새 부하 분산 동작을 이용해 사용자 지정 부하 분산 장치를 만듭니다.

Important

클라이언트 쪽 부하 분산을 확장하는 데 사용하는 API는 실험 단계입니다. 예고 없이 변경될 수 있습니다.

사용자 지정 확인자 만들기

확인자는

  • Resolver를 구현하며 ResolverFactory에서 생성합니다. 이러한 형식을 구현하여 사용자 지정 확인자를 만듭니다.
  • 부하 분산 장치에서 사용하는 주소를 확인할 책임이 있습니다.
  • 필요에 따라 서비스 구성을 제공할 수 있습니다.
public class FileResolver : PollingResolver
{
    private readonly Uri _address;
    private readonly int _port;

    public FileResolver(Uri address, int defaultPort, ILoggerFactory loggerFactory)
        : base(loggerFactory)
    {
        _address = address;
        _port = defaultPort;
    }

    public override async Task ResolveAsync(CancellationToken cancellationToken)
    {
        // Load JSON from a file on disk and deserialize into endpoints.
        var jsonString = await File.ReadAllTextAsync(_address.LocalPath);
        var results = JsonSerializer.Deserialize<string[]>(jsonString);
        var addresses = results.Select(r => new BalancerAddress(r, _port)).ToArray();

        // Pass the results back to the channel.
        Listener(ResolverResult.ForResult(addresses));
    }
}

public class FileResolverFactory : ResolverFactory
{
    // Create a FileResolver when the URI has a 'file' scheme.
    public override string Name => "file";

    public override Resolver Create(ResolverOptions options)
    {
        return new FileResolver(options.Address, options.DefaultPort, options.LoggerFactory);
    }
}

위의 코드에서

  • FileResolverFactoryResolverFactory를 구현합니다. file 스키마에 매핑되고 FileResolver 인스턴스를 만듭니다.
  • FileResolverPollingResolver를 구현합니다. PollingResolverResolveAsync를 재정의하여 비동기 논리로 확인자를 쉽게 구현할 수 있는 추상 기본 형식입니다.
  • In ResolveAsync:
    • 파일 URI는 로컬 경로로 변환됩니다. 예를 들어 file:///c:/addresses.jsonc:\addresses.json이 됩니다.
    • JSON은 디스크에서 로드되고 주소 컬렉션으로 변환됩니다.
    • 수신기가 결과와 함께 호출되어, 주소를 사용할 수 있음을 채널에 알립니다.

사용자 지정 부하 분산 장치 만들기

부하 분산 장치는 다음을 수행합니다

  • LoadBalancer를 구현하며 LoadBalancerFactory에서 생성합니다. 이러한 형식을 구현하여 사용자 지정 부하 분산 장치와 팩터리를 만듭니다.
  • 확인자로부터 주소를 받으며 Subchannel 인스턴스를 만듭니다.
  • 연결 상태를 추적하고 SubchannelPicker를 만듭니다. 채널은 내부에서 선택기를 사용하여, gRPC 호출을 할 때 주소를 선택합니다.

SubchannelsLoadBalancer

  • LoadBalancer를 구현하는 추상 기본 클래스입니다.
  • 주소에서 Subchannel 인스턴스를 만드는 작업을 관리합니다.
  • 하위 채널 컬렉션에 대한 사용자 지정 선택 정책을 쉽게 구현할 수 있습니다.
public class RandomBalancer : SubchannelsLoadBalancer
{
    public RandomBalancer(IChannelControlHelper controller, ILoggerFactory loggerFactory)
        : base(controller, loggerFactory)
    {
    }

    protected override SubchannelPicker CreatePicker(List<Subchannel> readySubchannels)
    {
        return new RandomPicker(readySubchannels);
    }

    private class RandomPicker : SubchannelPicker
    {
        private readonly List<Subchannel> _subchannels;

        public RandomPicker(List<Subchannel> subchannels)
        {
            _subchannels = subchannels;
        }

        public override PickResult Pick(PickContext context)
        {
            // Pick a random subchannel.
            return PickResult.ForSubchannel(_subchannels[Random.Shared.Next(0, _subchannels.Count)]);
        }
    }
}

public class RandomBalancerFactory : LoadBalancerFactory
{
    // Create a RandomBalancer when the name is 'random'.
    public override string Name => "random";

    public override LoadBalancer Create(LoadBalancerOptions options)
    {
        return new RandomBalancer(options.Controller, options.LoggerFactory);
    }
}

위의 코드에서

  • RandomBalancerFactoryLoadBalancerFactory를 구현합니다. random 정책 이름에 매핑되고 RandomBalancer 인스턴스를 만듭니다.
  • RandomBalancerSubchannelsLoadBalancer를 구현합니다. 하위 채널을 무작위로 선택하는 RandomPicker를 만듭니다.

사용자 지정 확인자 및 부하 분산 장치 구성

사용자 지정 확인자와 부하 분산 장치는 사용 시 DI(종속성 주입)를 이용해 등록해야 합니다. 두 가지 선택지가 있습니다.

  • 앱이 이미 ASP.NET Core 웹앱 같은 DI를 사용하고 있다면 기존 DI 구성을 이용해 등록할 수 있습니다. IServiceProvider는 DI에서 확인하고 GrpcChannelOptions.ServiceProvider를 이용해 채널에 전달할 수 있습니다.
  • 앱이 DI를 사용하지 않는 경우 다음을 만듭니다.
var services = new ServiceCollection();
services.AddSingleton<ResolverFactory, FileResolverFactory>();
services.AddSingleton<LoadBalancerFactory, RandomLoadBalancerFactory>();

var channel = GrpcChannel.ForAddress(
    "file:///c:/data/addresses.json",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new LoadBalancingConfig("random") } },
        ServiceProvider = services.BuildServiceProvider()
    });
var client = new Greet.GreeterClient(channel);

앞의 코드가 하는 역할은 다음과 같습니다.

  • ServiceCollection을 만들고 새 확인자 및 부하 분산 장치 구현을 등록합니다.
  • 새 구현을 사용하도록 구성된 채널을 만듭니다.
    • ServiceCollectionIServiceProvider에서 구축되고 GrpcChannelOptions.ServiceProvider로 설정됩니다.
    • 채널 주소는 file:///c:/data/addresses.json입니다. file 스키마는 FileResolverFactory에 매핑합니다.
    • service config 부하 분산 장치 이름은 random입니다. RandomLoadBalancerFactory에 매핑합니다.

부하 분산이 중요한 이유

HTTP/2는 단일 TCP 연결에서 여러 호출을 멀티플렉싱합니다. gRPC 및 HTTP/2를 NLB(네트워크 부하 분산 장치)와 함께 사용하는 경우, 연결은 서버로 전달되고 모든 gRPC 호출이 해당 서버로 전송됩니다. NLB에서의 다른 서버 인스턴스는 유휴 상태입니다.

네트워크 부하 분산 장치는 빠르고 간단하기 때문에 부하 분산을 위한 대표적인 솔루션입니다. 예를 들어 Kubernetes에서는 기본적으로 네트워크 부하 분산 장치를 사용하여 Pod 인스턴스 간의 연결 균형을 조정합니다. 그러나 네트워크 부하 분산 장치는 gRPC 및 HTTP/2와 함께 사용하는 경우에는 부하 분산 효과가 떨어집니다.

프록시와 클라이언트 쪽 부하 분산 중 무엇을 사용해야 하나요?

gRPC 및 HTTP/2는 애플리케이션 부하 분산 장치 프록시 또는 클라이언트 쪽 부하 분산을 사용하여 효과적으로 부하를 분산할 수 있습니다. 두 가지 선택지 모두 개별 gRPC 호출을 가용 서버에 분산할 수 있습니다. 프록시와 클라이언트 쪽 부하 분산을 선택하는 기준은 아키텍처 선택입니다. 두 방식 모두 고유한 장단점이 있습니다.

  • 프록시: gRPC 호출이 프록시로 전송되고, 프록시가 부하 분산을 결정하고, gRPC 호출이 최종 엔드포인트로 전송됩니다. 프록시는 엔드포인트를 확인할 책임이 있습니다. 프록시를 사용하면 다음을 확인할 수 있습니다.

    • gRPC 호출에 대한 추가 네트워크 홉이 추가됩니다.
    • 대기 시간이 증가하고 추가 리소스를 소비합니다.
    • 프록시 서버를 올바르게 설치하고 구성해야 합니다.
  • 클라이언트 쪽 부하 분산: gRPC 클라이언트는 gRPC 호출이 시작될 때 부하 부산을 결정합니다. gRPC 호출은 최종 엔드포인트로 바로 전송됩니다. 클라이언트 쪽 부하 분산을 사용하는 경우 다음을 확인합니다.

    • 클라이언트는 가용 엔드포인트를 확인하고 부하 분산 결정을 내릴 책임이 있습니다.
    • 추가 클라이언트 구성이 필요합니다.
    • 부하가 분산된 고성능 gRPC 호출은 프록시가 필요 없습니다.

추가 리소스