Compartilhar via


Balanceamento de carga do lado do cliente do gRPC

Por James Newton-King

O balanceamento de carga do lado do cliente é um recurso que permite que os clientes do gRPC distribuam a carga de forma ideal entre os servidores disponíveis. Esse artigo discute como configurar o balanceamento de carga do lado do cliente para criar aplicativos do gRPC escalonáveis e de alto desempenho no .NET.

O balanceamento de carga do lado do cliente exige:

Configurar o balanceamento de carga do lado do cliente do gRPC

O balanceamento de carga do lado do cliente é configurado quando um canal é criado. Os dois componentes a serem considerados ao usar o balanceamento de carga:

  • O resolvedor, que resolve os endereços do canal. Os resolvedores dão suporte à obtenção de endereços de uma fonte externa. Isso também é conhecido como descoberta de serviço.
  • O balanceador de carga, que cria conexões e escolhe o endereço que uma chamada do gRPC usará.

Implementações internas de resolvedores e balanceadores de carga estão incluídas no Grpc.Net.Client. O balanceamento de carga também pode ser estendido gravando resolvedores personalizados e balanceadores de carga.

Endereços, conexões e outro estado de balanceamento de carga são armazenados em uma instância de GrpcChannel. Um canal deve ser reutilizado ao fazer chamadas do gRPC para que o balanceamento de carga funcione corretamente.

Observação

Algumas configurações de balanceamento de carga usam injeção de dependência (DI). Aplicativos que não usam DI podem criar uma instância ServiceCollection.

Se um aplicativo já tiver configuração de DI, como um site ASP.NET Core, os tipos deverão ser registrados com a instância de DI existente. GrpcChannelOptions.ServiceProvider é configurado obtendo um IServiceProvider da ID.

Configurar resolvedor

O resolvedor é configurado usando o endereço com o qual um canal é criado. O esquema de URI do endereço especifica o resolvedor.

Esquema Tipo Descrição
dns DnsResolverFactory Resolve endereços consultando o nome do host para registros de endereço do DNS.
static StaticResolverFactory Resolve os endereços especificados pelo aplicativo. Recomendado se um aplicativo já souber os endereços que ele chama.

Um canal não chama diretamente um URI que corresponde a um resolvedor. Em vez disso, um resolvedor correspondente é criado e usado para resolve os endereços.

Por exemplo, usando GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }):

  • O esquema dns é mapeado para DnsResolverFactory. Uma nova instância de um resolvedor do DNS é criada para o canal.
  • O resolvedor faz uma consulta ao DNS de my-example-host e obtém dois resultados: 127.0.0.100 e 127.0.0.101.
  • O balanceador de carga usa 127.0.0.100:80 e 127.0.0.101:80 para criar conexões e fazer chamadas do gRPC.

DnsResolverFactory

O DnsResolverFactory cria um resolvedor projetado para obter endereços de uma fonte externa. A resolução do DNS é comumente usada para balancear a carga em instâncias de pod que têm serviços sem cabeça do Kubernetes.

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" });

O código anterior:

  • Configura o canal criado com o endereço dns:///my-example-host.
    • O esquema dns é mapeado para DnsResolverFactory.
    • my-example-hosté o nome do host a ser resolvido.
    • Nenhuma porta é especificada no endereço, portanto, as chamadas gRPC são enviadas para a porta 80. Essa é a porta padrão para canais não seguros. Opcionalmente, uma porta pode ser especificada após o nome do host. Por exemplo, dns:///my-example-host:8080 configura chamadas gRPC a serem enviadas para a porta 8080.
  • Não especifica um balanceador de carga. O canal usa como padrão um balanceador de carga de primeira opção.
  • Inicia a chamada SayHello do gRPC:
    • O resolvedor do DNS obtém endereços para o nome do host de my-example-host.
    • O balanceador de carga de primeira opção tenta se conectar a um dos endereços resolvidos.
    • A chamada é enviada para o primeiro endereço ao qual o canal se conecta com êxito.
Cache de endereço do DNS

O desempenho é importante durante o balanceamento de carga. A latência da resolução de endereços é eliminada das chamadas do gRPC armazenando em cache os endereços. Um resolvedor será invocado ao fazer a primeira chamada do gRPC e chamadas subsequentes usam o cache.

Os endereços serão atualizados automaticamente se uma conexão for interrompida. A atualização é importante em cenários em que os endereços mudam em runtime. Por exemplo, no Kubernetes, um pod reiniciado dispara o resolvedor do DNS para atualizar e obter o novo endereço do pod.

Por padrão, um resolvedor do DNS será atualizado se uma conexão for interrompida. O resolvedor do DNS também pode, opcionalmente, atualizar-se em um intervalo periódico. Isso pode ser útil para detectar rapidamente novas instâncias de pod.

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

O código anterior cria um DnsResolverFactory com um intervalo de atualização e o registra com injeção de dependência. Para obter mais informações sobre como usar um resolvedor configurado personalizado, consulte Configurar resolvedores personalizados e balanceadores de carga.

StaticResolverFactory

Um resolvedor estático é fornecido por StaticResolverFactory. Esse resolvedor:

  • Não chama uma fonte externa. Em vez disso, o aplicativo cliente configura os endereços.
  • Foi projetado para situações em que um aplicativo já sabe os endereços que chama.
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);

O código anterior:

  • Cria um StaticResolverFactory. Esta fábrica sabe sobre dois endereços: localhost:80 e localhost:81.
  • Registra a fábrica com injeção de dependência (ID).
  • Configura o canal criado com:
    • O endereço static:///my-example-host. O esquema static é mapeado para um resolvedor estático.
    • Define GrpcChannelOptions.ServiceProvider com o provedor de serviços de ID.

Esse exemplo cria um novo ServiceCollection para ID. Suponha que um aplicativo já tenha configuração de ID, como um site do ASP.NET Core. Nesse caso, os tipos devem ser registrados com a instância ID existente. GrpcChannelOptions.ServiceProvider é configurado obtendo um IServiceProvider da ID.

Configurar o balanceador de carga

Um balanceador de carga é especificado em um service config usando a coleção ServiceConfig.LoadBalancingConfigs. Dois balanceadores de carga são internos e são mapeados para nomes de configuração do balanceador de carga:

Nome Digitar Descrição
pick_first PickFirstLoadBalancerFactory Tenta se conectar a endereços até que uma conexão seja feita com êxito. As chamadas do gRPC são todas feitas para a primeira conexão bem-sucedida.
round_robin RoundRobinLoadBalancerFactory Tenta se conectar a todos os endereços. As chamadas do gRPC são distribuídas entre todas as conexões com êxito usando a lógica round robin.

service config é uma abreviação da configuração de serviço e é representada pelo tipo ServiceConfig. Há algumas maneiras pelas quais um canal pode obter um service config com um balanceador de carga configurado:

  • Um aplicativo pode especificar um service config quando um canal é criado usando GrpcChannelOptions.ServiceConfig.
  • Como alternativa, um resolvedor pode resolver um service config para um canal. Esse recurso permite que uma fonte externa especifique como seus chamadores devem executar o balanceamento de carga. Se um resolvedor dá suporte à resolução de um service config for depende da implementação do resolvedor. Desabilite esse recurso com GrpcChannelOptions.DisableResolverServiceConfig.
  • Se nenhum service config for fornecido, ou o service config não tiver um balanceador de carga configurado, o canal usará como padrão 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" });

O código anterior:

  • Especifica um RoundRobinLoadBalancerFactory no service config.
  • Inicia a chamada SayHello do gRPC:
    • DnsResolverFactory cria um resolvedor que obtém endereços para o nome do host my-example-host.
    • O balanceador de carga round-robin tenta se conectar a todos os endereços resolvidos.
    • As chamadas do gRPC são distribuídas uniformemente usando a lógica round-robin.

Configurar credenciais do canal

Um canal deve saber se as chamadas do gRPC são enviadas usando segurança de transporte. http e https não fazem mais parte do endereço; o esquema agora especifica um resolvedor, portanto Credentials deve ser configurado nas opções de canal ao usar o balanceamento de carga.

  • ChannelCredentials.SecureSsl - As chamadas do gRPC são protegidas com armazenamento local de thread (TLS, na sigla em inglês). Equivalente a um endereço https.
  • ChannelCredentials.Insecure - As chamadas do gRPC não usam segurança do transporte. Equivalente a um endereço 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" });

Usar balanceamento de carga com a fábrica de clientes gRPC

A fábrica de clientes gRPC pode ser configurada para usar o balanceamento de carga:

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();

O código anterior:

  • Configura o cliente com um endereço de balanceamento de carga.
  • Especifica credenciais de canal.
  • Registra tipos de DI com o IServiceCollection do aplicativo.

Gravar resolvedores personalizados e balanceadores de carga

O balanceamento de carga do lado do cliente é extensível:

  • Implemente Resolver para criar um resolvedor personalizado e resolva endereços de uma nova fonte de dados.
  • Implemente LoadBalancer para criar um balanceador de carga personalizado com novo comportamento de balanceamento de carga.

Importante

As APIs usadas para estender o balanceamento de carga do lado do cliente são experimentais. Elas podem ser alteradas sem aviso prévio.

Criar um resolvedor personalizado

Um resolvedor:

  • Implementa Resolver e é criado por um ResolverFactory. Crie um resolvedor personalizado implementando esses tipos.
  • É responsável por resolver os endereços que um balanceador de carga usa.
  • Como opção, pode fornecer uma configuração de serviço.
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);
    }
}

No código anterior:

  • FileResolverFactory implementa ResolverFactory. Ele mapeia para o esquema file e cria instâncias de FileResolver.
  • FileResolver implementa PollingResolver. PollingResolver é um tipo base abstrato que facilita a implementação de um resolvedor com lógica assíncrona substituindo ResolveAsync.
  • Em: ResolveAsync
    • O URI do arquivo é convertido em um caminho local. Por exemplo, file:///c:/addresses.json torna-se c:\addresses.json.
    • JSON é carregado do disco e convertido em uma coleção de endereços.
    • O ouvinte é chamado com resultados para informar ao canal que os endereços estão disponíveis.

Criar um balanceador de carga personalizado

Um balanceador de carga:

  • Implementa LoadBalancer e é criado por um LoadBalancerFactory. Crie um balanceador de carga personalizado e uma fábrica implementando esses tipos.
  • São dados endereços de um resolvedor e cria instâncias de Subchannel.
  • Rastreia o estado sobre a conexão e cria um SubchannelPicker. O canal usa internamente o seletor para escolher endereços ao fazer chamadas do gRPC.

O SubchannelsLoadBalancer é:

  • Uma classe base abstrata que implementa o LoadBalancer.
  • Gerencia a criação de instâncias de Subchannel dos endereços.
  • Facilita a implementação de uma política de seleção personalizada em uma coleção de subcanais.
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);
    }
}

No código anterior:

  • RandomBalancerFactory implementa LoadBalancerFactory. Mapeia para o nome da política random e cria instâncias de RandomBalancer.
  • RandomBalancer implementa SubchannelsLoadBalancer. Cria um RandomPicker que escolhe aleatoriamente um subcanal.

Configurar resolvedores personalizados e balanceadores de carga

Resolvedores personalizados e balanceadores de carga precisam ser registrados com injeção de dependência (ID) quando são usados. Há duas opções:

  • Se um aplicativo já estiver usando ID, como um aplicativo Web ASP.NET Core, ele poderá ser registrado com a configuração de ID existente. Um IServiceProvider pode ser resolvido a partir da ID e aprovado para o canal usando GrpcChannelOptions.ServiceProvider.
  • Se um aplicativo não estiver usando ID, crie:
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);

O código anterior:

  • Cria um ServiceCollection e registra novas implementações de resolvedor e balanceador de carga.
  • Cria um canal configurado para usar as novas implementações:
    • ServiceCollection é integrado a um IServiceProvider e definido como GrpcChannelOptions.ServiceProvider.
    • O endereço do canal é file:///c:/data/addresses.json. O esquema file é mapeado para FileResolverFactory.
    • O nome do balanceador de carga service config é random. Mapeia para RandomLoadBalancerFactory.

Por que o balanceamento de carga é importante

O HTTP/2 multiplexa várias chamadas em uma única conexão TCP. Se gRPC e HTTP/2 forem usados com um balanceador de carga de rede (NLB, na sigla em inglês), a conexão será encaminhada para um servidor e todas as chamadas do gRPC serão enviadas para esse servidor. As outras instâncias de servidor no NLB ficam ociosas.

Os balanceadores de carga de rede são uma solução comum para balanceamento de carga porque são rápidos e leves. Por exemplo, o Kubernetes, por padrão, usa um balanceador de carga de rede para equilibrar conexões entre instâncias de pod. No entanto, os balanceadores de carga de rede não são eficazes na distribuição de carga quando usados com gRPC e HTTP/2.

Balanceamento de carga do lado do cliente ou de proxy?

gRPC e HTTP/2 podem ser efetivamente balanceados por carga usando um proxy do balanceador de carga do aplicativo ou um balanceamento de carga do lado do cliente. Ambas as opções permitem que chamadas do gRPC individuais sejam distribuídas entre servidores disponíveis. Decidir entre o balanceamento de carga de proxy e o balanceamento de carga do lado do cliente é uma opção arquitetônica. Há prós e contras para cada um.

  • Proxy: as chamadas do gRPC são enviadas para o proxy; o proxy toma uma decisão de balanceamento de carga e a chamada do gRPC é enviada para o ponto de extremidade final. O proxy é responsável por saber sobre pontos de extremidade. O uso de um proxy adiciona:

    • Um salto de rede adicional para chamadas do gRPC.
    • Latência e consome recursos adicionais.
    • O servidor proxy deve ser configurado e configurado corretamente.
  • Balanceamento de carga do lado do cliente: o cliente gRPC toma uma decisão de balanceamento de carga quando uma chamada do gRPC é iniciada. A chamada do gRPC é enviada diretamente para o ponto de extremidade final. Quando usar o balanceamento de carga do lado do cliente:

    • O cliente é responsável por saber sobre os pontos de extremidade disponíveis e tomar decisões de balanceamento de carga.
    • Configuração adicional do cliente é necessária.
    • Chamadas do gRPC com balanceamento de carga de alto desempenho eliminam a necessidade de um proxy.

Recursos adicionais