gRPC-taakverdeling aan clientzijde

Opmerking

Dit is niet de nieuwste versie van dit artikel. Zie de .NET 10-versie van dit artikel voor de huidige release.

Waarschuwing

Deze versie van ASP.NET Core wordt niet meer ondersteund. Zie het .NET- en .NET Core-ondersteuningsbeleid voor meer informatie. Zie de .NET 10-versie van dit artikel voor de huidige release.

Door James Newton-King

Taakverdeling aan de clientzijde is een functie waarmee gRPC-clients de belasting optimaal kunnen verdelen over beschikbare servers. In dit artikel wordt beschreven hoe u taakverdeling aan de clientzijde configureert om schaalbare, krachtige gRPC-apps te maken in .NET.

Taakverdeling aan de clientzijde vereist:

GRPC-taakverdeling aan clientzijde configureren

Taakverdeling aan de clientzijde wordt geconfigureerd wanneer een kanaal wordt gemaakt. De twee onderdelen waarmee u rekening moet houden bij het gebruik van taakverdeling:

  • De resolver, die de adressen voor het kanaal oplost. Resolvers ondersteunen het ophalen van adressen van een externe bron. Dit wordt ook wel het ontdekken van services genoemd.
  • De load balancer, die verbindingen maakt en het adres kiest dat door een gRPC-aanroep wordt gebruikt.

Ingebouwde implementaties van resolvers en load balancers zijn opgenomen in Grpc.Net.Client. Taakverdeling kan ook worden uitgebreid door aangepaste resolvers en load balancers te schrijven.

Adressen, verbindingen en andere taakverdelingsstatus worden opgeslagen in een GrpcChannel exemplaar. Een kanaal moet opnieuw worden gebruikt bij het maken van gRPC-aanroepen om taakverdeling correct te laten werken.

Opmerking

Sommige taakverdelingsconfiguratie maakt gebruik van afhankelijkheidsinjectie (DI). Apps die DI niet gebruiken, kunnen een ServiceCollection exemplaar maken.

Als een app al een DI-installatie heeft, zoals een ASP.NET Core-website, moeten typen worden geregistreerd bij het bestaande DI-exemplaar. GrpcChannelOptions.ServiceProvider is geconfigureerd door een IServiceProvider van de DI op te halen.

Resolver configureren

De resolver wordt geconfigureerd met het adres waarmee een kanaal wordt gemaakt. Het URI-schema van het adres specificeert de resolver.

Plan Typologie Description
dns DnsResolverFactory Hiermee worden adressen omgezet door een query uit te voeren op de hostnaam voor DNS-adresrecords.
static StaticResolverFactory Hiermee worden adressen opgelost die door de app zijn opgegeven. Aanbevolen als een app al de adressen kent die worden aanroepen.

Een kanaal roept niet rechtstreeks een URI aan die overeenkomt met een resolver. In plaats daarvan wordt een overeenkomende resolver gemaakt en gebruikt om de adressen op te lossen.

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

  • Het dns schema komt overeen met DnsResolverFactory. Er wordt een nieuw exemplaar van een DNS-resolver gemaakt voor het kanaal.
  • De resolver maakt een DNS-query voor my-example-host en haalt twee resultaten op: 127.0.0.100 en 127.0.0.101.
  • De load balancer maakt gebruik van 127.0.0.100:80 en 127.0.0.101:80 om verbindingen te creëren en gRPC-aanroepen te doen.

DnsResolverFactory

Een resolver wordt gecreëerd die is ontworpen om adressen uit een externe bron op te halen. DNS-omzetting wordt vaak gebruikt om belasting te verdelen over pod-exemplaren met een Kubernetes headless services.

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

De voorgaande code:

  • Hiermee configureert u het gemaakte kanaal met het adres dns:///my-example-host.
    • Het dns schema komt overeen met DnsResolverFactory.
    • my-example-host is de hostnaam die moet worden opgelost.
    • Er is geen poort opgegeven in het adres, dus gRPC-aanroepen worden verzonden naar poort 80. Dit is de standaardpoort voor onbeveiligde kanalen. Een poort kan eventueel worden opgegeven na de hostnaam. Configureert bijvoorbeeld dns:///my-example-host:8080 gRPC-aanroepen die moeten worden verzonden naar poort 8080.
  • Geeft geen load balancer op. Het kanaal wordt standaard ingesteld op een pick first load balancer.
  • Start de gRPC-aanroep SayHello:
    • DNS-resolver haalt adressen op voor de hostnaam my-example-host.
    • Kies de eerste load balancer die probeert verbinding te maken met een van de opgeloste adressen.
    • De aanroep wordt verzonden naar het eerste adres waarnaar het kanaal verbinding heeft gemaakt.
DNS-adres-caching

Prestaties zijn belangrijk bij taakverdeling. De latentie van het oplossen van adressen wordt verwijderd uit gRPC-aanroepen door de adressen in de cache te plaatsen. Een resolver wordt aangeroepen bij het maken van de eerste gRPC-aanroep en volgende aanroepen maken gebruik van de cache.

Adressen worden automatisch vernieuwd als een verbinding wordt onderbroken. Vernieuwen is belangrijk in scenario's waarin adressen tijdens runtime veranderen. In Kubernetes activeert een opnieuw gestarte pod bijvoorbeeld de DNS-resolver om te vernieuwen en het nieuwe adres van de pod op te halen.

Standaard wordt een DNS-resolver vernieuwd als een verbinding wordt onderbroken. De DNS-resolver kan zichzelf ook optioneel vernieuwen met een periodiek interval. Dit kan handig zijn voor het snel detecteren van nieuwe pod-exemplaren.

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

De voorgaande code maakt een DnsResolverFactory aan met een vernieuwingsinterval en registreert deze met behulp van afhankelijkheidsinjectie. Zie Aangepaste resolvers en load balancers configureren voor meer informatie over het gebruik van een op maat geconfigureerde resolver.

StaticResolverFactory

Een statische resolver wordt geleverd door StaticResolverFactory. Deze resolver:

  • Hiermee wordt geen externe bron aangeroepen. In plaats daarvan configureert de client-app de adressen.
  • Is ontworpen voor situaties waarin een app al weet wat de adressen zijn die de app aanroept.
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);

De voorgaande code:

  • Maak een StaticResolverFactory. Deze fabriek kent twee adressen: localhost:80 en localhost:81.
  • Registreert de fabriek met afhankelijkheidsinjectie (DI).
  • Hiermee configureert u het gemaakte kanaal met:
    • Het adres static:///my-example-host. Het static schema wordt toegewezen aan een statische resolver.
    • Stel GrpcChannelOptions.ServiceProvider in met de DI-serviceprovider.

In dit voorbeeld wordt een nieuwe ServiceCollection voor DI gemaakt. Stel dat een app al EEN DI-installatie heeft, zoals een ASP.NET Core-website. In dat geval moeten typen worden geregistreerd bij het bestaande DI-exemplaar. GrpcChannelOptions.ServiceProvider is geconfigureerd door een IServiceProvider van de DI op te halen.

Configureer de load balancer

Een load balancer wordt opgegeven in een service config met behulp van de ServiceConfig.LoadBalancingConfigs verzameling. Twee load balancers zijn ingebouwd en corresponderen met de configuratienamen van load balancers.

Naam Typologie Description
pick_first PickFirstLoadBalancerFactory Probeert verbinding te maken met adressen totdat een verbinding tot stand is gebracht. gRPC-aanroepen worden allemaal uitgevoerd bij de eerste geslaagde verbinding.
round_robin RoundRobinLoadBalancerFactory Pogingen om verbinding te maken met alle adressen. gRPC-aanroepen worden verdeeld over alle geslaagde verbindingen met behulp van round robin-logica .

service config is een afkorting van de serviceconfiguratie en wordt vertegenwoordigd door het ServiceConfig type. Er zijn verschillende manieren waarop een kanaal een service config kan krijgen met een geconfigureerde load balancer.

  • Een app kan een service config specificeren wanneer een kanaal wordt gemaakt met behulp van GrpcChannelOptions.ServiceConfig.
  • Een resolver kan ook een service config voor een kanaal oplossen. Met deze functie kan een externe bron opgeven hoe de aanroepers taakverdeling moeten uitvoeren. Of een resolver ondersteuning biedt voor het oplossen van een oplossing service config , is afhankelijk van de implementatie van de resolver. Schakel deze functie uit met GrpcChannelOptions.DisableResolverServiceConfig.
  • Als er geen service config is opgegeven of als er service config geen load balancer is geconfigureerd, wordt het kanaal standaard PickFirstLoadBalancerFactoryingesteld op .
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" });

De voorgaande code:

  • Hiermee specificeert u een RoundRobinLoadBalancerFactory in de service config.
  • Start de gRPC-aanroep SayHello:
    • DnsResolverFactory maakt een resolver die adressen voor de hostnaam my-example-hostophaalt.
    • De Round-robin load balancer probeert verbinding te maken met alle de opgeloste adressen.
    • gRPC-aanroepen worden gelijkmatig gedistribueerd met behulp van round robin-logica.

Kanaalreferenties configureren

Een kanaal moet weten of gRPC-aanroepen worden verzonden met transportbeveiliging. http en https maakt geen deel meer uit van het adres, het schema geeft nu een resolver op, dus Credentials moet worden geconfigureerd voor kanaalopties bij het gebruik van taakverdeling.

  • ChannelCredentials.SecureSsl- gRPC-aanroepen worden beveiligd met Tls (Transport Layer Security). Gelijk aan een https adres.
  • ChannelCredentials.Insecure - gRPC-aanroepen maken geen gebruik van transportbeveiliging. Gelijk aan een http adres.
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" });

Taakverdeling gebruiken met gRPC-clientfactory

gRPC-clientfactory kan worden geconfigureerd voor het gebruik van taakverdeling:

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

De voorgaande code:

  • Hiermee configureert u de client met een adres voor load-balancing.
  • Hiermee specificeert u kanaalidentificatiegegevens.
  • Registreert DI-typen bij de app IServiceCollection.

Aangepaste resolvers en load balancers schrijven

Taakverdeling aan de clientzijde is uitbreidbaar:

  • Implementeer Resolver om een aangepaste resolver te maken en adressen op te lossen vanuit een nieuwe gegevensbron.
  • Implementeer LoadBalancer om een aangepaste load balancer te maken met nieuw taakverdelingsgedrag.

Belangrijk

De API's die worden gebruikt om taakverdeling aan de clientzijde uit te breiden, zijn experimenteel. Ze kunnen zonder kennisgeving veranderen.

Een aangepaste resolver maken

Een resolver:

  • Resolver wordt geïmplementeerd en gemaakt door een ResolverFactory. Maak een aangepaste resolver door deze typen te implementeren.
  • Is verantwoordelijk voor het oplossen van de adressen die een load balancer gebruikt.
  • Kan eventueel een serviceconfiguratie bieden.
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);
    }
}

In de voorgaande code:

  • FileResolverFactory implementeert ResolverFactory. Het wordt toegewezen aan het file schema en maakt FileResolver instantiaties.
  • FileResolver implementeert PollingResolver. PollingResolver is een abstract basistype waarmee u eenvoudig een resolver met asynchrone logica kunt implementeren door deze te overschrijven ResolveAsync.
  • In ResolveAsync:
    • De bestands-URI wordt geconverteerd naar een lokaal pad. Zo wordt file:///c:/addresses.json gewijzigd in c:\addresses.json.
    • JSON wordt vanaf schijf geladen en geconverteerd naar een verzameling adressen.
    • Listener wordt aangeroepen met resultaten om het kanaal te laten weten dat er adressen beschikbaar zijn.

Een aangepaste load balancer maken

Een load balancer:

  • LoadBalancer wordt geïmplementeerd en gemaakt door een LoadBalancerFactory. Maak een aangepaste load balancer en factory door deze typen te implementeren.
  • Krijgt adressen van een resolver en creëert Subchannel instanties.
  • Volgt de toestand van de verbinding en creëert een SubchannelPicker. Het kanaal gebruikt intern de kiezer om adressen te kiezen bij het maken van gRPC-aanroepen.

Dit is de SubchannelsLoadBalancer:

  • Een abstracte basisklasse die implementeert LoadBalancer.
  • Beheert het genereren van instanties Subchannel uit adressen.
  • Hiermee kunt u eenvoudig een aangepast pick-beleid implementeren voor een verzameling subkanalen.
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);
    }
}

In de voorgaande code:

  • RandomBalancerFactory implementeert LoadBalancerFactory. Dit komt overeen met de random beleidsnaam en genereert RandomBalancer exemplaren.
  • RandomBalancer implementeert SubchannelsLoadBalancer. Er wordt een RandomPicker gemaakt die willekeurig een subkanaal kiest.

Configureer aangepaste resolvers en load balancers

Aangepaste resolvers en load balancers moeten worden geregistreerd via dependency injection (DI) wanneer ze worden gebruikt. Er zijn een aantal opties:

  • Als een app al gebruikmaakt van DI, zoals een ASP.NET Core-web-app, kunnen deze worden geregistreerd bij de bestaande DI-configuratie. Een IServiceProvider kan worden verkregen uit DI en gegeven aan het kanaal met behulp van GrpcChannelOptions.ServiceProvider.
  • Als een app geen di gebruikt, maakt u het volgende:
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);

De voorgaande code:

  • Hiermee maakt u een ServiceCollection implementatie en registreert u nieuwe resolver- en load balancer-implementaties.
  • Hiermee maakt u een kanaal dat is geconfigureerd voor het gebruik van de nieuwe implementaties:
    • ServiceCollection is ingebouwd in een IServiceProvider en ingesteld op GrpcChannelOptions.ServiceProvider.
    • Kanaaladres is file:///c:/data/addresses.json. Het file schema komt overeen met FileResolverFactory.
    • service config de naam van de load balancer is random. Wordt toegewezen aan RandomLoadBalancerFactory.

Waarom taakverdeling belangrijk is

HTTP/2 multiplexen meerdere verzoeken op één TCP-verbinding. Als gRPC en HTTP/2 worden gebruikt met een netwerk load balancer (NLB), wordt de verbinding doorgestuurd naar een server en worden alle gRPC-aanroepen naar die ene server verzonden. De andere serverinstanties op de NLB zijn inactief.

Netwerktaakverdelers zijn een algemene oplossing voor taakverdeling, omdat ze snel en lichtgewicht zijn. Kubernetes gebruikt bijvoorbeeld standaard een netwerk load balancer om verbindingen tussen pod-exemplaren te verdelen. Netwerktaakverdelers zijn echter niet effectief bij het distribueren van belasting wanneer ze worden gebruikt met gRPC en HTTP/2.

Taakverdeling aan de clientzijde of proxy?

gRPC en HTTP/2 kunnen effectief worden verdeeld met behulp van een load balancer-proxy voor toepassingen of taakverdeling aan de clientzijde. Met beide opties kunnen afzonderlijke gRPC-aanroepen worden gedistribueerd over beschikbare servers. Kiezen tussen proxy- en clienttaakverdeling is een architectuurkeuze. Er zijn voor- en nadelen voor elk.

  • Proxy: gRPC-aanroepen worden verzonden naar de proxy, de proxy maakt een taakverdelingsbeslissing en de gRPC-aanroep wordt verzonden naar het uiteindelijke eindpunt. De proxy is verantwoordelijk voor het weten over eindpunten. Met behulp van een proxy voegt u het volgende toe:

    • Een extra netwerkovergang bij gRPC-oproepen.
    • Latentie en verbruikt extra middelen.
    • Proxyserver moet correct zijn ingesteld en geconfigureerd.
  • Taakverdeling aan de clientzijde: De gRPC-client maakt een taakverdelingsbeslissing wanneer een gRPC-aanroep wordt gestart. De gRPC-aanroep wordt rechtstreeks naar het uiteindelijke eindpunt verzonden. Wanneer u taakverdeling aan de clientzijde gebruikt:

    • De client is verantwoordelijk voor het weten over beschikbare eindpunten en het nemen van beslissingen over taakverdeling.
    • Aanvullende clientconfiguratie is vereist.
    • Hoge prestaties, gRPC-aanroepen met gelijke taakverdeling elimineren de noodzaak van een proxy.

Aanvullende bronnen