Partager via


Équilibrage de charge côté client gRPC

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 9 de cet article.

Par James Newton-King

L’équilibrage de charge côté client est une fonctionnalité qui permet aux clients gRPC de répartir la charge de manière optimale sur les serveurs disponibles. Cet article explique comment configurer l’équilibrage de charge côté client pour créer des applications gRPC évolutives et à hautes performances dans .NET.

L’équilibrage de charge côté client nécessite :

  • .NET 5 ou version ultérieure.
  • Grpc.Net.Client version 2.45.0 ou ultérieure.

Configurer l’équilibrage de charge côté client gRPC

L’équilibrage de charge côté client est configuré lors de la création d’un canal. Les deux composants à prendre en compte lors de l’utilisation de l’équilibrage de charge :

  • Le programme de résolution, qui résout les adresses du canal. Les programmes de résolution prennent en charge l’obtention d’adresses à partir d’une source externe. Cette fonction est également connue sous le nom de découverte de service.
  • L’équilibreur de charge, qui crée des connexions et choisit l’adresse qu’un appel gRPC utilisera.

Les implémentations intégrées de programmes de résolution et d’équilibreurs de charge sont incluses dans Grpc.Net.Client. L’équilibrage de charge peut également être étendu en écrivant des programmes de résolution et des équilibreurs de charge personnalisés.

Les adresses, les connexions et d’autres états d’équilibrage de charge sont stockés dans une instance GrpcChannel. Un canal doit être réutilisé lors de l’exécution d’appels gRPC pour que l’équilibrage de charge fonctionne correctement.

Notes

Certaines configurations d’équilibrage de charge utilisent l’injection de dépendances (DI). Les applications qui n’utilisent pas de di peuvent créer un ServiceCollection instance.

Si une application dispose déjà d’une configuration d’ID, comme un site web ASP.NET Core, les types doivent être inscrits auprès de l’instance d’ID existant. GrpcChannelOptions.ServiceProvider est configuré en obtenant un IServiceProvider à partir d’une DI.

Configurer le résolveur

Le programme de résolution est configuré à l’aide de l’adresse avec laquelle un canal est créé. Le schéma URI de l’adresse spécifie le programme de résolution.

Schéma Type Description
dns DnsResolverFactory Résout les adresses en interrogeant le nom d’hôte pour les enregistrements d’adresses DNS.
static StaticResolverFactory Résout les adresses spécifiées par l’application. Recommandé si une application connaît déjà les adresses qu’elle appelle.

Un canal n’appelle pas directement un URI qui correspond à un programme de résolution. Au lieu de cela, un programme de résolution correspondant est créé et utilisé pour résoudre les adresses.

Par exemple, en utilisant GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }) :

  • Le schéma dns est mappé à DnsResolverFactory. Une nouvelle instance d’un programme de résolution DNS est créée pour le canal.
  • Le programme de résolution effectue une requête DNS pour my-example-host et obtient deux résultats : 127.0.0.100 et 127.0.0.101.
  • L’équilibreur de charge utilise 127.0.0.100:80 et 127.0.0.101:80 pour créer des connexions et effectuer des appels gRPC.

DnsResolverFactory

Le DnsResolverFactory crée un programme de résolution conçu pour obtenir des adresses à partir d’une source externe. La résolution DNS est couramment utilisée pour équilibrer la charge sur les instances de pod qui ont des services sans affichage 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" });

Le code précédent :

  • Configure le canal créé avec l’adresse dns:///my-example-host.
    • Le schéma dns est mappé à DnsResolverFactory.
    • my-example-host est le nom d’hôte à résoudre.
    • Aucun port n’étant spécifié dans l’adresse, les appels gRPC sont envoyés au port 80. Il s’agit du port par défaut pour les canaux non sécurisés. Un port peut éventuellement être spécifié après le nom d’hôte. Par exemple, dns:///my-example-host:8080 configure les appels gRPC à envoyer au port 8080.
  • Ne spécifie pas d’équilibreur de charge. Par défaut, le canal est un équilibreur de charge de premier choix.
  • Démarre l’appel gRPC SayHello :
    • Le programme de résolution DNS obtient des adresses pour le nom d’hôte my-example-host.
    • L’équilibreur de charge de premier choix tente de se connecter à l’une des adresses résolues.
    • L’appel est envoyé à la première adresse à laquelle le canal se connecte correctement.
Mise en cache d’adresses DNS

Les performances sont importantes lors de l’équilibrage de charge. La latence de résolution des adresses est éliminée des appels gRPC par la mise en cache des adresses. Un programme de résolution est appelé lors du premier appel gRPC, et les appels suivants utilisent le cache.

Les adresses sont automatiquement actualisées si une connexion est interrompue. L’actualisation est importante dans les scénarios où les adresses changent au moment du runtime. Par exemple, dans Kubernetes, un pod redémarré déclenche le programme de résolution DNS pour l’actualisation et l’obtention de la nouvelle adresse du pod.

Par défaut, un programme de résolution DNS est actualisé si une connexion est interrompue. Le programme de résolution DNS peut également s’actualiser sur un intervalle périodique. Cela peut être utile pour détecter rapidement de nouvelles instances de pod.

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

Le code précédent crée un DnsResolverFactory avec un intervalle d’actualisation et l’enregistre avec l’injection de dépendances. Pour plus d’informations sur l’utilisation d’un programme de résolution configuré sur mesure, consultez Configurer des programmes de résolution et des équilibreurs de charge personnalisés.

StaticResolverFactory

Un programme de résolution statique est fourni par StaticResolverFactory. Ce programme de résolution :

  • N’appelle pas une source externe. Au lieu de cela, l’application cliente configure les adresses.
  • Est conçu pour les situations dans lesquelles une application connaît déjà les adresses qu’elle appelle.
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);

Le code précédent :

  • Crée un StaticResolverFactory. Cette fabrique connaît deux adresses : localhost:80 et localhost:81.
  • Enregistre la fabrique avec l’injection de dépendances (DI).
  • Configure le canal créé avec :
    • L’adresse static:///my-example-host. Le schéma static est mappé à un programme de résolution statique.
    • Définit GrpcChannelOptions.ServiceProvider avec le fournisseur de services DI.

Cet exemple crée un nouveau ServiceCollection pour la DI. Supposons qu’une application dispose déjà d’une configuration de DI, comme un site web ASP.NET Core. Dans ce cas, les types doivent être inscrits auprès de l’instance DI existante. GrpcChannelOptions.ServiceProvider est configuré en obtenant un IServiceProvider à partir d’une DI.

Configurer l'équilibreur de charge

Un équilibreur de charge est spécifié dans un service config à l’aide de la collection ServiceConfig.LoadBalancingConfigs. Deux équilibreurs de charge sont intégrés et mappés aux noms de configuration de l’équilibreur de charge :

Nom Type Description
pick_first PickFirstLoadBalancerFactory Tente de se connecter aux adresses jusqu’à ce qu’une connexion soit établie avec succès. Les appels gRPC sont tous effectués à la première connexion réussie.
round_robin RoundRobinLoadBalancerFactory Tente de se connecter à toutes les adresses. Les appels gRPC sont distribués entre toutes les connexions réussies à l’aide d’une logique round-robin.

service config est une abréviation de la configuration du service et est représenté par le type ServiceConfig. Il existe plusieurs façons pour un canal d’obtenir un service config avec un équilibreur de charge configuré :

  • Une application peut spécifier un service config lors de la création d’un canal à l’aide de GrpcChannelOptions.ServiceConfig.
  • Un programme de résolution peut également résoudre un service config pour un canal. Cette fonctionnalité permet à une source externe de spécifier la façon dont ses appelants doivent effectuer l’équilibrage de charge. Le fait qu’un programme de résolution prenne en charge la résolution d’un service config dépend de l’implémentation du programme de résolution. Désactivez cette fonctionnalité avec GrpcChannelOptions.DisableResolverServiceConfig.
  • Si aucun service config n’est fourni ou si le service config n’a pas d’équilibreur de charge configuré, le canal est défini par défaut sur 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" });

Le code précédent :

  • Spécifie un RoundRobinLoadBalancerFactory dans le service config.
  • Démarre l’appel gRPC SayHello :
    • DnsResolverFactory crée un programme de résolution qui obtient des adresses pour le nom d’hôte my-example-host.
    • L’équilibreur de charge round-robin tente de se connecter à toutes les adresses résolues.
    • Les appels gRPC sont distribués uniformément à l’aide de la logique round-robin.

Configurer les informations d’identification du canal

Un canal doit savoir si les appels gRPC sont envoyés à l’aide de la sécurité du transport. http et https ne font plus partie de l’adresse, le schéma spécifie désormais un programme de résolution. Credentials doit donc être configuré sur les options de canal lors de l’utilisation de l’équilibrage de charge.

  • ChannelCredentials.SecureSsl : les appels gRPC sont sécurisés avec le protocole TLS. Équivalent à une adresse https.
  • ChannelCredentials.Insecure : les appels gRPC n’utilisent pas la sécurité de transport. Équivalent à une adresse 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" });

Utiliser l’équilibrage de charge avec la fabrique de client gRPC

La fabrique de client gRPC peut être configurée pour utiliser l’équilibrage de charge :

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

Le code précédent :

  • Configure le client avec une adresse d’équilibrage de charge.
  • Spécifie les informations d’identification du canal.
  • Inscrit les types d’ID auprès de l’application IServiceCollection.

Écrire des programmes de résolution personnalisés et des équilibreurs de charge

L’équilibrage de charge côté client est extensible :

  • Implémentez Resolver pour créer un programme de résolution personnalisé et résoudre les adresses à partir d’une nouvelle source de données.
  • Implémentez LoadBalancer pour créer un équilibreur de charge personnalisé avec un nouveau comportement d’équilibrage de charge.

Important

Les API utilisées pour étendre l’équilibrage de charge côté client sont expérimentales. Elles peuvent changer sans préavis.

Créer un programme de résolution personnalisé

Un programme de résolution :

  • Implémente Resolver et est créé par un ResolverFactory. Créez un programme de résolution personnalisé en implémentant ces types.
  • Est responsable de la résolution des adresses qu’un équilibreur de charge utilise.
  • Peut éventuellement fournir une configuration de service.
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);
    }
}

Dans le code précédent :

  • L'objet FileResolverFactory implémente l'objet ResolverFactory. Il mappe au schéma file et crée des instances FileResolver.
  • L'objet FileResolver implémente l'objet PollingResolver. PollingResolver est un type de base abstrait qui facilite l’implémentation d’un programme de résolution avec une logique asynchrone en remplaçant ResolveAsync.
  • Dans : ResolveAsync
    • L’URI de fichier est converti en chemin d’accès local. Par exemple, file:///c:/addresses.json devient c:\addresses.json.
    • JSON est chargé à partir du disque et converti en une collection d’adresses.
    • L’écouteur est appelé avec des résultats pour informer le canal que les adresses sont disponibles.

Créer un équilibreur de charge personnalisé

Un équilibreur de charge :

  • Implémente LoadBalancer et est créé par un LoadBalancerFactory. Créez un équilibreur de charge et une fabrique personnalisés en implémentant ces types.
  • Reçoit des adresses à partir d’un programme de résolution et crée des instances Subchannel.
  • Suit l’état de la connexion et crée un SubchannelPicker. Le canal utilise le sélecteur en interne pour sélectionner des adresses lors des appels gRPC.

Le SubchannelsLoadBalancer est :

  • Une classe de base abstraite qui implémente LoadBalancer.
  • Gère la création d’instances Subchannel à partir d’adresses.
  • Facilite l’implémentation d’une stratégie de sélection personnalisée sur une collection de sous-canaux.
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);
    }
}

Dans le code précédent :

  • L'objet RandomBalancerFactory implémente l'objet LoadBalancerFactory. Il mappe au nom de la stratégie random et crée des instances RandomBalancer.
  • L'objet RandomBalancer implémente l'objet SubchannelsLoadBalancer. Il crée un RandomPicker qui sélectionne aléatoirement un sous-canal.

Configurer des programmes de résolution personnalisés et des équilibreurs de charge

Les programmes de résolution personnalisés et les équilibreurs de charge doivent être enregistrés avec l’injection de dépendances (DI) lorsqu’ils sont utilisés. Deux cas de figure peuvent se présenter :

  • Si une application utilise déjà la DI, par exemple une application web ASP.NET Core, elle peut être enregistrée avec la configuration de DI existante. Un IServiceProvider peut être résolu à partir de la DI et passé au canal à l’aide de GrpcChannelOptions.ServiceProvider.
  • Si une application n’utilise pas la DI, créez :
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);

Le code précédent :

  • Crée un ServiceCollection et enregistre de nouvelles implémentations de programme de résolution et d’équilibreur de charge.
  • Crée un canal configuré pour utiliser les nouvelles implémentations :
    • ServiceCollection est intégré à un IServiceProvider et défini sur GrpcChannelOptions.ServiceProvider.
    • L’adresse du canal est file:///c:/data/addresses.json. Le schéma file est mappé à FileResolverFactory.
    • Le nom de l'équilibreur de charge service config est random. Mappe à RandomLoadBalancerFactory.

Pourquoi l’équilibrage de charge est important

HTTP/2 multiplexe plusieurs appels sur une seule connexion TCP. Si gRPC et HTTP/2 sont utilisés avec un équilibreur de la charge réseau (NLB), la connexion est transférée à un serveur et tous les appels gRPC sont envoyés à ce serveur. Les autres instances de serveur sur la NLB sont inactives.

Les équilibreurs de la charge réseau sont une solution courante pour l’équilibrage de charge, car ils sont rapides et légers. Par exemple, Kubernetes utilise par défaut un équilibreur de la charge réseau pour équilibrer les connexions entre les instances de pod. Toutefois, les équilibreurs de la charge réseau ne sont pas efficaces pour distribuer la charge lorsqu’ils sont utilisés avec gRPC et HTTP/2.

Équilibrage de charge proxy ou côté client ?

gRPC et HTTP/2 peuvent être équilibrés efficacement à l’aide d’un proxy d’équilibreur de charge d’application ou d’un équilibrage de la charge côté client. Ces deux options permettent de distribuer des appels gRPC individuels sur les serveurs disponibles. Choisir entre le proxy et l’équilibrage de la charge côté client est un choix architectural. Il y a des avantages et des inconvénients pour chacun.

  • Proxy : les appels gRPC sont envoyés au proxy, le proxy prend une décision d’équilibrage de charge et l’appel gRPC est envoyé au point de terminaison final. Le proxy est chargé de connaître les points de terminaison. L’utilisation d’un proxy ajoute :

    • Un tronçon réseau supplémentaire vers les appels gRPC.
    • De la latence et consomme davantage de ressources.
    • Le serveur proxy doit être configuré correctement.
  • Équilibrage de la charge côté client : le client gRPC prend une décision d’équilibrage de charge lorsqu’un appel gRPC est démarré. L’appel gRPC est envoyé directement au point de terminaison final. Lors de l’utilisation de l’équilibrage de la charge côté client :

    • Le client est chargé de connaître les points de terminaison disponibles et de prendre des décisions d’équilibrage de charge.
    • Une configuration client supplémentaire est requise.
    • Les appels gRPC hautes performances et à charge équilibrée éliminent le besoin d’un proxy.

Ressources supplémentaires