Compartir a través de


Equilibrio de carga del lado cliente en gRPC

Por James Newton-King

El equilibrio de carga del lado cliente es una característica que permite a los clientes gRPC distribuir la carga de forma óptima entre los servidores disponibles. En este artículo se describe cómo configurar el equilibrio de carga del lado cliente para crear aplicaciones gRPC escalables y de alto rendimiento en .NET.

El equilibrio de carga del lado cliente requiere lo siguiente:

Configuración del equilibrio de carga del lado cliente gRPC

El equilibrio de carga del lado cliente se configura cuando se crea un canal. Los dos componentes que se deben tener en cuenta al usar el equilibrio de carga son los siguientes:

  • El solucionador, que resuelve las direcciones del canal. Los solucionadores admiten la obtención de direcciones de un origen externo. Esto también se conoce como detección de servicios.
  • El equilibrador de carga, que crea conexiones y elige la dirección que usará una llamada a gRPC.

Las implementaciones integradas de solucionadores y equilibradores de carga se incluyen en Grpc.Net.Client. El equilibrio de carga también se puede extender escribiendo solucionadores personalizados y equilibradores de carga.

Las direcciones, las conexiones y otro estado del equilibrio de carga se almacenan en una instancia de GrpcChannel. Se debe reutilizar un canal al realizar llamadas de gRPC para que el equilibrio de carga funcione correctamente.

Nota

Algunas configuraciones de equilibrio de carga usan la inserción de dependencias (DI). Las aplicaciones que no usan DI pueden crear una instancia de ServiceCollection.

Si una aplicación ya tiene configuración de inserción de dependencias, como un sitio web de ASP.NET Core, los tipos deben registrarse con la instancia de inserción de dependencia existente. GrpcChannelOptions.ServiceProvider se configura obteniendo IServiceProvider de la DI.

Configuración del solucionador

El solucionador se configura mediante la dirección con la que se crea un canal. El esquema URI de la dirección especifica el solucionador.

Scheme Tipo Descripción
dns DnsResolverFactory Resuelve las direcciones consultando al nombre de host los registros de las direcciones DNS.
static StaticResolverFactory Resuelve las direcciones que ha especificado la aplicación. Se recomienda si una aplicación ya conoce las direcciones a las que llama.

Un canal no llama directamente a un URI que coincida con un solucionador. En su lugar, se crea un solucionador coincidente y se usa para resolver las direcciones.

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

  • El esquema dns se asigna a DnsResolverFactory. Se crea una nueva instancia de un solucionador DNS para el canal.
  • El solucionador hace una consulta de DNS para my-example-host y obtiene dos resultados: 127.0.0.100 y 127.0.0.101.
  • El equilibrador de carga utiliza 127.0.0.100:80 y 127.0.0.101:80 para crear conexiones y realizar llamadas de gRPC.

DnsResolverFactory

DnsResolverFactory crea un solucionador diseñado para obtener direcciones de un origen externo. La resolución DNS se usa normalmente para equilibrar la carga en las instancias de pod que tienen servicios sin periféricos de 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" });

El código anterior:

  • Configura el canal creado con la dirección dns:///my-example-host.
    • El esquema dns se asigna a DnsResolverFactory.
    • my-example-host es el nombre de host que se va a resolver.
    • No se especifica ningún puerto en la dirección, por lo que las llamadas gRPC se envían al puerto 80. Este es el puerto predeterminado para los canales no seguros. Opcionalmente, se puede especificar un puerto después del nombre de host. Por ejemplo, dns:///my-example-host:8080 configura las llamadas gRPC que se enviarán al puerto 8080.
  • No especifica un equilibrador de carga. El valor predeterminado del canal es elegir el primer equilibrador de carga.
  • Inicia la llamada de gRPC SayHello:
    • El solucionador DNS obtiene las direcciones del nombre de host my-example-host.
    • Elija el primer equilibrador de carga que intente conectarse a una de las direcciones resueltas.
    • La llamada se envía a la primera dirección a la que se conecta correctamente el canal.
Almacenamiento en caché de direcciones DNS

El rendimiento es importante al equilibrar la carga. La latencia de resolución de direcciones se elimina de las llamadas de gRPC almacenando en caché las direcciones. Se invocará un solucionador al realizar la primera llamada a gRPC, y las llamadas posteriores usarán la memoria caché.

Las direcciones se actualizan automáticamente si se interrumpe una conexión. La actualización es importante en escenarios donde las direcciones cambian en el entorno de ejecución. Por ejemplo, en Kubernetes, un pod reiniciado hace que el solucionador de DNS se actualice y obtenga la nueva dirección del pod.

De forma predeterminada, se actualiza un solucionador DNS si se interrumpe una conexión. El solucionador DNS también puede actualizarse opcionalmente en un intervalo periódico. Esto puede ser útil para detectar rápidamente nuevas instancias de pod.

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

El código anterior crea un DnsResolverFactory con un intervalo de actualización y lo registra con inserción de dependencias. Para más información sobre el uso de un solucionador configurado de forma personalizada, consulte Configuración de resoluciones personalizadas y equilibradores de carga.

StaticResolverFactory

StaticResolverFactory proporciona un solucionador estático. Este solucionador:

  • No llama a un origen externo. En su lugar, la aplicación cliente configura las direcciones.
  • Está diseñado para situaciones en las que una aplicación ya conoce las direcciones a las que llama.
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);

El código anterior:

  • Crea una interfaz StaticResolverFactory. La fábrica ya conoce dos direcciones: localhost:80 y localhost:81.
  • Registra la fábrica con inserción de dependencias (DI).
  • Configura el canal creado con:
    • La dirección static:///my-example-host. El esquema static se asigna a un solucionador estático.
    • Establece GrpcChannelOptions.ServiceProvider con el proveedor de servicios de DI.

En este ejemplo se crea un nuevo ServiceCollection para la DI. Supongamos que una aplicación ya tiene la DI configurada, como un sitio web de ASP.NET Core. En ese caso, los tipos se deben registrar con la instancia de DI existente. GrpcChannelOptions.ServiceProvider se configura obteniendo IServiceProvider de la DI.

Configuración de un equilibrador de carga

Se especifica un equilibrador de carga en un service config mediante la colección ServiceConfig.LoadBalancingConfigs. Dos equilibradores de carga se integran y se asignan a los nombres de configuración del equilibrador de carga:

Nombre Tipo Descripción
pick_first PickFirstLoadBalancerFactory Intenta conectarse a direcciones hasta que se realiza correctamente una conexión. Todas las llamadas de gRPC se realizan a la primera conexión correcta.
round_robin RoundRobinLoadBalancerFactory Intenta conectarse a todas las direcciones. Las llamadas de gRPC se distribuyen entre todas las conexiones correctas utilizando una lógica round robin.

service config es una abreviatura de configuración de servicio y se representa por el tipo ServiceConfig. Hay un par de maneras en que un canal puede obtener un service config con un equilibrador de carga configurado:

  • Una aplicación puede especificar un service config cuando se crea un canal con GrpcChannelOptions.ServiceConfig.
  • De manera alternativa, un solucionador puede resolver un service config para un canal. Esta característica permite que un origen externo especifique cómo sus llamadores deben realizar el equilibrio de carga. El hecho de que un solucionador admita la resolución de un service config depende de la implementación del solucionador. Deshabilite esta característica con GrpcChannelOptions.DisableResolverServiceConfig.
  • Si no se proporciona service config, o el service config no tiene un equilibrador de carga configurado, el canal predeterminado es 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" });

El código anterior:

  • Especifica RoundRobinLoadBalancerFactory en el service config.
  • Inicia la llamada de gRPC SayHello:
    • DnsResolverFactory crea un solucionador que obtiene las direcciones del nombre de host my-example-host.
    • El equilibrador de carga round robin intenta conectarse a todas las direcciones resueltas.
    • Las llamadas de gRPC se distribuyen uniformemente mediante la lógica round robin.

Configuración de las credenciales del canal

Un canal debe saber si las llamadas gRPC se envían mediante la seguridad de transporte. http y https ya no forman parte de la dirección, el esquema ahora especifica un solucionador, por lo que Credentials debe configurarse en las opciones de canal al usar el equilibrio de carga.

  • ChannelCredentials.SecureSsl - Las llamadas de gRPC deben protegerse con Seguridad de la capa de transporte (TLS). Equivalente a una dirección https.
  • ChannelCredentials.Insecure - Las llamadas de gRPC no usan la seguridad de transporte. Equivalente a una dirección 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" });

Uso del equilibrio de carga con el generador de cliente gRPC

El generador de cliente gRPC se puede configurar para usar el equilibrio 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();

El código anterior:

  • Configura el cliente con una dirección de equilibrio de carga.
  • Especifica las credenciales del canal.
  • Registra los tipos de inserción de dependencias con el objeto IServiceCollectionde la aplicación.

Escritura de solucionadores y equilibradores de carga personalizados

El equilibrio de carga del lado cliente es extensible:

  • Implemente Resolver para crear un solucionador personalizado y resolver direcciones desde un nuevo origen de datos.
  • Implemente LoadBalancer para crear un equilibrador de carga personalizado con un nuevo comportamiento de equilibrio de carga.

Importante

Las API que se usan para extender el equilibrio de carga del lado cliente son experimentales. Pueden cambiar sin previo aviso.

Creación de un solucionador personalizado

Un solucionador:

  • Implementa Resolver y lo crea un ResolverFactory. Cree un solucionador personalizado implementando estos tipos.
  • Es responsable de resolver las direcciones que usa un equilibrador de carga.
  • Opcionalmente, puede proporcionar una configuración de servicio.
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);
    }
}

En el código anterior:

  • FileResolverFactory implementa ResolverFactory. Se asigna al esquema file y crea instancias de FileResolver.
  • FileResolver implementa PollingResolver. PollingResolver es un tipo base abstracto que facilita la implementación de un solucionador con lógica asincrónica invalidando ResolveAsync.
  • En: ResolveAsync
    • El URI del archivo se convierte en una ruta de acceso local. Por ejemplo, file:///c:/addresses.json se convierte en c:\addresses.json.
    • JSON se carga desde el disco y se convierte en una colección de direcciones.
    • Se llama al agente de escucha con los resultados para que el canal sepa que las direcciones están disponibles.

Creación de un equilibrador de carga personalizado

Un equilibrador de carga:

  • Implementa LoadBalancer, y lo crea un LoadBalancerFactory. Cree un equilibrador de carga y una fábrica personalizados implementando estos tipos.
  • Recibe direcciones de un solucionador y crea instancia de Subchannel.
  • Realiza un seguimiento del estado de la conexión y crea un SubchannelPicker. El canal usa internamente el selector para elegir direcciones al realizar llamadas de gRPC.

El SubchannelsLoadBalancer es:

  • Una clase base abstracta que implementa LoadBalancer.
  • Administra la creación de instancias de Subchannel a partir de las direcciones.
  • Facilita la implementación de una directiva de selección personalizada en una colección de subcanales.
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);
    }
}

En el código anterior:

  • RandomBalancerFactory implementa LoadBalancerFactory. Se asigna al nombre de directiva random y crea instancias de RandomBalancer.
  • RandomBalancer implementa SubchannelsLoadBalancer. Crea un RandomPicker que selecciona aleatoriamente un subcanal.

Configuración de solucionadores y equilibradores de carga personalizados

Los solucionadores y equilibradores de carga personalizados deben registrarse con inserción de dependencias (DI) cuando se usan. Hay dos opciones:

  • Si una aplicación ya usa la DI, como una aplicación web ASP.NET Core, se puede registrar con la configuración de DI existente. IServiceProvider se puede resolver desde la DI y pasarse al canal mediante GrpcChannelOptions.ServiceProvider.
  • Si una aplicación no usa la DI, cree lo siguiente:
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);

El código anterior:

  • Crea un ServiceCollection y registra nuevas implementaciones de equilibrador de carga y solucionador.
  • Crea un canal configurado para usar las nuevas implementaciones:
    • ServiceCollection se integra en IServiceProvider y se establece en GrpcChannelOptions.ServiceProvider.
    • La dirección del canal es file:///c:/data/addresses.json. El esquema file se asigna a FileResolverFactory.
    • El nombre del equilibrador de carga service config es random. Asigna a RandomLoadBalancerFactory.

¿Por qué es importante el equilibrio de carga?

HTTP/2 multiplexa varias llamadas en una sola conexión TCP. Si gRPC y HTTP/2 se usan con un equilibrador de carga de red (NLB), la conexión se reenvía a un servidor y todas las llamadas de gRPC se envían a ese servidor. Las demás instancias de servidor del NLB están inactivas.

Los equilibradores de carga de red son una solución común para el equilibrio de carga, ya que son rápidos y ligeros. Por ejemplo, Kubernetes usa de forma predeterminada un equilibrador de carga de red para equilibrar las conexiones entre las instancias de pod. Sin embargo, los equilibradores de carga de red no son eficaces para distribuir la carga cuando se usan con gRPC y HTTP/2.

¿Equilibrio de carga del lado cliente o proxy?

gRPC y HTTP/2 pueden equilibrar la carga de forma eficaz mediante un proxy del equilibrador de carga de la aplicación o el equilibrio de carga del lado cliente. Ambas opciones permiten que las llamadas de gRPC individuales se distribuyan entre los servidores disponibles. Decidir entre el equilibrio de carga del lado cliente y el proxy es una opción arquitectónica. Cada uno de las opciones tiene ventajas y desventajas.

  • Proxy: las llamadas de gRPC se envían al proxy, el proxy toma una decisión de equilibrio de carga y la llamada de gRPC se envía al punto de conexión final. El proxy es responsable de conocer los puntos de conexión. El uso de un proxy agrega:

    • Un salto de red adicional a las llamadas de gRPC.
    • Latencia y consume recursos adicionales.
    • El servidor proxy debe configurarse y establecerse correctamente.
  • Equilibrio de carga del lado cliente: el cliente gRPC toma una decisión de equilibrio de carga cuando se inicia una llamada a gRPC. La llamada a gRPC se envía directamente al punto de conexión final. Al usar el equilibrio de carga del lado cliente:

    • El cliente es responsable de conocer los puntos de conexión disponibles y tomar decisiones de equilibrio de carga.
    • Se requiere configuración de cliente adicional.
    • Las llamadas de gRPC de alto rendimiento y con equilibrio de carga eliminan la necesidad de un proxy.

Recursos adicionales