394 lines
19 KiB
Markdown
394 lines
19 KiB
Markdown
---
|
|
title: gRPC client-side load balancing
|
|
author: jamesnk
|
|
description: Learn how to make scalable, high-performance gRPC apps with client-side load balancing in .NET.
|
|
monikerRange: '>= aspnetcore-3.0'
|
|
ms.author: wpickett
|
|
ms.date: 05/11/2023
|
|
uid: grpc/loadbalancing
|
|
---
|
|
# gRPC client-side load balancing
|
|
|
|
[!INCLUDE[](~/includes/not-latest-version.md)]
|
|
|
|
By [James Newton-King](https://twitter.com/jamesnk)
|
|
|
|
Client-side load balancing is a feature that allows gRPC clients to distribute load optimally across available servers. This article discusses how to configure client-side load balancing to create scalable, high-performance gRPC apps in .NET.
|
|
|
|
Client-side load balancing requires:
|
|
|
|
* .NET 5 or later.
|
|
* [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) version 2.45.0 or later.
|
|
|
|
## Configure gRPC client-side load balancing
|
|
|
|
Client-side load balancing is configured when a channel is created. The two components to consider when using load balancing:
|
|
|
|
* The resolver, which resolves the addresses for the channel. Resolvers support getting addresses from an external source. This is also known as service discovery.
|
|
* The load balancer, which creates connections and picks the address that a gRPC call will use.
|
|
|
|
Built-in implementations of resolvers and load balancers are included in [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client). Load balancing can also be extended by [writing custom resolvers and load balancers](#write-custom-resolvers-and-load-balancers).
|
|
|
|
Addresses, connections and other load balancing state is stored in a `GrpcChannel` instance. A channel must be reused when making gRPC calls for load balancing to work correctly.
|
|
|
|
> [!NOTE]
|
|
> Some load balancing configuration uses dependency injection (DI). Apps that don't use DI can create a <xref:Microsoft.Extensions.DependencyInjection.ServiceCollection> instance.
|
|
>
|
|
> If an app already has DI setup, like an ASP.NET Core website, then types should be registered with the existing DI instance. `GrpcChannelOptions.ServiceProvider` is configured by getting an <xref:System.IServiceProvider> from DI.
|
|
|
|
## Configure resolver
|
|
|
|
The resolver is configured using the address a channel is created with. The [URI scheme](https://wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax) of the address specifies the resolver.
|
|
|
|
| Scheme | Type | Description |
|
|
| -------- | ----------------------- | ----------- |
|
|
| `dns` | `DnsResolverFactory` | Resolves addresses by querying the hostname for [DNS address records](https://wikipedia.org/wiki/List_of_DNS_record_types#A). |
|
|
| `static` | `StaticResolverFactory` | Resolves addresses that the app has specified. Recommended if an app already knows the addresses it calls. |
|
|
|
|
A channel doesn't directly call a URI that matches a resolver. Instead, a matching resolver is created and used to resolve the addresses.
|
|
|
|
For example, using `GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })`:
|
|
|
|
* The `dns` scheme maps to `DnsResolverFactory`. A new instance of a DNS resolver is created for the channel.
|
|
* The resolver makes a DNS query for `my-example-host` and gets two results: `127.0.0.100` and `127.0.0.101`.
|
|
* The load balancer uses `127.0.0.100:80` and `127.0.0.101:80` to create connections and make gRPC calls.
|
|
|
|
#### DnsResolverFactory
|
|
|
|
The `DnsResolverFactory` creates a resolver designed to get addresses from an external source. DNS resolution is commonly used to load balance over pod instances that have a [Kubernetes headless services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services).
|
|
|
|
```csharp
|
|
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" });
|
|
```
|
|
|
|
The preceding code:
|
|
|
|
* Configures the created channel with the address `dns:///my-example-host`.
|
|
* The `dns` scheme maps to `DnsResolverFactory`.
|
|
* `my-example-host` is the hostname to resolve.
|
|
* No port is specified in the address, so gRPC calls are sent to port 80. This is the default port for unsecured channels. A port can optionally be specified after the hostname. For example, `dns:///my-example-host:8080` configures gRPC calls to be sent to port 8080.
|
|
* Doesn't specify a load balancer. The channel defaults to a pick first load balancer.
|
|
* Starts the gRPC call `SayHello`:
|
|
* DNS resolver gets addresses for the hostname `my-example-host`.
|
|
* Pick first load balancer attempts to connect to one of the resolved addresses.
|
|
* The call is sent to the first address the channel successfully connects to.
|
|
|
|
##### DNS address caching
|
|
|
|
Performance is important when load balancing. The latency of resolving addresses is eliminated from gRPC calls by caching the addresses. A resolver will be invoked when making the first gRPC call, and subsequent calls use the cache.
|
|
|
|
Addresses are automatically refreshed if a connection is interrupted. Refreshing is important in scenarios where addresses change at runtime. For example, in Kubernetes a [restarted pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/) triggers the DNS resolver to refresh and get the pod's new address.
|
|
|
|
By default, a DNS resolver is refreshed if a connection is interrupted. The DNS resolver can also optionally refresh itself on a periodic interval. This can be useful for quickly detecting new pod instances.
|
|
|
|
```csharp
|
|
services.AddSingleton<ResolverFactory>(
|
|
sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
|
|
```
|
|
|
|
The preceding code creates a `DnsResolverFactory` with a refresh interval and registers it with dependency injection. For more information on using a custom-configured resolver, see [Configure custom resolvers and load balancers](#configure-custom-resolvers-and-load-balancers).
|
|
|
|
#### StaticResolverFactory
|
|
|
|
A static resolver is provided by `StaticResolverFactory`. This resolver:
|
|
|
|
* Doesn't call an external source. Instead, the client app configures the addresses.
|
|
* Is designed for situations where an app already knows the addresses it calls.
|
|
|
|
```csharp
|
|
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);
|
|
```
|
|
|
|
The preceding code:
|
|
|
|
* Creates a `StaticResolverFactory`. This factory knows about two addresses: `localhost:80` and `localhost:81`.
|
|
* Registers the factory with dependency injection (DI).
|
|
* Configures the created channel with:
|
|
* The address `static:///my-example-host`. The `static` scheme maps to a static resolver.
|
|
* Sets `GrpcChannelOptions.ServiceProvider` with the DI service provider.
|
|
|
|
This example creates a new <xref:Microsoft.Extensions.DependencyInjection.ServiceCollection> for DI. Suppose an app already has DI setup, like an ASP.NET Core website. In that case, types should be registered with the existing DI instance. `GrpcChannelOptions.ServiceProvider` is configured by getting an <xref:System.IServiceProvider> from DI.
|
|
|
|
## Configure load balancer
|
|
|
|
A load balancer is specified in a [`service config`](https://github.com/grpc/grpc/blob/master/doc/service_config.md) using the `ServiceConfig.LoadBalancingConfigs` collection. Two load balancers are built-in and map to load balancer config names:
|
|
|
|
| Name | Type | Description |
|
|
| ------------- | ------------------------------- | ----------- |
|
|
| `pick_first` | `PickFirstLoadBalancerFactory` | Attempts to connect to addresses until a connection is successfully made. gRPC calls are all made to the first successful connection. |
|
|
| `round_robin` | `RoundRobinLoadBalancerFactory` | Attempts to connect to all addresses. gRPC calls are distributed across all successful connections using [round-robin](https://www.nginx.com/resources/glossary/round-robin-load-balancing/) logic. |
|
|
|
|
`service config` is an abbreviation of service configuration and is represented by the `ServiceConfig` type. There are a couple of ways a channel can get a `service config` with a load balancer configured:
|
|
|
|
* An app can specify a `service config` when a channel is created using `GrpcChannelOptions.ServiceConfig`.
|
|
* Alternatively, a resolver can resolve a `service config` for a channel. This feature allows an external source to specify how its callers should perform load balancing. Whether a resolver supports resolving a `service config` is dependent on the resolver implementation. Disable this feature with `GrpcChannelOptions.DisableResolverServiceConfig`.
|
|
* If no `service config` is provided, or the `service config` doesn't have a load balancer configured, the channel defaults to `PickFirstLoadBalancerFactory`.
|
|
|
|
```csharp
|
|
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" });
|
|
```
|
|
|
|
The preceding code:
|
|
|
|
* Specifies a `RoundRobinLoadBalancerFactory` in the `service config`.
|
|
* Starts the gRPC call `SayHello`:
|
|
* `DnsResolverFactory` creates a resolver that gets addresses for the hostname `my-example-host`.
|
|
* Round-robin load balancer attempts to connect to all resolved addresses.
|
|
* gRPC calls are distributed evenly using round-robin logic.
|
|
|
|
## Configure channel credentials
|
|
|
|
A channel must know whether gRPC calls are sent using [transport security](xref:grpc/security#transport-security). `http` and `https` are no longer part of the address, the scheme now specifies a resolver, so `Credentials` must be configured on channel options when using load balancing.
|
|
|
|
* `ChannelCredentials.SecureSsl` - gRPC calls are secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). Equivalent to an `https` address.
|
|
* `ChannelCredentials.Insecure` - gRPC calls don't use transport security. Equivalent to an `http` address.
|
|
|
|
```csharp
|
|
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" });
|
|
```
|
|
|
|
## Use load balancing with gRPC client factory
|
|
|
|
[gRPC client factory](xref:grpc/clientfactory) can be configured to use load balancing:
|
|
|
|
```csharp
|
|
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();
|
|
```
|
|
|
|
The preceding code:
|
|
|
|
* Configures the client with a load-balancing address.
|
|
* Specifies channel credentials.
|
|
* Registers DI types with the app's <xref:Microsoft.Extensions.DependencyInjection.IServiceCollection>.
|
|
|
|
## Write custom resolvers and load balancers
|
|
|
|
Client-side load balancing is extensible:
|
|
|
|
* Implement `Resolver` to create a custom resolver and resolve addresses from a new data source.
|
|
* Implement `LoadBalancer` to create a custom load balancer with new load balancing behavior.
|
|
|
|
> [!IMPORTANT]
|
|
> The APIs used to extend client-side load balancing are experimental. They can change without notice.
|
|
|
|
### Create a custom resolver
|
|
|
|
A resolver:
|
|
|
|
* Implements `Resolver` and is created by a `ResolverFactory`. Create a custom resolver by implementing these types.
|
|
* Is responsible for resolving the addresses a load balancer uses.
|
|
* Can optionally provide a service configuration.
|
|
|
|
```csharp
|
|
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 the preceding code:
|
|
|
|
* `FileResolverFactory` implements `ResolverFactory`. It maps to the `file` scheme and creates `FileResolver` instances.
|
|
* `FileResolver` implements `PollingResolver`. `PollingResolver` is an abstract base type that makes it easy to implement a resolver with asynchronous logic by overriding `ResolveAsync`.
|
|
* In `ResolveAsync`:
|
|
* The file URI is converted to a local path. For example, `file:///c:/addresses.json` becomes `c:\addresses.json`.
|
|
* JSON is loaded from disk and converted into a collection of addresses.
|
|
* Listener is called with results to let the channel know that addresses are available.
|
|
|
|
### Create a custom load balancer
|
|
|
|
A load balancer:
|
|
|
|
* Implements `LoadBalancer` and is created by a `LoadBalancerFactory`. Create a custom load balancer and factory by implementing these types.
|
|
* Is given addresses from a resolver and creates `Subchannel` instances.
|
|
* Tracks state about the connection and creates a `SubchannelPicker`. The channel internally uses the picker to pick addresses when making gRPC calls.
|
|
|
|
The `SubchannelsLoadBalancer` is:
|
|
|
|
* An abstract base class that implements `LoadBalancer`.
|
|
* Manages creating `Subchannel` instances from addresses.
|
|
* Makes it easy to implement a custom picking policy over a collection of subchannels.
|
|
|
|
```csharp
|
|
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 the preceding code:
|
|
|
|
* `RandomBalancerFactory` implements `LoadBalancerFactory`. It maps to the `random` policy name and creates `RandomBalancer` instances.
|
|
* `RandomBalancer` implements `SubchannelsLoadBalancer`. It creates a `RandomPicker` that randomly picks a subchannel.
|
|
|
|
## Configure custom resolvers and load balancers
|
|
|
|
Custom resolvers and load balancers need to be registered with dependency injection (DI) when they are used. There are a couple of options:
|
|
|
|
* If an app is already using DI, such as an ASP.NET Core web app, they can be registered with the existing DI configuration. An <xref:System.IServiceProvider> can be resolved from DI and passed to the channel using `GrpcChannelOptions.ServiceProvider`.
|
|
* If an app isn't using DI then create:
|
|
* A <xref:Microsoft.Extensions.DependencyInjection.ServiceCollection> with types registered with it.
|
|
* A service provider using <xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider*>.
|
|
|
|
```csharp
|
|
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);
|
|
```
|
|
|
|
The preceding code:
|
|
|
|
* Creates a `ServiceCollection` and registers new resolver and load balancer implementations.
|
|
* Creates a channel configured to use the new implementations:
|
|
* `ServiceCollection` is built into an `IServiceProvider` and set to `GrpcChannelOptions.ServiceProvider`.
|
|
* Channel address is `file:///c:/data/addresses.json`. The `file` scheme maps to `FileResolverFactory`.
|
|
* `service config` load balancer name is `random`. Maps to `RandomLoadBalancerFactory`.
|
|
|
|
## Why load balancing is important
|
|
|
|
HTTP/2 multiplexes multiple calls on a single TCP connection. If gRPC and HTTP/2 are used with a network load balancer (NLB), the connection is forwarded to a server, and all gRPC calls are sent to that one server. The other server instances on the NLB are idle.
|
|
|
|
Network load balancers are a common solution for load balancing because they are fast and lightweight. For example, Kubernetes by default uses a network load balancer to balance connections between pod instances. However, network load balancers are not effective at distributing load when used with gRPC and HTTP/2.
|
|
|
|
### Proxy or client-side load balancing?
|
|
|
|
gRPC and HTTP/2 can be effectively load balanced using either an application load balancer proxy or client-side load balancing. Both of these options allow individual gRPC calls to be distributed across available servers. Deciding between proxy and client-side load balancing is an architectural choice. There are pros and cons for each.
|
|
|
|
* **Proxy**: gRPC calls are sent to the proxy, the proxy makes a load balancing decision, and the gRPC call is sent on to the final endpoint. The proxy is responsible for knowing about endpoints. Using a proxy adds:
|
|
|
|
* An additional network hop to gRPC calls.
|
|
* Latency and consumes additional resources.
|
|
* Proxy server must be setup and configured correctly.
|
|
|
|
* **Client-side load balancing**: The gRPC client makes a load balancing decision when a gRPC call is started. The gRPC call is sent directly to the final endpoint. When using client-side load balancing:
|
|
|
|
* The client is responsible for knowing about available endpoints and making load balancing decisions.
|
|
* Additional client configuration is required.
|
|
* High-performance, load balanced gRPC calls eliminate the need for a proxy.
|
|
|
|
## Additional resources
|
|
|
|
* <xref:grpc/client>
|