Condividi tramite


bilanciamento del carico lato client gRPC

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Di James Newton-King

Il bilanciamento del carico lato client è una funzionalità che consente ai client gRPC di distribuire il carico in modo ottimale tra i server disponibili. Questo articolo illustra come configurare il bilanciamento del carico lato client per creare app gRPC a prestazioni elevate e scalabili in .NET.

Il bilanciamento del carico lato client richiede:

  • .NET 5 o versione successiva.
  • Grpc.Net.Client versione 2.45.0 o successiva.

Configurare il bilanciamento del carico lato client gRPC

Il bilanciamento del carico lato client viene configurato quando viene creato un canale. I due componenti da considerare quando si usa il bilanciamento del carico:

  • Resolver, che risolve gli indirizzi per il canale. I resolver supportano il recupero di indirizzi da un'origine esterna. Questa operazione è nota anche come individuazione dei servizi.
  • Il servizio di bilanciamento del carico, che crea connessioni e seleziona l'indirizzo che verrà usato da una chiamata gRPC.

Le implementazioni predefinite di resolver e servizi di bilanciamento del carico sono incluse in Grpc.Net.Client. Il bilanciamento del carico può essere esteso anche scrivendo resolver personalizzati e servizi di bilanciamento del carico.

Gli indirizzi, le connessioni e altri stati di bilanciamento del carico vengono archiviati in un'istanza GrpcChannel di . Un canale deve essere riutilizzato quando si effettuano chiamate gRPC per il corretto funzionamento del bilanciamento del carico.

Nota

Alcune configurazioni di bilanciamento del carico usano l'inserimento delle dipendenze. Le app che non usano l'inserimento delle dipendenze possono creare un'istanza ServiceCollection di .

Se un'app ha già la configurazione dell'inserimento delle dipendenze, ad esempio un sito Web di ASP.NET Core, i tipi devono essere registrati con l'istanza di inserimento delle dipendenze esistente. GrpcChannelOptions.ServiceProvider viene configurato recuperando un'istanza dall'inserimento delle dipendenze IServiceProvider .

Configurare il sistema di risoluzione

Il sistema di risoluzione viene configurato usando l'indirizzo con cui viene creato un canale. Lo schema URI dell'indirizzo specifica il resolver.

Schema Tipo Descrizione
dns DnsResolverFactory Risolve gli indirizzi eseguendo una query sul nome host per i record di indirizzi DNS.
static StaticResolverFactory Risolve gli indirizzi specificati dall'app. Consigliato se un'app conosce già gli indirizzi che chiama.

Un canale non chiama direttamente un URI che corrisponde a un resolver. Viene invece creato e usato un sistema di risoluzione corrispondente per risolvere gli indirizzi.

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

  • Lo schema esegue il dns mapping a DnsResolverFactory. Viene creata una nuova istanza di un resolver DNS per il canale.
  • Il resolver esegue una query DNS per my-example-host e ottiene due risultati: 127.0.0.100 e 127.0.0.101.
  • Il servizio di bilanciamento del carico usa 127.0.0.100:80 e 127.0.0.101:80 per creare connessioni ed effettuare chiamate gRPC.

DnsResolverFactory

DnsResolverFactory Crea un sistema di risoluzione progettato per ottenere gli indirizzi da un'origine esterna. La risoluzione DNS viene comunemente usata per bilanciare il carico sulle istanze di pod che dispongono di servizi headless 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" });

Il codice precedente:

  • Configura il canale creato con l'indirizzo dns:///my-example-host.
    • Lo schema esegue il dns mapping a DnsResolverFactory.
    • my-example-host è il nome host da risolvere.
    • Nessuna porta specificata nell'indirizzo, quindi le chiamate gRPC vengono inviate alla porta 80. Questa è la porta predefinita per i canali non protetti. È possibile specificare una porta facoltativamente dopo il nome host. Ad esempio, dns:///my-example-host:8080 configura le chiamate gRPC da inviare alla porta 8080.
  • Non specifica un servizio di bilanciamento del carico. Per impostazione predefinita, il canale è un servizio di bilanciamento del carico pick first.
  • Avvia la chiamata SayHellogRPC :
    • Il resolver DNS ottiene gli indirizzi per il nome my-example-hosthost .
    • Selezionare il primo servizio di bilanciamento del carico tenta di connettersi a uno degli indirizzi risolti.
    • La chiamata viene inviata al primo indirizzo a cui si connette correttamente il canale.
Memorizzazione nella cache degli indirizzi DNS

Le prestazioni sono importanti durante il bilanciamento del carico. La latenza di risoluzione degli indirizzi viene eliminata dalle chiamate gRPC memorizzando nella cache gli indirizzi. Un resolver verrà richiamato quando si effettua la prima chiamata gRPC e le chiamate successive usano la cache.

Gli indirizzi vengono aggiornati automaticamente se una connessione viene interrotta. L'aggiornamento è importante negli scenari in cui gli indirizzi cambiano in fase di esecuzione. Ad esempio, in Kubernetes un pod riavviato attiva il resolver DNS per aggiornare e ottenere il nuovo indirizzo del pod.

Per impostazione predefinita, un sistema di risoluzione DNS viene aggiornato se una connessione viene interrotta. Il sistema di risoluzione DNS può anche essere aggiornato facoltativamente in base a un intervallo periodico. Ciò può essere utile per rilevare rapidamente nuove istanze di pod.

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

Il codice precedente crea un oggetto DnsResolverFactory con un intervallo di aggiornamento e lo registra con l'inserimento delle dipendenze. Per altre informazioni sull'uso di un resolver configurato personalizzato, vedere Configurare resolver e servizi di bilanciamento del carico personalizzati.

StaticResolverFactory

Un sistema di risoluzione statico viene fornito da StaticResolverFactory. Questo sistema di risoluzione:

  • Non chiama un'origine esterna. L'app client configura invece gli indirizzi.
  • È progettato per situazioni in cui un'app conosce già gli indirizzi che chiama.
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);

Il codice precedente:

  • Crea un oggetto StaticResolverFactory. Questa factory conosce due indirizzi: localhost:80 e localhost:81.
  • Registra la factory con inserimento delle dipendenze( DI).
  • Configura il canale creato con:
    • Indirizzo static:///my-example-host. Lo schema esegue il static mapping a un sistema di risoluzione statico.
    • Imposta GrpcChannelOptions.ServiceProvider con il provider di servizi di inserimento delle dipendenze.

In questo esempio viene creato un nuovo ServiceCollection oggetto per l'inserimento delle dipendenze. Si supponga che un'app abbia già la configurazione dell'inserimento delle dipendenze, ad esempio un sito Web di ASP.NET Core. In tal caso, i tipi devono essere registrati con l'istanza di inserimento delle dipendenze esistente. GrpcChannelOptions.ServiceProvider viene configurato recuperando un'istanza dall'inserimento delle dipendenze IServiceProvider .

Configurare il bilanciamento del carico

Un servizio di bilanciamento del carico viene specificato in un service config oggetto utilizzando la ServiceConfig.LoadBalancingConfigs raccolta . Due servizi di bilanciamento del carico sono incorporati ed eseguono il mapping ai nomi di configurazione del servizio di bilanciamento del carico:

Nome Tipo Descrizione
pick_first PickFirstLoadBalancerFactory Tenta di connettersi agli indirizzi fino a quando non viene stabilita una connessione. Tutte le chiamate gRPC vengono effettuate alla prima connessione riuscita.
round_robin RoundRobinLoadBalancerFactory Tenta di connettersi a tutti gli indirizzi. Le chiamate gRPC vengono distribuite in tutte le connessioni riuscite usando la logica round robin .

service config è un'abbreviazione della configurazione del servizio ed è rappresentata dal ServiceConfig tipo . Esistono due modi per ottenere un service config canale con un servizio di bilanciamento del carico configurato:

  • Un'app può specificare un oggetto service config quando viene creato un canale usando GrpcChannelOptions.ServiceConfig.
  • In alternativa, un sistema di risoluzione può risolvere un oggetto service config per un canale. Questa funzionalità consente a un'origine esterna di specificare il modo in cui i chiamanti devono eseguire il bilanciamento del carico. Indica se un resolver supporta la risoluzione di un service config oggetto dipende dall'implementazione del resolver. Disabilitare questa funzionalità con GrpcChannelOptions.DisableResolverServiceConfig.
  • Se non viene specificato alcun valore service config o service config se non è configurato un servizio di bilanciamento del carico, per impostazione predefinita il canale è 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" });

Il codice precedente:

  • Specifica un oggetto RoundRobinLoadBalancerFactory in service config.
  • Avvia la chiamata SayHellogRPC :
    • DnsResolverFactory crea un sistema di risoluzione che ottiene gli indirizzi per il nome my-example-hosthost .
    • Il servizio di bilanciamento del carico round robin tenta di connettersi a tutti gli indirizzi risolti.
    • Le chiamate gRPC vengono distribuite in modo uniforme usando la logica round robin.

Configurare le credenziali del canale

Un canale deve sapere se le chiamate gRPC vengono inviate usando la sicurezza del trasporto. http e https non fanno più parte dell'indirizzo, lo schema ora specifica un sistema di risoluzione, quindi Credentials deve essere configurato nelle opzioni del canale quando si usa il bilanciamento del carico.

  • ChannelCredentials.SecureSsl- le chiamate gRPC sono protette con Transport Layer Security (TLS). Equivalente a un https indirizzo.
  • ChannelCredentials.Insecure - le chiamate gRPC non usano la sicurezza del trasporto. Equivalente a un http indirizzo.
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" });

Usare il bilanciamento del carico con la factory client gRPC

La factory client gRPC può essere configurata per l'uso del bilanciamento del carico:

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

Il codice precedente:

  • Configura il client con un indirizzo di bilanciamento del carico.
  • Specifica le credenziali del canale.
  • Registra i tipi di inserimento delle dipendenze con l'oggetto dell'app IServiceCollection.

Scrivere resolver personalizzati e servizi di bilanciamento del carico

Il bilanciamento del carico lato client è estendibile:

  • Implementare Resolver per creare un sistema di risoluzione personalizzato e risolvere gli indirizzi da una nuova origine dati.
  • Implementare LoadBalancer per creare un servizio di bilanciamento del carico personalizzato con un nuovo comportamento di bilanciamento del carico.

Importante

Le API usate per estendere il bilanciamento del carico lato client sono sperimentali. Possono cambiare senza preavviso.

Creare un sistema di risoluzione personalizzato

Un sistema di risoluzione:

  • Implementa Resolver e viene creato da un oggetto ResolverFactory. Creare un sistema di risoluzione personalizzato implementando questi tipi.
  • È responsabile della risoluzione degli indirizzi usati da un servizio di bilanciamento del carico.
  • Facoltativamente, è possibile specificare una configurazione del servizio.
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);
    }
}

Nel codice precedente:

  • FileResolverFactory implementa ResolverFactory. Esegue il file mapping allo schema e crea FileResolver istanze.
  • FileResolver implementa PollingResolver. PollingResolver è un tipo di base astratto che semplifica l'implementazione di un resolver con logica asincrona eseguendo l'override di ResolveAsync.
  • In ResolveAsync:
    • L'URI del file viene convertito in un percorso locale. Ad esempio, file:///c:/addresses.json diventa c:\addresses.json.
    • JSON viene caricato dal disco e convertito in una raccolta di indirizzi.
    • Il listener viene chiamato con i risultati per informare il canale che gli indirizzi sono disponibili.

Creare un servizio di bilanciamento del carico personalizzato

Un servizio di bilanciamento del carico:

  • Implementa LoadBalancer e viene creato da un oggetto LoadBalancerFactory. Creare un servizio di bilanciamento del carico personalizzato e una factory implementando questi tipi.
  • Vengono assegnati indirizzi da un sistema di risoluzione e vengono create Subchannel istanze.
  • Tiene traccia dello stato della connessione e crea un oggetto SubchannelPicker. Il canale usa internamente la selezione per selezionare gli indirizzi durante l'esecuzione di chiamate gRPC.

è SubchannelsLoadBalancer :

  • Classe di base astratta che implementa LoadBalancer.
  • Gestisce la creazione di Subchannel istanze da indirizzi.
  • Semplifica l'implementazione di un criterio di selezione personalizzato su una raccolta di sottocanali.
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);
    }
}

Nel codice precedente:

  • RandomBalancerFactory implementa LoadBalancerFactory. Esegue il mapping al nome del random criterio e crea RandomBalancer istanze.
  • RandomBalancer implementa SubchannelsLoadBalancer. Crea un oggetto RandomPicker che seleziona in modo casuale un sottocanale.

Configurare resolver personalizzati e servizi di bilanciamento del carico

I resolver personalizzati e i servizi di bilanciamento del carico devono essere registrati con inserimento delle dipendenze (DI) quando vengono usati. Sono disponibili due opzioni:

  • Se un'app usa già l'inserimento delle dipendenze, ad esempio un'app Web ASP.NET Core, può essere registrata con la configurazione di inserimento delle dipendenze esistente. Un IServiceProvider oggetto può essere risolto dall'inserimento di dipendenze e passato al canale tramite GrpcChannelOptions.ServiceProvider.
  • Se un'app non usa l'inserimento delle dipendenze, creare:
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);

Il codice precedente:

  • Crea un oggetto ServiceCollection e registra nuove implementazioni del sistema di risoluzione e del bilanciamento del carico.
  • Crea un canale configurato per l'uso delle nuove implementazioni:
    • ServiceCollection è integrato in un IServiceProvider oggetto e impostato su GrpcChannelOptions.ServiceProvider.
    • L'indirizzo del canale è file:///c:/data/addresses.json. Lo schema esegue il file mapping a FileResolverFactory.
    • service config il nome del servizio di bilanciamento del carico è random. Esegue il mapping a RandomLoadBalancerFactory.

Perché il bilanciamento del carico è importante

HTTP/2 multiplexes multiple calls on a single TCP connection (HTTP/2 multiplexes on a single TCP connection). Se gRPC e HTTP/2 vengono usati con un servizio di bilanciamento del carico di rete (NLB), la connessione viene inoltrata a un server e tutte le chiamate gRPC vengono inviate a tale server. Le altre istanze del server nel bilanciamento carico di rete sono inattive.

I servizi di bilanciamento del carico di rete sono una soluzione comune per il bilanciamento del carico perché sono veloci e leggeri. Ad esempio, Kubernetes usa per impostazione predefinita un servizio di bilanciamento del carico di rete per bilanciare le connessioni tra istanze di pod. Tuttavia, i servizi di bilanciamento del carico di rete non sono efficaci durante la distribuzione del carico quando vengono usati con gRPC e HTTP/2.

Bilanciamento del carico sul lato proxy o sul lato client?

GRPC e HTTP/2 possono essere efficacemente bilanciati tramite un proxy del servizio di bilanciamento del carico dell'applicazione o il bilanciamento del carico lato client. Entrambe queste opzioni consentono la distribuzione di singole chiamate gRPC tra i server disponibili. La scelta tra il bilanciamento del carico sul lato client e il proxy è una scelta architetturale. Ci sono vantaggi e svantaggi per ognuno.

  • Proxy: le chiamate gRPC vengono inviate al proxy, il proxy prende una decisione di bilanciamento del carico e la chiamata gRPC viene inviata all'endpoint finale. Il proxy è responsabile della conoscenza degli endpoint. L'uso di un proxy aggiunge:

    • Un hop di rete aggiuntivo per le chiamate gRPC.
    • Latenza e utilizzo di risorse aggiuntive.
    • Il server proxy deve essere configurato e configurato correttamente.
  • Bilanciamento del carico lato client: il client gRPC prende una decisione di bilanciamento del carico all'avvio di una chiamata gRPC. La chiamata gRPC viene inviata direttamente all'endpoint finale. Quando si usa il bilanciamento del carico lato client:

    • Il client è responsabile della conoscenza degli endpoint disponibili e delle decisioni di bilanciamento del carico.
    • È necessaria una configurazione client aggiuntiva.
    • Le chiamate gRPC con bilanciamento del carico ad alte prestazioni eliminano la necessità di un proxy.

Risorse aggiuntive