Teilen über


gRPC: Clientseitiger Lastenausgleich

Von James Newton-King

Der clientseitige Lastenausgleich ist ein Feature, mit dem gRPC-Clients die Last optimal auf verfügbare Server verteilen können. In diesem Artikel wird erläutert, wie Sie den clientseitigen Lastenausgleich konfigurieren, um skalierbare, leistungsstarke gRPC-Apps in .NET zu erstellen.

Für clientseitigen Lastenausgleich ist Folgendes erforderlich:

Konfigurieren von clientseitigem gRPC-Lastenausgleich

Clientseitiger Lastenausgleich wird konfiguriert, wenn ein Kanal erstellt wird. Die folgenden beiden Komponenten sind bei der Verwendung des Lastenausgleichs zu berücksichtigen:

  • Der Resolver, der die Adressen für den Kanal auflöset. Resolver unterstützen das Abrufen von Adressen aus einer externen Quelle. Dies wird auch als Dienstermittlung bezeichnet.
  • Der Lastenausgleich, der Verbindungen erstellt und die Adresse auswählt, die ein gRPC-Aufruf verwendet.

Integrierte Implementierungen von Resolvern und Load Balancern sind in Grpc.Net.Client enthalten. Der Lastenausgleich kann auch erweitert werden, indem benutzerdefinierte Resolver und Load Balancer geschrieben werden.

Adressen, Verbindungen und andere Lastenausgleichsstatus werden in einer GrpcChannel-Instanz gespeichert. Ein Kanal muss beim Ausführen von gRPC-Aufrufen wiederverwendet werden, damit der Lastenausgleich ordnungsgemäß funktioniert.

Hinweis

Einige Konfigurationen für den Lastenausgleich verwenden die Abhängigkeitsinjektion (Dependency Injection, DI). Apps ohne Verwendung der Abhängigkeitsinjektion können eine ServiceCollection-Instanz erstellen.

Wenn eine App bereits über eine DI-Konfiguration verfügt, z. B. eine ASP.NET Core-Website, dann sollten Typen mit der vorhandenen DI-Instanz registriert werden. GrpcChannelOptions.ServiceProvider wird konfiguriert, indem ein IServiceProvider aus DI abgerufen wird.

Konfigurieren des Resolvers

Der Resolver wird mit der Adresse konfiguriert, mit der ein Kanal erstellt wird. Das URI-Scheme der Adresse gibt den Resolver an.

Schema type BESCHREIBUNG
dns DnsResolverFactory Löst Adressen durch Abfragen des Hostnamens für DNS-Adresseinträge auf.
static StaticResolverFactory Löst Adressen auf, die von der App angegeben wurden. Empfohlen, wenn eine App die Adressen bereits kennt, die sie aufruft.

Ein Kanal aufruft nicht direkt einen URI auf, der einem Konfliktlöser entspricht. Stattdessen wird ein entsprechender Resolver erstellt und verwendet, um die Adressen aufzulösen.

Beispielsweise gilt bei Verwendung von GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure }):

  • Das dns-Schema wird DnsResolverFactory zugeordnet. Für den Kanal wird eine neue Instanz eines DNS-Resolvers erstellt.
  • Der Resolver nimmt eine DNS-Abfrage für my-example-host vor und erhält zwei Ergebnisse: 127.0.0.100 und 127.0.0.101.
  • Der Load Balancer verwendet 127.0.0.100:80 und 127.0.0.101:80, um Verbindungen herzustellen und gRPC-Aufrufe vorzunehmen.

DnsResolverFactory

DnsResolverFactory erstellt einen Resolver, der zum Abrufen von Adressen aus einer externen Quelle entworfen wurde. DNS-Auflösung wird häufig für den Lastenausgleich über Podinstanzen verwendet, die über einen headless-Dienst von Kubernetes verfügen.

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

Der vorangehende Code:

  • Konfiguriert den erstellten Kanal mit der Adresse dns:///my-example-host.
    • Das dns-Schema wird DnsResolverFactory zugeordnet.
    • my-example-host ist der Hostname, der aufgelöst werden soll.
    • In der Adresse ist kein Port angegeben, sodass gRPC-Aufrufe an Port 80 gesendet werden. Dies ist der Standardport für ungesicherte Kanäle. Optional kann ein Port nach dem Hostnamen angegeben werden. dns:///my-example-host:8080 konfiguriert beispielsweise gRPC-Aufrufe so, dass sie an Port 8080 gesendet werden.
  • Gibt keinen Lastenausgleich an. Der Kanal verwendet standardmäßig einen Pick first-Lastenausgleich.
  • Startet den gRPC-Aufruf SayHello:
    • Der DNS-Resolver ruft Adressen für den Hostnamen my-example-host ab.
    • Der Pick first-Lastenausgleich versucht, eine Verbindung mit einer der aufgelösten Adressen herzustellen.
    • Der Aufruf wird an die erste Adresse gesendet, mit der der Kanal erfolgreich eine Verbindung herstellt.
Zwischenspeicherung von DNS-Adressen

Beim Lastenausgleich ist die Leistung wichtig. Die Latenz beim Auflösen von Adressen wird durch Zwischenspeichern der Adressen durch gRPC-Aufrufe beseitigt. Beim ersten gRPC-Aufruf wird ein Resolver aufgerufen, und nachfolgende Aufrufe verwenden den Cache.

Adressen werden automatisch aktualisiert, wenn eine Verbindung unterbrochen wird. Aktualisierung ist wichtig in Szenarien, in denen sich Adressen zur Laufzeit ändern. In Kubernetes löst ein erneut gestarteter Pod beispielsweise eine Aktualisierung des DNS-Resolvers aus, um die neue Adresse des Pods zu erhalten.

Ein DNS-Resolver wird standardmäßig aktualisiert, wenn eine Verbindung unterbrochen wird. Optional kann sich der DNS-Resolver auch in regelmäßigen Abständen selbst aktualisieren. Dies kann nützlich sein, um schnell neue Podinstanzen zu erkennen.

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

Der vorangehende Code erstellt eine DnsResolverFactory mit einem Aktualisierungsintervall und registriert sie per Abhängigkeitsinjektion. Weitere Informationen zur Verwendung eines benutzerdefinierten Resolvers finden Sie unter Konfigurieren von benutzerdefinierten Resolvern und Load Balancern.

StaticResolverFactory

Ein statischer Resolver wird von StaticResolverFactory bereitgestellt. Für diesen Resolver gilt:

  • Er ruft keine externe Quelle auf. Stattdessen konfiguriert die Client-App die Adressen.
  • Er ist für Situationen konzipiert, in denen eine App bereits die Adressen kennt, die sie aufruft.
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);

Der vorangehende Code:

  • Erstellt eine StaticResolverFactory. Diese Factory kennt zwei Adressen: localhost:80 und localhost:81.
  • Registriert die Factory mit Abhängigkeitsinjektion (Dependency Injection, DI).
  • Konfiguriert den erstellten Kanal mit:
    • Der Adresse static:///my-example-host. Das static-Schema entspricht einem statischen Resolver.
    • Legt GrpcChannelOptions.ServiceProvider mit dem DI-Dienstanbieter fest.

In diesem Beispiel wird eine neue ServiceCollection für DI erstellt. Angenommen, eine App hat bereits DI eingerichtet, z. B. eine ASP.NET Core-Website. In diesem Fall sollten Typen bei der vorhandenen DI-Instanz registriert werden. GrpcChannelOptions.ServiceProvider wird konfiguriert, indem ein IServiceProvider aus DI abgerufen wird.

Konfigurieren des Lastenausgleichs

Ein Load Balancer wird in einer service config unter Verwendung der ServiceConfig.LoadBalancingConfigs-Sammlung angegeben. Zwei Load Balancer sind integriert und sind den Konfigurationsnamen des Lastenausgleichs zugeordnet:

Name Art Beschreibung
pick_first PickFirstLoadBalancerFactory Versucht, eine Verbindung mit Adressen herzustellen, bis eine Verbindung erfolgreich hergestellt wurde. gRPC-Aufrufe werden alle für die erste erfolgreiche Verbindung vorgenommen.
round_robin RoundRobinLoadBalancerFactory Versucht, eine Verbindung mit allen Adressen herzustellen. gRPC-Aufrufe werden mit Roundrobin-Logik auf alle erfolgreichen Verbindungen verteilt.

service config ist eine Abkürzung für Dienstkonfiguration und wird durch den Typ ServiceConfig dargestellt. Es gibt mehrere Möglichkeiten, wie ein Kanal eine service config mit einem konfigurierten Lastenausgleich abrufen kann:

  • Eine Anwendung kann eine service config angeben, wenn ein Kanal mit GrpcChannelOptions.ServiceConfig erstellt wird.
  • Alternativ dazu kann ein Resolver eine service config für einen Kanal auflösen. Mit diesem Feature kann eine externe Quelle angeben, wie die Aufrufer den Lastenausgleich durchführen sollen. Ob ein Resolver das Auflösen einer service config unterstützt, hängt von der Resolverimplementierung ab. Dieses Feature mit GrpcChannelOptions.DisableResolverServiceConfig deaktivieren.
  • Wenn keine service config angegeben wird oder für service config kein Load Balancer konfiguriert ist, wird der Kanal standardmäßig auf PickFirstLoadBalancerFactory festgelegt.
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" });

Der vorangehende Code:

  • Gibt eine RoundRobinLoadBalancerFactory in der service config an.
  • Startet den gRPC-Aufruf SayHello:
    • DnsResolverFactory erstellt einen Resolver, der Adressen für den Hostnamen my-example-host abruft.
    • Der Roundrobin-Lastenausgleich versucht, eine Verbindung mit allen aufgelösten Adressen herzustellen.
    • gRPC-Aufrufe werden gleichmäßig mit Roundrobin-Logik verteilt.

Konfigurieren von Kanalanmeldeinformationen

Ein Kanal muss wissen, ob gRPC-Aufrufe mithilfe von Transportsicherheit gesendet werden. http und https sind nicht mehr Teil der Adresse. Das Schema gibt jetzt einen Resolver an. Daher muss bei Verwendung von Lastenausgleich Credentials für Kanaloptionen konfiguriert werden.

  • ChannelCredentials.SecureSsl: gRPC-Aufrufe werden mit ChannelCredentials.SecureSsl gesichert. Äquivalent zu einer https-Adresse.
  • ChannelCredentials.Insecure: gRPC-Aufrufe verwenden keine Transportsicherheit. Äquivalent zu einer http-Adresse.
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" });

Verwenden des Lastenausgleichs mit der gRPC-Clientfactory

Die gRPC-Clientfactory kann zur Verwendung des Lastenausgleichs konfiguriert werden:

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

Der vorangehende Code:

  • Konfiguriert den Client mit einer Adresse für den Lastenausgleich.
  • Gibt Kanalanmeldeinformationen an.
  • Registriert DI-Typen bei der IServiceCollection der App.

Schreiben von benutzerdefinierten Resolvern und Load Balancern

Clientseitiger Lastenausgleich ist erweiterbar:

  • Implementieren Sie Resolver, um einen benutzerdefinierten Resolver zu erstellen und Adressen aus einer neuen Datenquelle aufzulösen.
  • Implementieren Sie LoadBalancer, um einen benutzerdefinierten Load Balancer mit neuem Lastenausgleichsverhalten zu erstellen.

Wichtig

Die APIs, die zum Erweitern des clientseitigen Lastenausgleichs verwendet werden, sind experimentell. Sie können ohne vorherige Ankündigung geändert werden.

Erstellen eines benutzerdefinierten Resolvers

Ein Resolver:

  • Implementiert Resolver und wird von einer ResolverFactory erstellt. Erstellen Sie einen benutzerdefinierten Resolver, indem Sie diese Typen implementieren.
  • Er ist für die Auflösung der Adressen verantwortlich, die ein Load Balancer verwendet.
  • Kann optional eine Dienstkonfiguration bereitstellen.
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);
    }
}

Für den Code oben gilt:

  • FileResolverFactory implementiert ResolverFactory. Die Zuordnung erfolgt zum file-Schema, und es werden FileResolver-Instanzen erstellt.
  • FileResolver implementiert PollingResolver. PollingResolver ist ein abstrakter Basistyp, der das Implementieren eines Resolvers mit asynchroner Logik durch das Überschreiben von ResolveAsync erleichtert.
  • In: ResolveAsync
    • Der Datei-URI wird in einen lokalen Pfad konvertiert. file:///c:/addresses.json wird beispielsweise zu c:\addresses.json.
    • JSON wird vom Datenträger geladen und in eine Sammlung von Adressen konvertiert.
    • Der Listener wird mit Ergebnissen aufgerufen, um den Kanal darüber zu informieren, dass Adressen verfügbar sind.

Erstellen eines benutzerdefinierten Load Balancers

Ein Load Balancer:

  • Implementiert LoadBalancer und wird von einer LoadBalancerFactory erstellt. Erstellen Sie einen benutzerdefinierten Load Balancer und eine Factory, indem Sie diese Typen implementieren.
  • Erhält Adressen von einem Resolver und erstellt Subchannel-Instanzen.
  • Verfolgt den Zustand der Verbindung nach und erstellt einen SubchannelPicker. Der Kanal verwendet intern die Auswahl, um Adressen beim Ausführen von gRPC-Aufrufen auszuwählen.

Für den SubchannelsLoadBalancer gilt Folgendes:

  • Er ist eine abstrakte Basisklasse, die LoadBalancer implementiert.
  • Er verwaltet das Erstellen von Subchannel-Instanzen aus Adressen.
  • Erleichtert die Implementierung einer benutzerdefinierten Auswahlrichtlinie für eine Sammlung von Unterkanälen.
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);
    }
}

Für den Code oben gilt:

  • RandomBalancerFactory implementiert LoadBalancerFactory. Die Zuordnung erfolgt zum random-Richtliniennamen, und es werden RandomBalancer-Instanzen erstellt.
  • RandomBalancer implementiert SubchannelsLoadBalancer. Es wird ein RandomPicker erstellt, der zufällig einen Unterkanal auswählt.

Konfigurieren von benutzerdefinierten Resolvern und Load Balancern

Benutzerdefinierte Resolver und Load Balancer müssen mit Dependency Injection (DI) registriert werden, wenn sie verwendet werden. Es sind mehrere Optionen verfügbar:

  • Wenn eine App bereits DI verwendet (z. B. eine ASP.NET Core Web-App), kann die Registrierung mit der vorhandenen DI-Konfiguration erfolgen. Ein IServiceProvider kann aus DI aufgelöst und mit GrpcChannelOptions.ServiceProvider an den Kanal übergeben werden.
  • Wenn eine App keine DI verwendet, erstellen Sie Folgendes:
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);

Der vorangehende Code:

  • Erstellt eine ServiceCollection und registriert neue Resolver- und Load Balancer-Implementierungen.
  • Erstellt einen Kanal, der für die Verwendung der neuen Implementierungen konfiguriert ist:
    • ServiceCollection ist in einen IServiceProvider integriert und auf GrpcChannelOptions.ServiceProvider festgelegt.
    • Die Kanaladresse ist file:///c:/data/addresses.json. Das file-Schema wird FileResolverFactory zugeordnet.
    • service config Lastenausgleichsname ist random. Wird RandomLoadBalancerFactory zugeordnet.

Warum Lastenausgleich wichtig ist

HTTP/2 führt Multiplexing für mehrere Aufrufe über eine einzige TCP-Verbindung aus. Wenn gRPC und HTTP/2 mit einem Netzwerklastenausgleich (Network Load Balancer, NLB) verwendet werden, wird die Verbindung an einen Server weitergeleitet, und alle gRPC-Aufrufe werden an diesen einen Server gesendet. Die anderen Serverinstanzen auf dem NLB befinden sich im Leerlauf.

Netzwerklastenausgleiche sind eine gängige Lösung für den Lastenausgleich, da sie schnell und schlank sind. Kubernetes verwendet beispielsweise standardmäßig einen Netzwerklastenausgleich, um Verbindungen zwischen Podinstanzen auszugleichen. Bei Verwendung mit gRPC und HTTP/2 sind Netzwerklastenausgleiche jedoch nicht effektiv bei der Verteilung der Last.

Proxy- oder clientseitiger Lastenausgleich?

gRPC und HTTP/2 können effektiv mithilfe eines Anwendungslastenausgleichs-Proxys oder clientseitigen Lastenausgleichs ausgeglichen werden. Beide Optionen ermöglichen die Verteilung einzelner gRPC-Aufrufe auf verfügbare Server. Die Entscheidung zwischen proxy- und clientseitigem Lastenausgleich ist eine Architekturentscheidung. Beide Ansätze haben Vor- und Nachteile.

  • Proxy: gRPC-Aufrufe werden an den Proxy gesendet, der Proxy trifft eine Lastenausgleichsentscheidung, und der gRPC-Aufruf wird an den endgültigen Endpunkt gesendet. Der Proxy ist dafür verantwortlich, die Endpunkte zu kennen. Die Verwendung eines Proxys fügt Folgendes hinzu:

    • Einen zusätzlichen Netzwerkhop für gRPC-Aufrufe.
    • Latenz sowie Verbrauch zusätzlicher Ressourcen.
    • Der Proxyserver muss ordnungsgemäß eingerichtet und konfiguriert sein.
  • Clientseitiger Lastenausgleich: Der gRPC-Client trifft eine Lastenausgleichsentscheidung, wenn ein gRPC-Aufruf gestartet wird. Der gRPC-Aufruf wird direkt an den endgültigen Endpunkt gesendet. Bei Verwendung von clientseitigem Lastenausgleich:

    • Der Client ist dafür verantwortlich, die verfügbaren Endpunkte zu kennen und Entscheidungen zum Lastenausgleich zu treffen.
    • Weitere Clientkonfiguration ist erforderlich.
    • Bei gRPC-Hochleistungsaufrufen mit Lastenausgleich entfällt die Notwendigkeit eines Proxys.

Zusätzliche Ressourcen